¿Hay alguna razón para usar clases de "datos antiguos simples"?

43

En el código heredado ocasionalmente veo clases que no son más que envoltorios para datos. algo como:

class Bottle {
   int height;
   int diameter;
   Cap capType;

   getters/setters, maybe a constructor
}

Entiendo que OO es que las clases son estructuras para datos y los métodos para operar con esos datos. Esto parece excluir objetos de este tipo. Para mí no son más que una structsespecie de derrota del propósito de OO. No creo que sea necesariamente malo, aunque puede ser un olor a código.

¿Hay algún caso en el que tales objetos serían necesarios? Si esto se usa con frecuencia, ¿hace que el diseño sea sospechoso?

Michael K
fuente
1
Esto no responde a su pregunta, pero parece relevante: stackoverflow.com/questions/36701/struct-like-objects-in-java
Adam Lear
2
Creo que el término que está buscando es POD (Plain Old Data).
Gaurav
99
Este es un ejemplo típico de programación estructurada. No necesariamente malo, simplemente no orientado a objetos.
Konrad Rudolph
1
¿No debería estar esto en desbordamiento de pila?
Muad'Dib
77
@ Muad'Dib: técnicamente, se trata de programadores. A su compilador no le importa si usa estructuras de datos antiguas y simples. Probablemente su CPU lo disfrute (en la forma "Me encanta el olor de los datos recién salidos del caché"). Son las personas quienes se obsesionan con "¿esto hace que mi metodología sea menos pura?" preguntas
Shog9

Respuestas:

67

Definitivamente no es malvado ni un código de olor en mi mente. Los contenedores de datos son ciudadanos de OO válidos. A veces desea encapsular información relacionada juntos. Es mucho mejor tener un método como

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

que

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

El uso de una clase también le permite agregar un parámetro adicional a Bottle sin modificar cada llamada de DoStuffWithBottle. Y puede subclasificar Bottle y aumentar aún más la legibilidad y la organización de su código, si es necesario.

También hay objetos de datos simples que pueden devolverse como resultado de una consulta de base de datos, por ejemplo. Creo que el término para ellos en ese caso es "Objeto de transferencia de datos".

En algunos idiomas también hay otras consideraciones. Por ejemplo, en C # las clases y las estructuras se comportan de manera diferente, ya que las estructuras son un tipo de valor y las clases son tipos de referencia.

Adam Lear
fuente
25
Nah DoStuffWithdebería ser un método de la Bottleclase en OOP (y probablemente también debería ser inmutable). Lo que ha escrito anteriormente no es un buen patrón en un lenguaje OO (a menos que esté interactuando con una API heredada). Que es , sin embargo, un diseño válido en un entorno no-OO.
Konrad Rudolph
11
@Javier: Entonces Java tampoco es la mejor respuesta. El énfasis de Java en OO es abrumador, el lenguaje básicamente no sirve para nada más.
Konrad Rudolph
11
@ John: Por supuesto, estaba simplificando. Pero, en general, los objetos en estado OO encapsulan , no los datos. Esta es una distinción fina pero importante. El objetivo de OOP es precisamente no tener una gran cantidad de datos. Está enviando mensajes entre estados que crean un nuevo estado. No veo cómo puede enviar mensajes ao desde un objeto sin método. (Y esta es la definición original de OOP, así que no lo consideraría defectuoso.)
Konrad Rudolph
13
@Konrad Rudolph: Es por eso que explícitamente hice el comentario dentro del método. Estoy de acuerdo en que los métodos que afectan las instancias de Bottle deberían ir en esa clase. Pero si otro objeto necesita modificar su estado basado en la información en Botella, entonces creo que mi diseño sería bastante válido.
Adam Lear
10
@Konrad, no estoy de acuerdo con que doStuffWithBottle deba ir en la clase de botella. ¿Por qué una botella debe saber hacer cosas consigo misma? doStuffWithBottle indica que algo más hará algo con una botella. Si la botella tuviera eso, sería un acoplamiento apretado. Sin embargo, si la clase Bottle tuviera un método isFull (), eso sería totalmente apropiado.
Nemi
25

Las clases de datos son válidas en algunos casos. Los DTO son un buen ejemplo mencionado por Anna Lear. Sin embargo, en general, debe considerarlos como la semilla de una clase cuyos métodos aún no han brotado. Y si se encuentra con muchos de ellos en código antiguo, trátelos como un fuerte olor a código. A menudo los usan viejos programadores de C / C ++ que nunca han hecho la transición a la programación OO y son un signo de programación procesal. Confiar en getters y setters todo el tiempo (o peor aún, en el acceso directo de miembros no privados) puede meterte en problemas antes de que te des cuenta. Considere un ejemplo de un método externo que necesita información Bottle.

Aquí Bottlehay una clase de datos):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Aquí hemos dado Bottlealgún comportamiento):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

