¿Separar la mayoría de las clases en clases de solo campo de datos y solo de método (si es posible) es un buen o un antipatrón?

10

Por ejemplo, una clase generalmente tiene miembros y métodos de clase, por ejemplo:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public void printInfo(){
        System.out.println("Name:"+this.name+",weight:"+this.weight);
    }

    public void draw(){
        //some draw code which uses this.image
    }
}

Pero después de leer sobre el principio de responsabilidad única y el principio de apertura cerrada, prefiero separar una clase en DTO y clase auxiliar con métodos estáticos solamente, por ejemplo:

public class CatData{
    public String name;
    public int weight;
    public Image image;
}

public class CatMethods{
    public static void printInfo(Cat cat){
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat){
        //some draw code which uses cat.image
    }
}

Creo que se ajusta al principio de responsabilidad única porque ahora la responsabilidad de CatData es mantener solo los datos, no le importan los métodos (también para CatMethods). Y también se ajusta al principio abierto cerrado porque agregar nuevos métodos no necesita cambiar la clase CatData.

Mi pregunta es, ¿es un buen o un antipatrón?

ocomfd
fuente
15
Hacer algo a ciegas en todas partes porque aprendiste un nuevo concepto es siempre un antipatrón.
Kayaman
44
¿Cuál sería el punto de las clases si siempre es mejor tener datos separados de los métodos que los modifican? Eso suena como una programación no orientada a objetos (que ciertamente tiene su lugar). Dado que esto está etiquetado como "orientado a objetos", supongo que desea utilizar las herramientas que proporciona OOP.
user1118321
44
Otra pregunta que demuestra que SRP está tan mal entendido que debería prohibirse. Mantener los datos en un montón de campos públicos no es una responsabilidad .
user949300
1
@ user949300, si la clase es responsable de esos campos, moverlos a otra parte de la aplicación, por supuesto, no tendrá ningún efecto. O para decirlo de otra manera, te equivocas: son 100% una responsabilidad.
David Arno
2
@DavidArno y R Schmitz: contener campos no cuenta como una responsabilidad a los fines de SRP. Si lo hiciera, no podría haber OOP, ya que todo sería un DTO, y luego las clases separadas contendrían procedimientos para trabajar en los datos. Una única responsabilidad corresponde a un solo interesado . (Aunque esto parece cambiar ligeramente en cada iteración del principal)
user949300

Respuestas:

10

Ha mostrado dos extremos ("todo privado y todos los métodos (quizás no relacionados) en un objeto" frente a "todo público y ningún método dentro del objeto"). En mi humilde opinión, el buen modelado OO no es ninguno de ellos , el punto óptimo está en algún lugar en el medio.

Una prueba decisiva de qué métodos o lógica pertenece a una clase, y qué pertenece fuera, es observar las dependencias que los métodos introducirán. Los métodos que no introducen dependencias adicionales están bien, siempre que se ajusten bien a la abstracción del objeto dado . Los métodos que requieren dependencias externas adicionales (como una biblioteca de dibujo o una biblioteca de E / S) rara vez se ajustan bien. Incluso si hiciera desaparecer las dependencias mediante el uso de la inyección de dependencias, todavía pensaría dos veces si es realmente necesario colocar dichos métodos dentro de la clase de dominio.

Por lo tanto, no debe hacer que todos los miembros sean públicos, ni necesita implementar métodos para cada operación en un objeto dentro de la clase. Aquí hay una sugerencia alternativa:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public String getInfo(){
        return "Name:"+this.name+",weight:"+this.weight;
    }
    public Image getImage(){
        return image;
    }
}

Ahora el Catobjeto proporciona suficiente lógica para permitir que el código circundante se implemente fácilmente printInfoy drawsin exponer todos los atributos en público. El lugar adecuado para estos dos métodos no es muy probablemente una clase de dios CatMethods(ya printInfoy drawson muy probablemente diferentes preocupaciones, así que creo que es muy poco probable que pertenecen a la misma clase).

Me puedo imaginar una CatDrawingControllerque implemente draw(y tal vez use una inyección de dependencia para obtener un objeto Canvas). También puedo imaginar otra clase que implementa algunos resultados y usos de la consola getInfo(por lo que printInfopuede volverse obsoleto en este contexto). Pero para tomar decisiones sensatas sobre esto, uno necesita conocer el contexto y cómo se Catusará realmente la clase.

