¿Este diseño de clase viola el principio de responsabilidad única?

63

Hoy tuve una discusión con alguien.

Estaba explicando los beneficios de tener un modelo de dominio rico en lugar de un modelo de dominio anémico. Y demostré mi punto con una clase simple con ese aspecto:

public class Employee
{
    public Employee(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastname;
    }

    public string FirstName { get private set; }
    public string LastName { get; private set;}
    public int CountPaidDaysOffGranted { get; private set;}

    public void AddPaidDaysOffGranted(int numberOfdays)
    {
        // Do stuff
    }
}

Mientras defendía su enfoque de modelo anémico, uno de sus argumentos fue: "Soy un creyente en . Usted está violando el principio de responsabilidad única (SRP) ya que ambos representan datos y ejecutan la lógica en el misma clase. "

Encontré esta afirmación realmente sorprendente, ya que siguiendo este razonamiento, cualquier clase que tenga una propiedad y un método viola el SRP, y por lo tanto OOP en general no es SOLID y la programación funcional es el único camino al cielo.

Decidí no responder a sus muchos argumentos, pero siento curiosidad por lo que la comunidad piensa sobre esta pregunta.

Si hubiera respondido, hubiera comenzado señalando la paradoja mencionada anteriormente, y luego indicaría que el SRP depende en gran medida del nivel de granularidad que desea considerar y que si lo toma lo suficiente, cualquier clase que contenga más de una propiedad o un método lo viola.

¿Qué hubieras dicho?

Actualización: guntbert ha actualizado el ejemplo con generosidad para que el método sea más realista y nos ayude a enfocarnos en la discusión subyacente.

    
pregunta tobiak777 07.01.2016 - 22:35

11 respuestas

68

La responsabilidad única debe entenderse como una abstracción de tareas lógicas en su sistema. Una clase debe tener la responsabilidad única de (hacer todo lo necesario para) realizar una sola tarea específica. Esto realmente puede traer mucho a una clase bien diseñada, dependiendo de cuál es la responsabilidad. La clase que ejecuta su motor de secuencias de comandos, por ejemplo, puede tener muchos métodos y datos involucrados en el procesamiento de secuencias de comandos.

Tu compañero de trabajo se está enfocando en lo incorrecto. La pregunta no es "¿qué miembros tiene esta clase?" pero "¿qué operaciones útiles realiza esta clase dentro del programa?" Una vez que se entienda, tu modelo de dominio se ve bien.

    
respondido por el Mason Wheeler 07.01.2016 - 22:41
41

El Principio de Responsabilidad Única solo se refiere a si un fragmento de código (en OOP, normalmente estamos hablando de clases) tiene responsabilidad sobre una parte de funcionalidad . Creo que su amigo que dice que las funciones y los datos no pueden mezclarse no entendió realmente esa idea. Si Employee también contuviera información sobre su lugar de trabajo, qué tan rápido va su auto y qué tipo de comida come su perro, tendríamos un problema.

Dado que esta clase solo trata con un Employee , creo que es justo decir que no viola el SRP de manera descarada, pero la gente siempre tendrá sus propias opiniones.

Un lugar donde podríamos mejorar es separar la información del Empleado (como nombre, número de teléfono, correo electrónico) de sus vacaciones. En mi opinión, esto no significa que los métodos y los datos no puedan mezclarse , simplemente significa que quizás la funcionalidad de vacaciones podría estar en un lugar separado.

    
respondido por el Frank Bryce 07.01.2016 - 22:38
20

En mi opinión, esta clase podría potencialmente violar el SRP si continuara representando tanto un Employee como un EmployeeHolidays .

Tal como está, y si llegara a mí para la Revisión por Pares, probablemente lo dejaría pasar. Si se agregaran más propiedades y métodos específicos de los empleados, y se agregaran más propiedades específicas de vacaciones, probablemente recomendaría una división, citando tanto el SRP como el ISP.

    
respondido por el NikolaiDante 07.01.2016 - 23:06
20

Ya hay respuestas excelentes que señalan que SRP es un concepto abstracto sobre funcionalidad lógica, pero hay puntos más sutiles que creo que vale la pena agregar.

Las dos primeras letras de SOLID, SRP y OCP, son acerca de cómo cambia su código en respuesta a un cambio en los requisitos. Mi definición favorita del SRP es: "un módulo / clase / función debe tener solo una razón para cambiar". Discutir sobre las posibles razones para que su código cambie es mucho más productivo que discutir si su código es SÓLIDO o no.

¿Cuántas razones tiene que cambiar su clase de empleado? No lo sé, porque no sé el contexto en el que lo está utilizando y tampoco puedo ver el futuro. Lo que puedo hacer es hacer una lluvia de ideas de los posibles cambios en función de lo que he visto en el pasado, y usted puede evaluar subjetivamente qué tan probable es. Si hay más de una puntuación entre "razonablemente probable" y "mi código ya ha cambiado por esa razón exacta", entonces está violando el SRP contra ese tipo de cambio. Aquí hay uno: alguien con más de dos nombres se une a su compañía (o un arquitecto lee este excelente artículo del W3C ). Aquí hay otro: su empresa cambia la forma en que asigna los días festivos.