El primer método viola el principio Tell-Don't-Ask, y al mantenernos Bottletontos, hemos permitido que el conocimiento implícito sobre las botellas, como lo que hace que uno fragle (the Cap), se deslice a la lógica que está fuera de la Bottleclase. Tienes que estar alerta para evitar este tipo de "fuga" cuando habitualmente confías en los captadores.

El segundo método Bottlesolo pregunta qué necesita para hacer su trabajo, y deja Bottledecidir si es frágil o más grande que un tamaño determinado. El resultado es un acoplamiento mucho más flexible entre el método y la implementación de Bottle. Un efecto secundario agradable es que el método es más limpio y más expresivo.

Raramente utilizará tantos campos de un objeto sin escribir alguna lógica que debería residir en la clase con esos campos. Averigua cuál es esa lógica y luego muévela a donde pertenece.

Mike E
fuente
1
No puedo creer que esta respuesta no tenga votos (bueno, ahora tiene uno). Este podría ser un ejemplo simple, pero cuando se abusa lo suficiente de OO, obtienes clases de servicio que se convierten en pesadillas que contienen toneladas de funcionalidad que deberían haberse encapsulado en clases.
Alb
¿"por los viejos programadores de C / C ++ que nunca han hecho la transición (sic) a OO"? Los programadores de C ++ generalmente son bastante OO, ya que es un lenguaje OO, es decir, el objetivo de usar C ++ en lugar de C.
nappyfalcon
7

Si este es el tipo de cosas que necesitas, está bien, pero por favor, por favor, hazlo como

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

en lugar de algo como

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

Por favor.

compman
fuente
14
El único problema con esto es que no hay validación. Cualquier lógica sobre qué es una Botella válida debe estar en la clase Botella. Sin embargo, usando su implementación propuesta, puedo tener una botella con una altura y diámetro negativos: no hay forma de aplicar ninguna regla comercial en el objeto sin validar cada vez que se usa el objeto. Al usar el segundo método, puedo asegurarme de que si tengo un objeto Botella, fue, es y siempre será un objeto Botella válido de acuerdo con mi contrato.
Thomas Owens
66
Esa es un área donde .NET tiene una ligera ventaja sobre las propiedades, ya que puede agregar un descriptor de acceso de propiedad con lógica de validación, con la misma sintaxis que si estuviera accediendo a un campo. También puede definir una propiedad donde las clases pueden obtener el valor de la propiedad, pero no establecerlo.
JohnL
3
@ user9521 Si está seguro de que su código no causará un error fatal con valores "incorrectos", vaya a su método. Sin embargo, si necesita una validación adicional, o la capacidad de usar carga diferida u otras verificaciones cuando los datos se leen o escriben, entonces use getters y setters explícitos. Personalmente, tiendo a mantener mis variables privadas y uso getters y setters para mantener la coherencia. De esta forma, todas mis variables se tratan de la misma manera, independientemente de la validación y / u otras técnicas "avanzadas".
Jonathan
2
La ventaja de usar el constructor es que hace que sea mucho más fácil hacer que la clase sea inmutable. Esto es esencial si desea escribir código multiproceso.
Fortyrunner
77
Haría los campos finales siempre que sea posible. En mi humilde opinión, hubiera preferido que los campos sean finales por defecto y tengan una palabra clave para campos mutables. eg var
Peter Lawrey
6

Como dijo @Anna, definitivamente no es malo. Claro que puede poner operaciones (métodos) en clases, pero solo si lo desea . Usted no tiene a.

Permítame una pequeña queja sobre la idea de que tiene que poner operaciones en las clases, junto con la idea de que las clases son abstracciones. En la práctica, esto alienta a los programadores a

  1. Haga más clases de las que necesitan (estructura de datos redundante). Cuando una estructura de datos contiene más componentes de los mínimos necesarios, no está normalizada y, por lo tanto, contiene estados inconsistentes. En otras palabras, cuando se altera, debe alterarse en más de un lugar para mantenerse consistente. No realizar todos los cambios coordinados lo hace inconsistente y es un error.

  2. Resuelva el problema 1 colocando métodos de notificación , de modo que si se modifica la parte A, intente propagar los cambios necesarios a las partes B y C. Esta es la razón principal por la que se recomienda tener métodos de acceso para obtener y establecer. Dado que esta es una práctica recomendada, parece disculpar el problema 1, causando más del problema 1 y más de la solución 2. Esto da como resultado no solo errores debidos a la implementación incompleta de las notificaciones, sino a un problema de disminución de rendimiento de las notificaciones fuera de control. Estos no son cálculos infinitos, sino muy largos.

Estos conceptos se enseñan como cosas buenas, generalmente por maestros que no han tenido que trabajar dentro de aplicaciones monstruosas de millones de líneas plagadas de estos problemas.