Esa es realmente la forma en que interpreté los críticos del Modelo de dominio anémico de Fowler : para una lógica generalmente reutilizable (sin dependencias externas), las clases de dominio en sí mismas son un buen lugar, por lo que deberían usarse para eso. Pero eso no significa implementar ninguna lógica allí, sino todo lo contrario.

Tenga en cuenta también que el ejemplo anterior todavía deja espacio para tomar una decisión sobre la (im) mutabilidad. Si la Catclase no expondrá ningún setter, y Imagees inmutable en sí misma, este diseño permitirá hacer Catinmutable (lo que no hará el enfoque DTO). Pero si cree que la inmutabilidad no es necesaria o no es útil para su caso, también puede ir en esa dirección.

Doc Brown
fuente
Dispara a los votantes felices, esto se vuelve aburrido. Si tiene algunas críticas, hágamelo saber. Estaré encantado de aclarar cualquier malentendido, si tiene alguno.
Doc Brown
Aunque generalmente estoy de acuerdo con tu respuesta ... creo que getInfo()podría ser engañoso para alguien que hace esta pregunta. Mezcla sutilmente las responsabilidades de presentación como lo printInfo()hace, derrotando el propósito de la DrawingControllerclase
Adriano Repetti
@AdrianoRepetti No veo que esté frustrando el propósito de DrawingController. Estoy de acuerdo con eso getInfo()y printInfo()son nombres horribles. Considere toString()yprintTo(Stream stream)
candied_orange
@candid no se trata (solo) del nombre sino de lo que hace. Ese código se trata solo de un detalle de presentación y efectivamente simplemente abstraemos la salida, no el contenido procesado y su lógica.
Adriano Repetti
@AdrianoRepetti: honestamente, este es solo un ejemplo artificial, para ajustarse a la pregunta original y demostrar mi punto. En un sistema real, habrá un contexto circundante que permitirá tomar decisiones sobre mejores métodos y nombres, por supuesto.
Doc Brown
6

Respuesta tardía pero no puedo resistirme.

¿Es X la mayoría de las clases en Y buenas o un antipatrón?

En la mayoría de los casos, la mayoría de las reglas, aplicadas sin pensar, irán terriblemente mal (incluida esta).

Permíteme contarte una historia sobre el nacimiento de un objeto en medio del caos de un código de procedimiento correcto, rápido y sucio que ocurrió, no por diseño, sino por desesperación.

Mi pasante y yo estamos programando pares para crear rápidamente un código desechable para raspar una página web. No tenemos absolutamente ninguna razón para esperar que este código dure mucho, por lo que solo estamos eliminando algo que funciona. Tomamos toda la página como una cadena y cortamos las cosas que necesitamos de la manera más increíblemente frágil que puedas imaginar. No juzgues Funciona.

Ahora, mientras hacía esto, creé algunos métodos estáticos para cortar. Mi pasante creó una clase de DTO que se parecía mucho a la tuya CatData.

Cuando vi por primera vez el DTO me molestó. Los años de daño que Java ha causado en mi cerebro me hicieron retroceder en los campos públicos. Pero estábamos trabajando en C #. C # no necesita getters y setters prematuros para preservar su derecho a hacer que los datos sean inmutables o encapsulados más tarde. Sin cambiar la interfaz, puede agregarlos cuando lo desee. Tal vez solo para poder establecer un punto de interrupción. Todo sin decirles nada a sus clientes. Sí C #. Boo Java.

Así que me contuve la lengua. Vi como él usaba mis métodos estáticos para inicializar esto antes de usarlo. Teníamos unos 14 de ellos. Era feo, pero no teníamos motivos para preocuparnos.

Luego lo necesitábamos en otros lugares. Nos encontramos queriendo copiar y pegar el código. Se lanzaron 14 líneas de inicialización. Estaba empezando a ser doloroso. Dudó y me pidió ideas.

De mala gana le pregunté, "¿considerarías un objeto?"

Volvió a mirar su DTO y frunció el ceño confundido. "Es un objeto".