Tenga en cuenta que estas razones son igualmente válidas incluso si elimina el método AddHolidays. Un montón de modelos de dominio anémico violan el SRP. Muchos de ellos son solo tablas de base de datos en código, y es muy común que las tablas de base de datos tengan más de 20 razones para cambiar.

Aquí hay algo para masticar: ¿cambiaría su clase de empleado si su sistema fuera necesario para realizar un seguimiento de los salarios de los empleados? Direcciones? ¿Informacion de contacto de emergencia? Si dijiste "sí" (y "es probable que ocurra") a dos de esos, entonces tu clase estaría violando el SRP incluso si aún no tuviera un código. SOLID se trata de procesos y arquitectura tanto como de código.

    
respondido por el Carl Leth 08.01.2016 - 04:04
9

Que la clase representa datos no es responsabilidad de la clase, es un detalle de implementación privada.

La clase tiene una responsabilidad, representar a un empleado. En este contexto, eso significa que presenta una API pública que le proporciona la funcionalidad que necesita para tratar con los empleados (si AddHolidays es un buen ejemplo es discutible).

La implementación es interna; sucede que esto necesita algunas variables privadas y algo de lógica. Eso no significa que la clase ahora tenga múltiples responsabilidades.

    
respondido por el RemcoGerlich 08.01.2016 - 09:47
5

La idea de que mezclar lógica y datos de alguna manera siempre es incorrecta, es tan ridícula que ni siquiera merece discusión. Sin embargo, hay una clara violación de la responsabilidad única en el ejemplo, pero no es porque haya una propiedad DaysOfHolidays y una función AddHolidays(int) .

Se debe a que la identidad del empleado está entremezclada con la gestión de vacaciones, lo cual es malo. La identidad del empleado es una cosa compleja que se requiere para rastrear vacaciones, salario, horas extra, para representar quién está en qué equipo, para vincular a informes de desempeño, etc. Un empleado también puede cambiar el nombre, el apellido o ambos, y permanecer igual. empleado. Los empleados pueden incluso tener varias ortografías de su nombre, como un ASCII y una ortografía de Unicode. Las personas pueden tener de 0 a n nombres y / o apellidos. Pueden tener diferentes nombres en diferentes jurisdicciones. Rastrear la identidad de un empleado es una responsabilidad suficiente como para que la administración de días festivos o de vacaciones no se pueda agregar en la parte superior sin que sea una segunda responsabilidad.

    
respondido por el Peter 09.01.2016 - 15:14
3
  

"Soy un creyente en SOLID. Usted está violando el principio de responsabilidad única (SRP), ya que ambos representan datos y ejecutan la lógica en la misma clase".

Como los demás, no estoy de acuerdo con esto.

Diría que se viola el SRP si está ejecutando más de una lógica en la clase. La cantidad de datos que se deben almacenar dentro de la clase para lograr esa lógica única es irrelevante.

    
respondido por el Lightness Races in Orbit 08.01.2016 - 02:41
2

No me parece útil en estos días debatir sobre qué constituye y qué no constituye una responsabilidad única o una única razón para cambiar. Propondría un principio de pena mínima en su lugar:

  

Principio de pena mínima: el código debe buscar minimizar su probabilidad de requerir cambios o maximizar la facilidad de cambio.

¿Cómo es eso? No debería recurrir a un científico espacial para descubrir por qué esto puede ayudar a reducir los costos de mantenimiento y, con suerte, no debería ser un punto de debate interminable, pero como con SOLID en general, no es algo que se aplique a ciegas en todas partes. Es algo a tener en cuenta al equilibrar las compensaciones.

En cuanto a la probabilidad de requerir cambios, eso se reduce con:

  1. Buenas pruebas (confiabilidad mejorada).
  2. Involucrando solo el código mínimo necesario para hacer algo específico (esto puede incluir reducir los acoplamientos aferentes).
  3. Solo haciendo que el código sea duro en lo que hace (vea Hacer el principio de Badass).

En cuanto a la dificultad de realizar cambios, aumenta con los acoplamientos eferentes. Las pruebas introducen acoplamientos eferentes pero mejoran la confiabilidad. Hecho bien, generalmente hace más bien que daño y es totalmente aceptable y promovido por el Principio de Dolor Mínimo.

  

Haga el principio de Badass: las clases que se usan en muchos lugares deben ser increíbles. Deben ser confiables y eficientes si eso está relacionado con su calidad, etc.

Y el principio de Make Badass está ligado al principio de pena mínima, ya que las cosas badass encontrarán una menor probabilidad de requerir cambios que las cosas que apestan a lo que hacen.

  

Habría comenzado señalando la paradoja mencionada anteriormente, y   luego indique que el SRP es altamente dependiente del nivel de   granularidad que desea considerar y que si lo toma lo suficientemente lejos,   cualquier clase que contenga más de una propiedad o un método lo viola.

Desde un punto de vista de SRP, una clase que apenas hace algo sin duda tendría solo una (a veces cero) razones para cambiar:

class Float
{
public:
    explicit Float(float val);
    float get() const;
    void set(float new_val);
};

¡Eso prácticamente no tiene razones para cambiar! Es mejor que SRP. ¡Es ZRP!

Excepto que sugeriría que se trata de una violación flagrante del principio de Make Badass. También es absolutamente inútil. Algo que hace tan poco no puede esperar ser rudo. Tiene muy poca información (TLI). Y, naturalmente, cuando tienes algo que es TLI, no puede hacer nada realmente significativo, ni siquiera con la información que encapsula, por lo que tiene que filtrarlo al mundo exterior con la esperanza de que alguien más realmente haga algo significativo y rudo. Y esa filtración está bien para algo que solo está destinado a agregar datos y nada más, pero ese umbral es la diferencia que veo entre "datos" y "objetos".

Por supuesto, algo que es TMI también es malo. Podríamos poner todo nuestro software en una clase. Incluso puede tener solo un método run . Y alguien podría incluso argumentar que ahora tiene una razón muy amplia para cambiar: "Esta clase solo tendrá que cambiarse si el software necesita mejoras". Estoy siendo tonto, pero por supuesto podemos imaginar todos los problemas de mantenimiento con eso.

Así que hay un acto de equilibrio en cuanto a la granularidad o la tosquedad de los objetos que diseñas. A menudo lo juzgo por la cantidad de información que tiene que filtrar al mundo exterior y la funcionalidad significativa que puede realizar. A menudo encuentro que el Principio de Make Badass es útil para encontrar el equilibrio mientras lo combino con el Principio de Duelo Mínimo.

    
respondido por el user204677 08.01.2016 - 00:50
1

Por el contrario, para mí, el modelo de dominio anémico rompe algunos conceptos principales de OOP (que unen atributos y comportamiento), pero puede ser inevitable en función de las elecciones arquitectónicas. Los dominios anémicos son más fáciles de pensar, menos orgánicos y más secuenciales.

Muchos sistemas tienden a hacer esto cuando varias capas deben jugar con los mismos datos (capa de servicio, capa web, capa de cliente, agentes ...).

Es más fácil definir la estructura de datos en un lugar y el comportamiento en otras clases de servicio. Si se usó la misma clase en varias capas, ésta puede crecer en grande, y pregunta qué capa es responsable de definir el comportamiento que necesita y quién puede llamar a los métodos.

Por ejemplo, puede que no sea una buena idea que un proceso del Agente que calcula las estadísticas de todos sus empleados puede llamar a un cálculo para los días pagados. Y la GUI de la lista de empleados ciertamente no necesita en absoluto el nuevo cálculo de identificación agregado utilizado en este agente de estadísticas (y los datos técnicos que lo acompañan). Cuando segrega los métodos de esta manera, generalmente termina con una clase con solo estructuras de datos.

Usted puede serializar / deserializar fácilmente los datos del "objeto", o solo algunos de ellos, o en otro formato (json) ... sin preocuparse por ningún concepto / responsabilidad del objeto. Sin embargo, es sólo el paso de datos. Siempre puede realizar la asignación de datos entre dos clases (Employee, EmployeeVO, EmployeeStatistic ...) pero, ¿qué significa realmente Employee aquí?

Entonces sí, separa completamente los datos en las clases de dominio y el manejo de datos en las clases de servicio, pero aquí es necesario. Dicho sistema es, al mismo tiempo, funcional para aportar valor comercial y técnico para propagar los datos donde sea necesario, mientras se mantiene un ámbito de responsabilidad adecuado (y el objeto distribuido tampoco resuelve esto).

Si no necesita separar los ámbitos de comportamiento, es más libre de poner los métodos en clases de servicio o en clases de dominio, dependiendo de cómo vea su objeto. Tiendo a ver un objeto como el concepto "real", esto naturalmente ayuda a mantener el SRP. Por lo tanto, en su ejemplo, es más realista que el día de pago adicional del Empleado del Jefe otorgado a su cuenta PayDayAccount. Un empleado es contratado por la compañía, Works, puede estar enfermo o pedirle un consejo, y tiene una cuenta de día de pago (el jefe puede recuperarla directamente de él o de un registro PayDayAccount ...) Pero puede hacer un atajo agregado aquí para simplificar si no desea demasiada complejidad para un software simple.

    
respondido por el Vince 13.01.2016 - 23:35
0
  

Usted está violando el principio de responsabilidad única (SRP) a medida que   ambos representan datos y realizan lógica en la misma clase.

Suena muy razonable para mí. Modelo podría no tener propiedades públicas si expone acciones. Básicamente es una idea de separación de comando-consulta. Tenga en cuenta que Command tendrá un estado privado seguro.

    
respondido por el Dmitry Nogin 25.01.2016 - 13:55
0

No puedes violar el Principio de Responsabilidad Única porque es solo un criterio estético, no una regla de la Naturaleza. No se deje engañar por el nombre que suena científicamente y las letras mayúsculas.

    
respondido por el ZunTzu 25.01.2016 - 18:13

Lea otras preguntas en las etiquetas