Esto es lo que trato de hacer:

  1. Mantenga los datos lo más normalizados posible, de modo que cuando se realice un cambio en los datos, se haga en el menor número de puntos de código posible, para minimizar la probabilidad de entrar en un estado inconsistente.

  2. Cuando los datos no se deben normalizar y la redundancia es inevitable, no use notificaciones para mantener la coherencia. Más bien, tolere la inconsistencia temporal. Resuelva la inconsistencia con barridos periódicos a través de los datos mediante un proceso que solo hace eso. Esto centraliza la responsabilidad de mantener la coherencia, al tiempo que evita los problemas de rendimiento y corrección a los que son susceptibles las notificaciones. Esto da como resultado un código mucho más pequeño, sin errores y eficiente.

Mike Dunlavey
fuente
3

Este tipo de clases son bastante útiles cuando se trata de aplicaciones medianas / grandes, por algunas razones:

  • es bastante fácil crear algunos casos de prueba y garantizar que los datos sean consistentes.
  • contiene todo tipo de comportamientos que involucran esa información, por lo que se reduce el tiempo de seguimiento de errores de datos
  • Usarlos debería mantener el método args liviano.
  • Cuando se usan ORM, esta clase brinda flexibilidad y consistencia. Al agregar un atributo complejo que se calcula en base a información simple que ya está en la clase, se resuelve al escribir un método simple. Esto es bastante más ágil y productivo que tener que verificar su base de datos y asegurarse de que todas las bases de datos estén parcheadas con nuevas modificaciones.

En resumen, en mi experiencia, generalmente son más útiles que molestos.

Guiman
fuente
3

Con el diseño del juego, la sobrecarga de miles de llamadas a funciones y los oyentes de eventos a veces pueden hacer que valga la pena tener clases que solo almacenen datos, y tener otras clases que recorran todas las clases de solo datos para realizar la lógica.

AttackingHobo
fuente
3

De acuerdo con Anna Lear,

Definitivamente no es malvado ni un código de olor en mi mente. Los contenedores de datos son ciudadanos de OO válidos. A veces desea encapsular información relacionada juntos. Es mucho mejor tener un método como ...

A veces las personas se olvidan de leer las Convenciones de codificación de Java de 1999 que dejan muy claro que este tipo de programación está perfectamente bien. De hecho, si lo evita, ¡su código huele mal! (demasiados captadores / setters)

De Java Code Conventions 1999: Un ejemplo de variables de instancia pública apropiadas es el caso donde la clase es esencialmente una estructura de datos, sin comportamiento. En otras palabras, si hubiera utilizado una estructura en lugar de una clase (si Java admite estructura), entonces es apropiado hacer públicas las variables de instancia de la clase. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177

Cuando se usan correctamente, los POD (estructuras de datos antiguas) son mejores que los POJO, al igual que los POJO a menudo son mejores que los EJB.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures

Richard Faber
fuente
3

Las estructuras tienen su lugar, incluso en Java. Solo debe usarlos si las siguientes dos cosas son ciertas:

  • Solo necesita agregar datos que no tengan ningún comportamiento, por ejemplo, pasar como parámetro
  • No importa un poco qué tipo de valores tienen los datos agregados

Si este es el caso, debe hacer públicos los campos y omitir los captadores / establecedores. Getters y setters son torpes de todos modos, y Java es tonto por no tener propiedades como un lenguaje útil. Dado que su objeto tipo estructura no debería tener ningún método de todos modos, los campos públicos tienen más sentido.

Sin embargo, si alguno de esos no se aplica, se trata de una clase real. Eso significa que todos los campos deben ser privados. (Si realmente necesita un campo en un alcance más accesible, use un captador / definidor).

Para verificar si su supuesta estructura tiene comportamiento, observe cuándo se usan los campos. Si parece violar la información , no preguntes , entonces debes mover ese comportamiento a tu clase.

Si algunos de sus datos no cambian, entonces debe hacer que todos esos campos sean finales. Puede considerar hacer que su clase sea inmutable . Si necesita validar sus datos, proporcione la validación en los instaladores y constructores. (Un truco útil es definir un setter privado y modificar su campo dentro de su clase usando solo ese setter).

Su ejemplo de botella probablemente fallaría en ambas pruebas. Podría tener un código (artificial) que se vea así:

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

En cambio debería ser

double volume = bottle.calculateVolumeAsCylinder();

Si cambiara la altura y el diámetro, ¿sería la misma botella? Probablemente no. Esos deberían ser finales. ¿Está bien un valor negativo para el diámetro? ¿Debe su botella ser más alta que ancha? ¿Puede la gorra ser nula? ¿No? ¿Cómo estás validando esto? Suponga que el cliente es estúpido o malvado. ( Es imposible notar la diferencia ) . Debe verificar estos valores.