"Me refiero a un objeto real"

"¿Eh?"

"Déjame mostrarte algo. Tú decides si es útil"

Elegí un nuevo nombre y rápidamente preparé algo que se parecía un poco a esto:

public class Cat{
    CatData(string catPage) {
        this.catPage = catPage
    }
    private readonly string catPage;

    public string name() { return chop("name prefix", "name suffix"); }
    public string weight() { return chop("weight prefix", "weight suffix"); }
    public string image() { return chop("image prefix", "image suffix"); }

    private string chop(string prefix, string suffix) {
        int start = catPage.indexOf(prefix) + prefix.Length;
        int end = catPage.indexOf(suffix);
        int length = end - start;
        return catPage.Substring(start, length);
    }
}

Esto no hizo nada que los métodos estáticos no estuvieran haciendo. Pero ahora había absorbido los 14 métodos estáticos en una clase donde podían estar solos con los datos en los que trabajaban.

No forcé a mi interno a usarlo. Simplemente lo ofrecí y le dejé decidir si quería seguir con los métodos estáticos. Me fui a casa pensando que probablemente se apegaría a lo que ya tenía trabajando. Al día siguiente descubrí que lo estaba usando en muchos lugares. Descomprimió el resto del código que todavía era feo y procesal, pero esta parte de complejidad ahora estaba oculta para nosotros detrás de un objeto. Fue un poco mejor.

Ahora, seguro, cada vez que acceda a esto, está haciendo un buen trabajo. Un DTO es un buen valor rápido en caché. Me preocupé por eso, pero me di cuenta de que podía agregar el almacenamiento en caché si alguna vez lo necesitáramos sin tocar ninguno de los códigos de uso. Así que no me voy a molestar hasta que nos importe.

¿Estoy diciendo que siempre debes apegarte a los objetos OO sobre los DTO? No. El DTO brilla cuando necesita cruzar un límite que le impide moverse. Los DTO tienen su lugar.

Pero también lo hacen los objetos OO. Aprende a usar ambas herramientas. Aprende lo que cuesta cada uno. Aprenda a dejar que el problema, la situación y el interno decidan. Dogma no es tu amigo aquí.


Dado que mi respuesta ya es ridículamente larga, permítame desacreditarle algunos conceptos erróneos con una revisión de su código.

Por ejemplo, una clase generalmente tiene miembros y métodos de clase, por ejemplo:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public void printInfo(){
        System.out.println("Name:"+this.name+",weight:"+this.weight);
    }

    public void draw(){
        //some draw code which uses this.image
    }
}

¿Dónde está tu constructor? Esto no me muestra lo suficiente como para saber si es útil.

Pero después de leer sobre el principio de responsabilidad única y el principio de apertura cerrada, prefiero separar una clase en DTO y clase auxiliar con métodos estáticos solamente, por ejemplo:

public class CatData{
    public String name;
    public int weight;
    public Image image;
}

public class CatMethods{
    public static void printInfo(Cat cat){
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat){
        //some draw code which uses cat.image
    }
}

Creo que se ajusta al principio de responsabilidad única porque ahora la responsabilidad de CatData es mantener solo los datos, no le importan los métodos (también para CatMethods).

Puede hacer muchas cosas tontas en nombre del Principio de responsabilidad única. Podría argumentar que las cadenas de gato y las entradas de gato deben estar separadas. Que los métodos de dibujo y las imágenes deben tener su propia clase. Que su programa en ejecución es una responsabilidad única, por lo que solo debe tener una clase. :PAG

Para mí, la mejor manera de seguir el Principio de responsabilidad única es encontrar una buena abstracción que le permita poner la complejidad en una caja para que pueda ocultarla. Si puede darle un buen nombre que evite que las personas se sorprendan por lo que encuentran cuando miran dentro, lo ha seguido bastante bien. Esperar que dicte más decisiones que eso es pedir problemas. Honestamente, ambos listados de código lo hacen, así que no veo por qué SRP importa aquí.

Y también se ajusta al principio abierto cerrado porque agregar nuevos métodos no necesita cambiar la clase CatData.

