oop - patrones - Principio de responsabilidad individual frente a modelo de dominio anémico antipatrón
patrones de diseño java (7)
Estoy en un proyecto que toma el Principio de Responsabilidad Individual bastante en serio. Tenemos muchas clases pequeñas y las cosas son bastante simples. Sin embargo, tenemos un modelo de dominio anémico: no hay comportamiento en ninguna de nuestras clases modelo, solo son bolsas de propiedades. Esto no es una queja sobre nuestro diseño, parece que funciona bastante bien
Durante las revisiones de diseño, SRP se lanza cada vez que se agrega un nuevo comportamiento al sistema, por lo que el nuevo comportamiento típicamente termina en una nueva clase. Esto hace que las cosas se comprueben muy fácilmente por unidad, pero a veces me siento perplejo porque se siente como sacar el comportamiento del lugar donde es relevante.
Estoy tratando de mejorar mi comprensión de cómo aplicar SRP correctamente. Me parece que SRP se opone a agregar comportamiento de modelado empresarial que comparte el mismo contexto con un objeto, porque el objeto inevitablemente termina haciendo más de una cosa relacionada, o haciendo una cosa, pero sabiendo múltiples reglas de negocios que cambian la forma de sus productos.
Si eso es así, parece que el resultado final es un Modelo de Dominio Anémico, que sin duda es el caso en nuestro proyecto. Sin embargo, el modelo de dominio anémico es un antipatrón.
¿Pueden estas dos ideas coexistir?
EDITAR: Un par de enlaces relacionados con el contexto:
SRP - http://www.objectmentor.com/resources/articles/srp.pdf
Modelo de dominio anémico - http://martinfowler.com/bliki/AnemicDomainModel.html
No soy el tipo de desarrollador que simplemente le gusta encontrar un profeta y seguir lo que dicen como evangelio. Así que no proporciono enlaces a estos como una forma de decir "estas son las reglas", solo como una fuente de definición de los dos conceptos.
Antes de entrar en mi despotricar, esta es mi opinión en pocas palabras: en algún lugar todo debe unirse ... y luego un río lo atraviesa.
Estoy obsesionado por la codificación.
=======
El modelo de datos anémicos y yo ... bueno, nos buscamos mucho. Tal vez es solo la naturaleza de las aplicaciones de tamaño pequeño a mediano con muy poca lógica de negocios incorporada en ellas. Tal vez estoy un poco ''tarded''.
Sin embargo, aquí están mis 2 centavos:
¿No podría simplemente factorizar el código en las entidades y vincularlo a una interfaz?
public class Object1
{
public string Property1 { get; set; }
public string Property2 { get; set; }
private IAction1 action1;
public Object1(IAction1 action1)
{
this.action1 = action1;
}
public void DoAction1()
{
action1.Do(Property1);
}
}
public interface IAction1
{
void Do(string input1);
}
¿Esto de alguna manera viola los principios de SRP?
Además, ¿no es tener un montón de clases sentadas sin estar atadas entre sí por algo más que el código de consumo en realidad una violación más grande de SRP, pero empujó una capa?
Imagine al tipo que escribe el código del cliente sentado allí tratando de descubrir cómo hacer algo relacionado con Object1. Si tiene que trabajar con su modelo, trabajará con Object1, la bolsa de datos y un montón de ''servicios'', cada uno con una sola responsabilidad. Será su trabajo asegurarse de que todas esas cosas interactúen correctamente. Entonces, ahora su código se convierte en un script de transacción, y ese script en sí mismo contiene todas las responsabilidades necesarias para completar adecuadamente esa transacción en particular (o unidad de trabajo).
Además, podría decir: "no brah, todo lo que tiene que hacer es acceder a la capa de servicio. Es como Object1Service.DoActionX (Object1). Piece of cake". Bueno, entonces, ¿dónde está la lógica ahora? Todo en ese único método? Aún estás presionando el código, y pase lo que pase, terminarás con los datos y la lógica separada.
Entonces, en este escenario, ¿por qué no exponer al código del cliente ese Object1Service particular y hacer que DoActionX () básicamente sea otro gancho para su modelo de dominio? Con esto quiero decir:
public class Object1Service
{
private Object1Repository repository;
public Object1Service(Object1Repository repository)
{
this.repository = repository;
}
// Tie in your Unit of Work Aspect''ing stuff or whatever if need be
public void DoAction1(Object1DTO object1DTO)
{
Object1 object1 = repository.GetById(object1DTO.Id);
object1.DoAction1();
repository.Save(object1);
}
}
Aún tiene en cuenta el código real para Acción1 del Objeto1, pero para todos los propósitos intensivos, tenga un Objeto1 no anémico.
Digamos que necesita Acción1 para representar 2 (o más) operaciones diferentes que le gustaría hacer atómicas y separadas en sus propias clases. Simplemente cree una interfaz para cada operación atómica y conéctela dentro de DoAction1.
Así es como podría abordar esta situación. Pero, de nuevo, realmente no sé de qué se trata SRP.
Convierta sus objetos de dominio simple a patrón de ActiveRecord con una clase base común a todos los objetos de dominio. Ponga el comportamiento común en la clase base y anule el comportamiento en clases derivadas donde sea necesario o defina el nuevo comportamiento donde sea necesario.
Descubrí que seguir los principios sólidos realmente me alejaban del rico modelo de dominio de DDD, al final, descubrí que no me importaba. Más al punto, encontré que el concepto lógico de un modelo de dominio, y una clase en cualquier idioma no fueron mapeados 1: 1, a menos que estuviéramos hablando de una fachada de algún tipo.
No diría que esto es exactamente un estilo de programación c donde tienes estructuras y módulos, sino que probablemente terminarás con algo más funcional, me doy cuenta de que los estilos son similares, pero los detalles marcan una gran diferencia. Descubrí que las instancias de mi clase terminan comportándose como funciones de orden superior, aplicaciones de funciones parciales, funciones evaluadas de forma ocasional o alguna combinación de las anteriores. Es algo inefable para mí, pero esa es la sensación que me produce escribir código después de TDD + SOLID, terminó comportándose como un estilo OO / Funcional híbrido.
En cuanto a que la herencia sea una mala palabra, creo que eso se debe más al hecho de que la herencia no tiene suficiente grano fino en idiomas como Java / C #. En otros idiomas, es menos problemático y más útil.
El modelo de dominio enriquecido (RDM) y el principio de responsabilidad única (SRP) no están necesariamente en desacuerdo. RDM está más en desacuerdo con una subclase de SRP muy especializada, el modelo que defiende "beans de datos + toda la lógica empresarial en clases de controlador" (DBABLICC).
Si lees el http://www.objectmentor.com/resources/articles/srp.pdf Martin''s http://www.objectmentor.com/resources/articles/srp.pdf , verás que su ejemplo de módem está completamente en la capa de dominio, pero abstrayendo los conceptos de DataChannel y Connection como clases separadas. Mantiene el módem como un contenedor, ya que es una abstracción útil para el código del cliente. Se trata mucho más de factorización (re) adecuada que la mera estratificación . La cohesión y el acoplamiento siguen siendo los principios básicos del diseño.
Finalmente, tres problemas:
Como Martin mismo señala, no siempre es fácil ver las diferentes "razones para el cambio". Los mismos conceptos de YAGNI, Agile, etc. desalientan la anticipación de futuras razones para el cambio, por lo que no deberíamos inventar los que no son obvios inmediatamente. Veo ''razones prematuras y anticipadas para el cambio'' como un riesgo real al aplicar SRP y debe ser administrado por el desarrollador.
Además de lo anterior, incluso la aplicación correcta (pero innecesariamente analítica ) de SRP puede dar como resultado una complejidad no deseada. Siempre piense en el siguiente pobre ciudadano que tiene que mantener su clase: ¿la abstracción diligente del comportamiento trivial en sus propias interfaces, clases base e implementaciones de una línea realmente ayuda a su comprensión de lo que simplemente debería haber sido una sola clase?
El diseño de software a menudo trata de obtener el mejor compromiso entre fuerzas competidoras. Por ejemplo, una arquitectura en capas es principalmente una buena aplicación de SRP, pero ¿qué pasa con el hecho de que, por ejemplo, el cambio de una propiedad de una clase de negocios de, digamos, un booleano a una enumeración tiene un efecto dominó en todas las capas? - desde db a través del dominio, fachadas, servicio web, a GUI? ¿Esto apunta a un mal diseño? No necesariamente: apunta al hecho de que su diseño favorece un aspecto del cambio a otro.
La cita del documento de SRP es muy correcta; SRP es difícil de acertar. Este y OCP son los dos elementos de SOLID que simplemente deben relajar al menos hasta cierto punto para poder realizar un proyecto. La aplicación demasiado entusiasta de cualquiera producirá rápidamente un código de ravioles.
SRP puede tomarse hasta extremos ridículos, si las "razones para el cambio" son demasiado específicas. Incluso una "bolsa de datos" de POCO / POJO puede considerarse una violación de SRP, si se considera que el tipo de campo cambia como un "cambio". Pensarías que el sentido común te diría que el cambio de tipo de un campo es una concesión necesaria para el "cambio", pero he visto capas de dominio con envoltorios para tipos de valores incorporados; un infierno que hace que ADM parezca una utopía.
A menudo es bueno estar conectado a tierra con un objetivo realista, basado en la legibilidad o el nivel de cohesión deseado. Cuando dice: "Quiero que esta clase haga una cosa", no debería tener más ni menos de lo necesario para hacerlo. Puede mantener al menos la cohesión de procedimiento con esta filosofía básica. "Quiero que esta clase mantenga todos los datos de una factura" generalmente permitirá ALGUNA lógica comercial, incluso sumar subtotales o calcular el impuesto a las ventas, en función de la responsabilidad del objeto de saber cómo darle un valor preciso e internamente consistente para cualquier campo contiene.
Personalmente no tengo un gran problema con un dominio "liviano". El simple hecho de ser el "experto en datos" hace que el objeto de dominio sea el guardián de cada campo / propiedad pertinente a la clase, así como toda la lógica de campo calculada, cualquier conversión de tipos de datos explícitos / implícitos y posiblemente las reglas de validación más simples (es decir, campos obligatorios, límites de valores, cosas que romperían la instancia internamente si está permitido). Si es probable que cambie un algoritmo de cálculo, tal vez para un promedio ponderado o variable, encapsule el algoritmo y haga referencia a él en el campo calculado (eso es solo bueno OCP / PV).
No considero que ese objeto de dominio sea "anémico". Mi percepción de ese término es una "bolsa de datos", una colección de campos que no tiene ningún concepto del mundo exterior o incluso la relación entre sus campos, excepto que los contiene. También he visto eso, y no es divertido buscar incoherencias en el estado del objeto que el objeto nunca supo que era un problema. Un SRP demasiado entusiasta conducirá a esto al afirmar que un objeto de datos no es responsable de ninguna lógica comercial, pero el sentido común generalmente intervendría primero y diría que el objeto, como experto en datos, debe ser responsable de mantener un estado interno consistente.
De nuevo, opinión personal, prefiero el patrón Repositorio a Active Record. Un objeto, con una responsabilidad, y muy poco o nada más en el sistema de arriba de esa capa, tiene que saber algo sobre cómo funciona. Active Record requiere que la capa de dominio conozca al menos algunos detalles específicos sobre el método o marco de persistencia (ya sean los nombres de los procedimientos almacenados utilizados para leer / escribir cada clase, las referencias a objetos específicos del marco o los atributos que decoran los campos con información ORM ), y así inyecta una segunda razón para cambiar a cada clase de dominio por defecto.
Mi $ 0.02.
Me gusta la definición de SRP como:
"Una clase tiene solo un motivo comercial para cambiar"
Por lo tanto, siempre que los comportamientos se puedan agrupar en "razones comerciales" individuales, entonces no hay razón para que no coexistan en la misma clase. Por supuesto, lo que define una "razón comercial" está abierto al debate (y debe ser debatido por todos los interesados).
Tendría que decir "sí", pero debe hacer su SRP correctamente. Si la misma operación se aplica a una sola clase, pertenece a esa clase, ¿no crees? ¿Qué tal si la misma operación se aplica a múltiples clases? En ese caso, si quiere seguir el modelo OO de combinar datos y comportamiento, colocaría la operación en una clase base, ¿no?
Sospecho que por su descripción, terminan con clases que son básicamente bolsas de operaciones, por lo que esencialmente ha recreado el estilo C de codificación: estructuras y módulos.
Del documento de SRP vinculado: " El SRP es uno de los más simples del principio, y uno de los más difíciles de entender " .