Así es como podría verse su nueva clase de botella:

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}
Eva
fuente
0

En mi humilde opinión, a menudo no hay suficientes clases como esta en sistemas muy orientados a objetos. Necesito calificar eso cuidadosamente.

Por supuesto, si los campos de datos tienen un amplio alcance y visibilidad, eso puede ser extremadamente indeseable si hay cientos o miles de lugares en su base de código que alteran dichos datos. Eso es pedir problemas y dificultades para mantener invariantes. Sin embargo, al mismo tiempo, eso no significa que todas las clases de la base de código completa se beneficien de la ocultación de información.

Pero hay muchos casos en los que dichos campos de datos tendrán un alcance muy limitado. Un ejemplo muy sencillo es una Nodeclase privada de una estructura de datos. A menudo puede simplificar mucho el código al reducir la cantidad de interacciones de objetos que suceden si se trata Nodesimplemente de datos sin procesar. Que sirve como un mecanismo de desacoplamiento ya que la versión alternativa podría requerir acoplamiento bidireccional de, por ejemplo, Tree->Nodey Node->Treeen lugar de simplemente Tree->Node Data.

Un ejemplo más complejo serían los sistemas de componentes de entidad, como se usan a menudo en los motores de juego. En esos casos, los componentes a menudo son solo datos sin procesar y clases como la que ha mostrado. Sin embargo, su alcance / visibilidad tiende a ser limitado ya que generalmente solo hay uno o dos sistemas que pueden acceder a ese tipo particular de componente. Como resultado, todavía tiende a encontrar bastante fácil mantener invariantes en esos sistemas y, además, estos sistemas tienen muy pocas object->objectinteracciones, lo que hace que sea muy fácil comprender lo que está sucediendo a nivel del ojo del pájaro.

En tales casos, puede terminar con algo más como esto en lo que respecta a las interacciones (este diagrama indica interacciones, no acoplamiento, ya que un diagrama de acoplamiento puede incluir interfaces abstractas para la segunda imagen a continuación):

ingrese la descripción de la imagen aquí

... a diferencia de esto:

ingrese la descripción de la imagen aquí

... y el primer tipo de sistema tiende a ser mucho más fácil de mantener y razonar en términos de corrección a pesar del hecho de que las dependencias realmente fluyen hacia los datos. Obtiene mucho menos acoplamiento principalmente porque muchas cosas pueden convertirse en datos sin procesar en lugar de objetos que interactúan entre sí formando un gráfico muy complejo de interacciones.


fuente
-1

Incluso existen criaturas mucho más extrañas en el mundo OOP. Una vez he creado una clase, que es abstracta y no contiene nada, solo para ser un padre común de otras dos clases ... es la clase Port:

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends Port

Por lo tanto, SceneMember es la clase base, hace todo el trabajo, que debe aparecer en la escena: agregar, eliminar, obtener ID de configuración, etc. El mensaje es la conexión entre puertos, tiene su propia vida. InputPort y OutputPort contienen las funciones específicas de E / S propias.

El puerto está vacío Solo se usa para agrupar InputPort y OutputPort juntos para, por ejemplo, una lista de puertos. Está vacío porque SceneMember contiene todas las cosas que son para actuar como miembro, y InputPort y OutputPort contienen las tareas especificadas por el puerto. InputPort y OutputPort son tan diferentes que no tienen funciones comunes (excepto SceneMember). Entonces, el puerto está vacío. Pero es legal.

Quizás sea un antipatrón , pero me gusta.

(Es como la palabra "compañero", que se usa tanto para "esposa" como para "esposo". Nunca se usa la palabra "compañero" para una persona concreta, porque conoce el sexo de él / ella, y no importa si él / ella está casado o no, entonces usa "alguien" en su lugar, pero todavía existe y se usa en situaciones raras y abstractas, por ejemplo, un texto de ley).

ern0
fuente
1
¿Por qué sus puertos necesitan extender SceneMember? ¿Por qué no crear las clases de puerto para operar en SceneMembers?
Michael K
1
Entonces, ¿por qué no usar el patrón de interfaz de marcador estándar? Básicamente idéntico a su clase base abstracta vacía, pero es un idioma mucho más común.
TMN
1
@Michael: Solo por razones teóricas, reservé Port para uso futuro, pero este futuro aún no ha llegado, y tal vez nunca lo haga. No me di cuenta de que no tienen nada en común, a diferencia de lo que suponen sus nombres. Voy a pagar una indemnización por todos los que sufrió ninguna pérdida se produjo por esa clase vacía ...
ern0
1
@ TMN: los tipos de SceneMember (derivado) tienen el método getMemberType (), hay varios casos en que Scene se escanea en busca de un subconjunto de objetos SceneMember.
ern0
44
Esto no responde la pregunta.
Eva