Bueno no. El principio de abrir y cerrar no se trata de agregar nuevos métodos. Se trata de poder cambiar la implementación de métodos antiguos y no editar nada. Nada que te use y no tus viejos métodos. En su lugar, escribe un código nuevo en otro lugar. Alguna forma de polimorfismo lo hará muy bien. No veo eso aquí.

Mi pregunta es, ¿es un buen o un antipatrón?

Bueno demonios, ¿cómo debería saberlo? Mire, hacerlo de cualquier manera tiene beneficios y costos. Cuando separa el código de los datos, puede cambiarlos sin tener que volver a compilar el otro. Tal vez eso es críticamente importante para ti. Tal vez solo haga que su código sea innecesariamente complicado.

Si te hace sentir mejor, no estás tan lejos de algo que Martin Fowler llama un objeto de parámetro . No tiene que tomar solo primitivas en su objeto.

Lo que me gustaría que hicieras es desarrollar una idea de cómo hacer tu separación, o no, en cualquier estilo de codificación. Porque lo creas o no, no estás obligado a elegir un estilo. Solo tienes que vivir con tu elección.

naranja confitada
fuente
2

Te has topado con un tema de debate que ha estado creando discusiones entre los desarrolladores durante más de una década. En 2003, Martin Fowler acuñó la frase "Modelo de dominio anémico" (ADM) para describir esta separación de datos y funcionalidad. Él, y otros que están de acuerdo con él, argumentan que los "Modelos de dominio enriquecido" (mezcla de datos y funcionalidad) son "OO adecuados", y que el enfoque ADM es un antipatrón no OO.

Siempre ha habido quienes rechazan este argumento y ese lado del argumento se ha vuelto más fuerte y audaz en los últimos años con la adopción por parte de más desarrolladores de técnicas de desarrollo funcional. Ese enfoque fomenta activamente la separación de datos y preocupaciones de función. Los datos deben ser inmutables tanto como sea posible, por lo que la encapsulación del estado mutable se convierte en una preocupación. No es beneficioso adjuntar funciones directamente a esos datos en tales situaciones. Si es entonces "no OO" o no, no tiene absolutamente ningún interés para esa gente.

Independientemente de en qué lado de la cerca te sientes (me siento muy firmemente en el lado "Martin Fowler está hablando un montón de cosas viejas"), tu uso de métodos estáticos printInfoy drawestá casi universalmente mal visto. Es difícil burlarse de los métodos estáticos al escribir pruebas unitarias. Por lo tanto, si tienen efectos secundarios (como imprimir o dibujar en una pantalla u otro dispositivo), no deberían ser estáticos, o deberían pasar la ubicación de salida como un parámetro.

Entonces podrías tener una interfaz:

public interface CatMethods {
    void printInfo(Cat cat);
    void draw(Cat cat);
}

Y una implementación que se inyecta en el resto de su sistema en tiempo de ejecución (con otras implementaciones que se utilizan para las pruebas):

internal class CatMethodsForScreen implements CatMethods {
    public void printInfo(Cat cat) {
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public void draw(Cat cat) {
        //some draw code which uses cat.image
    }
}

O agregue parámetros adicionales para eliminar los efectos secundarios de esos métodos:

public static class CatMethods {
    public static void printInfo(Cat cat, OutputHandler output) {
        output.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat, Canvas canvas) {
        //some draw code which uses cat.image and draws it on canvas
    }
}
David Arno
fuente
Pero, ¿por qué tratarías de calzar un lenguaje OO como Java en un lenguaje funcional, en lugar de utilizar un lenguaje funcional desde el principio? ¿Es algún tipo de "Odio Java, pero todos lo usan, así que tengo que hacerlo, pero al menos voy a escribirlo a mi manera". A menos que esté afirmando que el paradigma OO es obsoleto y desactualizado de alguna manera. Me suscribo a la escuela de pensamiento "Si quieres X, ya sabes dónde conseguirlo".
Kayaman
@Kayaman, " Me suscribo a la" escuela de pensamiento ". Si quieres X, ya sabes dónde conseguirlo ". Yo no. Considero que esa actitud es miope y ligeramente ofensiva. No uso Java, pero uso C # e ignoro algunas de las características "reales OO". No me interesan la herencia, los objetos con estado y similares, y escribo muchos métodos estáticos y clases selladas (finales). Las herramientas y el tamaño de la comunidad en torno a C # son insuperables. Para F #, es mucho más pobre. Así que me suscribo a la escuela de pensamiento "Si quieres X, pide que se agregue a tu herramienta actual".
David Arno
1
Yo diría que ignorar los aspectos de un idioma es muy diferente de tratar de mezclar y combinar características de otros idiomas. Tampoco trato de escribir "OO real", ya que tratar de seguir las reglas (o principios) en una ciencia inexacta como la programación no funciona. Por supuesto, necesita conocer las reglas para poder romperlas. Agregar características a ciegas puede ser malo para el desarrollo del lenguaje. Sin ofender, pero en mi humilde opinión, una parte de la escena de FP parece sentir "FP es lo mejor desde el pan rebanado, ¿por qué la gente no lo entiende". Nunca diría (o pensaría) OO o Java es "el mejor".
Kayaman
1
@Kayaman, pero ¿qué significa "tratar de mezclar y combinar características de otros idiomas"? ¿Está argumentando que las características funcionales no deberían agregarse a Java, porque "Java es un lenguaje OO"? Si es así, ese es un argumento miope en mi opinión. Deje que el lenguaje evolucione con las necesidades de los desarrolladores. Creo que obligarlos a cambiar a otro idioma es el enfoque equivocado. Claro que algunos "defensores de la PF" van más allá de lo que dice que la PF es lo mejor. Y algunos "defensores de OO" se exageran al decir que OO "ganó la batalla porque es claramente superior". Ignóralos a ambos.
David Arno
1
No estoy siendo anti-FP. Lo uso en Java donde es útil. Pero si un programador dice "Quiero esto" es como un cliente que dice "Quiero esto". Los programadores no son diseñadores de idiomas y los clientes no son programadores. Voté su respuesta ya que usted dice que la "solución" de OP está mal vista. Sin embargo, el comentario en la pregunta es más sucinto, es decir, ha reinventado la programación de procedimientos en un lenguaje OO. Sin embargo, la idea general tiene un punto, como lo demostró. Un modelo de dominio no anémico no significa que los datos y el comportamiento sean exclusivos, y que los renderizadores o presentadores externos estarían prohibidos.
Kayaman
0

DTO: objetos de transporte de datos

son útiles solo para eso. Si va a desviar datos entre programas o sistemas, los DTO son deseables, ya que proporcionan un subconjunto manejable de un objeto que solo se ocupa de la estructura y el formato de los datos. La ventaja es que no tiene que sincronizar las actualizaciones de métodos complejos en varios sistemas (siempre que los datos subyacentes no cambien).

El objetivo de OO es vincular los datos y el código que actúa sobre esos datos muy juntos. Separar un objeto lógico en clases separadas suele ser una mala idea.

James Anderson
fuente
-1

Muchos patrones y principios de diseño entran en conflicto y, en última instancia, solo su criterio como buen programador lo ayudará a decidir qué patrones deben aplicarse a un problema específico.

En mi opinión, el principio Hacer lo más simple que posiblemente podría funcionar debería ganar por defecto a menos que tenga una buena razón para hacer que el diseño sea más complicado. Entonces, en su ejemplo específico, optaría por el primer diseño, que es un enfoque orientado a objetos más tradicional con datos y funciones juntas en la misma clase.

Aquí hay un par de preguntas que puede hacerse sobre la aplicación:

  • ¿Alguna vez tendré que proporcionar una forma alternativa de renderizar una imagen Cat? Si es así, ¿la herencia no será una forma conveniente de hacerlo?
  • ¿Mi clase Cat necesita heredar de otra clase o satisfacer una interfaz?

Si responde que sí a cualquiera de estas preguntas, podría obtener un diseño más complejo con DTO y clases funcionales separadas.

Jordan Rieger
fuente
-1

¿Es un buen patrón?

Probablemente obtendrá diferentes respuestas aquí porque las personas siguen diferentes escuelas de pensamiento. Entonces, incluso antes de comenzar: esta respuesta está de acuerdo con la programación orientada a objetos como la describe Robert C. Martin en el libro "Código limpio".


"Verdadero" POO

Que yo sepa, hay 2 interpretaciones de OOP. Para la mayoría de las personas, se reduce a "un objeto es una clase".

Sin embargo, la otra escuela de pensamiento me pareció más útil: "Un objeto es algo que hace algo". Si está trabajando con clases, cada clase sería una de dos cosas: un " titular de datos " o un objeto . Los titulares de datos tienen datos (duh), los objetos hacen cosas. ¡Eso no significa que un titular de datos no pueda tener métodos! Piense en un list: A listrealmente no hace nada, pero tiene métodos para imponer la forma en que almacena los datos .

Titulares de datos

Usted Cates un excelente ejemplo de un titular de datos. Claro, fuera de su programa, un gato es algo que hace algo , pero dentro de su programa, un Cates una Cadena name, un int weighty una Imagen image.

¡Que los titulares de datos no hagan nada tampoco significa que no contengan lógica empresarial! Si su Catclase tiene un constructor que lo requiere name, weighty imageha encapsulado con éxito la regla de negocio que cada gato tiene. Si puede cambiar weightdespués de que Catse haya creado una clase, esa es otra regla empresarial encapsulada. Estas son cosas muy básicas, pero eso solo significa que es muy importante no equivocarse, y de esta manera, se asegura de que sea imposible equivocarse.

Objetos

Los objetos hacen algo. Si desea objetos Clean *, podemos cambiarlo a "Los objetos hacen una cosa".

Imprimir la información del gato es el trabajo de un objeto. Por ejemplo, podría llamar a este objeto " CatInfoLogger", con un método público Log(Cat). Eso es todo lo que necesitamos saber desde el exterior: este objeto registra la información de un gato y para hacerlo, necesita un Cat.

En el interior, este objeto tendría referencias a lo que sea necesario para cumplir con su responsabilidad única de registrar a los gatos. En este caso, eso es solo una referencia para Systemfor System.out.println, pero en la mayoría de los casos, serán referencias privadas a otros objetos. Si la lógica para formatear la salida antes de imprimirla se vuelve demasiado compleja, CatInfoLoggersolo puede obtener una referencia a un nuevo objeto CatInfoFormatter. Si más adelante, cada registro también debe escribirse en un archivo, puede agregar un CatToFileLoggerobjeto que lo haga.

Titulares de datos vs objetos - resumen

Ahora, ¿por qué harías todo eso? Porque ahora cambiar una clase cambia solo una cosa ( la forma en que se registra un gato en el ejemplo). Si está cambiando un objeto, solo está cambiando la forma en que se realiza una determinada acción; si cambia un titular de datos, solo cambia qué datos se retienen y / o de qué manera se conservan.

Cambiar los datos puede significar que la forma en que se hace algo también tiene que cambiar, pero ese cambio no sucederá hasta que lo haga usted mismo cambiando el objeto responsable.

¿Exceso?

Esta solución puede parecer un poco exagerada. Déjame ser sincero: lo es . Si todo su programa es tan grande como el ejemplo, no haga nada de lo anterior, solo elija cualquiera de las formas propuestas con un lanzamiento de moneda. No hay peligro de confundir a otros códigos si no es ningún otro código.

Sin embargo, cualquier software "serio" (= más que un script o 2) es probablemente lo suficientemente grande como para beneficiarse de una estructura como esta.


Responder

¿Separar la mayoría de las clases en DTO y clases auxiliares [...] es bueno o un antipatrón?

Convertir "la mayoría de las clases" (sin ninguna calificación adicional) en DTO / titulares de datos, no es un anti patrón, porque en realidad no es ningún patrón, es más o menos aleatorio.

Sin embargo, mantener los datos y los objetos separados se considera una buena manera de mantener limpio su código *. A veces solo necesitará un DTO plano y "anémico" y eso es todo. Otras veces, necesita métodos para imponer la forma en que se almacenan los datos. Simplemente no ponga la funcionalidad de su programa con los datos.


* Eso es "Limpio" con una C mayúscula como el título del libro, porque, recuerda el primer párrafo, se trata de una escuela de pensamiento, mientras que también hay otras.

R. Schmitz
fuente