¿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 que se ve así:

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 SOLID . Usted está violando el principio de responsabilidad única (SRP) ya que ambos representan datos y realizan la lógica en la 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, la POO en general no es SÓLIDA, y la programación funcional es el único camino al cielo.

Decidí no responder a sus muchos argumentos, pero tengo curiosidad por saber qué piensa la comunidad sobre esta pregunta.

Si hubiera respondido, habría comenzado señalando la paradoja mencionada anteriormente, y luego habría indicado que el SRP depende en gran medida del nivel de granularidad que desee tener en cuenta y que si lo lleva lo suficientemente lejos, cualquier clase que contenga más de uno propiedad o un método lo viola.

¿Que hubieras dicho tu?

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

tobiak777
fuente
19
esta clase viola SRP no porque mezcle datos con lógica, sino porque tiene baja cohesión - potencial objeto de Dios
mosquito
1
¿Para qué sirve agregar vacaciones al empleado? Tal vez debería haber una clase de calendario o algo que tenga las vacaciones. Creo que tu amigo tiene razón.
James Black
99
Nunca escuches a alguien que dice "Soy un creyente en X".
Stig Hemmer
29
No es tanto si esto es una violación del SRP como si es un buen modelo en absoluto. Supongamos que soy un empleado. Cuando le pregunto a mi gerente si está de acuerdo con él si me tomo un fin de semana largo para ir a esquiar, mi gerente no me agrega vacaciones . El código aquí no coincide con la realidad que pretende modelar y, por lo tanto, es sospechoso.
Eric Lippert
2
+1 para la programación funcional es el único camino al cielo.
erip

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 tarea única y específica. Esto realmente puede aportar mucho a una clase bien diseñada, dependiendo de cuál sea la responsabilidad. La clase que ejecuta su motor de script, por ejemplo, puede tener muchos métodos y datos involucrados en el procesamiento de scripts.

Su compañero de trabajo se está centrando 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 entiende, su modelo de dominio se ve muy bien.

Mason Wheeler
fuente
Si se creó la Programación Orientada a Objetos y se pretende modelar los objetos y clases de la vida real (acciones + atributos), ¿por qué no escribir la clase de código que tiene múltiples responsabilidades (acciones)? Un objeto del mundo real puede tener múltiples responsabilidades. Por ejemplo, un periodista escribe editoriales en periódicos y entrevista a políticos en un programa de televisión. ¡Dos responsabilidades para un objeto de la vida real! ¿Qué pasa si voy a escribir un periodista de clase?
user1451111
41

El Principio de responsabilidad única solo se refiere a si un código (o no, en OOP, generalmente estamos hablando de clases) tiene responsabilidad sobre un componente funcional . Creo que tu amigo que dice que las funciones y los datos no pueden mezclarse realmente no entendió esa idea. Si Employeetambién contuviera información sobre su lugar de trabajo, qué tan rápido va su automóvil y qué tipo de comida come su perro, entonces tendríamos un problema.

Como esta clase solo trata con un Employee, creo que es justo decir que no viola SRP descaradamente, pero las personas siempre van a tener 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 , solo significa que quizás la funcionalidad de las vacaciones podría estar en un lugar separado.

Frank Bryce
fuente
20

En mi opinión, esta clase podría violar potencialmente SRP si continuara representando tanto an Employeecomo EmployeeHolidays.

Tal como están las cosas, y si se me ocurriera para Peer Review, probablemente lo dejaría pasar. Si se añadieran más propiedades y métodos específicos para empleados, y se agregaran más propiedades específicas para vacaciones, probablemente recomendaría una división, citando tanto SRP como ISP.

NikolaiDante
fuente
Estoy de acuerdo. Si el código es tan simple como se proporciona aquí, probablemente lo dejaría pasar. Pero en mi opinión, no debería ser responsabilidad del Empleado manejar sus propias vacaciones. Puede que no parezca un gran problema dónde se coloca la responsabilidad, pero mírelo de esta manera: si fuera nuevo en la base del código y tuviera que trabajar en la funcionalidad específica de las vacaciones, ¿dónde buscaría primero? Para la lógica de vacaciones, yo personalmente NO miraría a la entidad Empleado para empezar.
Niklas H
1
@NiklasH De acuerdo. Aunque personalmente no miraría al azar y trataría de adivinar en la clase que buscaría en el estudio la palabra "Vacaciones" y vería en qué clases surgió. :)
NikolaiDante
44
Cierto. Pero, ¿y si no se llama "vacaciones" en este nuevo sistema, sino "vacaciones" o "tiempo libre"? Pero estoy de acuerdo, con eso normalmente tiene la capacidad de buscarlo, o puede preguntarle a un compañero de trabajo. Mi comentario fue principalmente para que el OP modelara mentalmente la responsabilidad y donde el lugar más obvio para la lógica sería :-)
Niklas H
Estoy de acuerdo con tu primera declaración. Sin embargo, si se trata de una revisión por pares, no creo que lo haría porque violar SRP es una pendiente resbaladiza y esta podría ser la primera de muchas ventanas rotas. Salud.
Jim Speaker
20

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

Las dos primeras letras en SOLID, SRP y OCP tratan sobre 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 una sola razón para cambiar". Discutir sobre las posibles razones para que cambie su código es mucho más productivo que discutir sobre 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ás usando, y tampoco puedo ver el futuro. Lo que puedo hacer es hacer una lluvia de ideas sobre posibles cambios basados ​​en lo que he visto en el pasado, y usted puede evaluar subjetivamente qué tan probable es. Si hay más de un puntaje entre "razonablemente probable" y "mi código ya ha cambiado por ese motivo exacto", está violando SRP contra ese tipo de cambio. Aquí hay uno: alguien con más de dos nombres se une a su empresa (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 estos motivos son igualmente válidos incluso si elimina el método AddHolidays. Muchos modelos de dominio anémico violan el SRP. Muchos de ellos son solo tablas de bases de datos en código, y es muy común que las tablas de bases de datos tengan más de 20 razones para cambiar.

Aquí hay algo para analizar: ¿cambiaría su clase de Empleado si su sistema necesitara rastrear los salarios de los empleados? Direcciones? ¿Informacion de contacto de emergencia? Si dijiste "sí" (y "es probable que suceda") a dos de ellos, entonces tu clase estaría violando SRP incluso si todavía no tuviera código. SOLID se trata tanto de procesos y arquitectura como de código.

Carl Leth
fuente
9

Que la clase represente datos no es responsabilidad de la clase, es un detalle de implementación privado.

La clase tiene una responsabilidad, representar a un empleado. En este contexto, eso significa que presenta una API pública que le brinda 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.

RemcoGerlich
fuente
Interesante línea de pensamiento, muchas gracias por compartir
tobiak777
Niza: buena forma de expresar los objetivos previstos de OOP.
user949300
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 individual en el ejemplo, pero no es porque haya una propiedad DaysOfHolidaysy una función AddHolidays(int).

Se debe a que la identidad del empleado está entremezclada con la gestión de las vacaciones, lo cual es malo. La identidad del empleado es algo complejo que se requiere para rastrear las vacaciones, el salario, las horas extra, para representar quién está en qué equipo, para vincular a los informes de rendimiento, etc. Un empleado también puede cambiar el nombre, el apellido o ambos, y permanecer igual empleado. Los empleados pueden incluso tener múltiples deletreos de su nombre, como un ASCII y un deletreo unicode. Las personas pueden tener 0 a n nombres y / o apellidos. Pueden tener diferentes nombres en diferentes jurisdicciones. El seguimiento de la identidad de un empleado es una responsabilidad suficiente para que la administración de vacaciones o vacaciones no pueda agregarse sin llamarla una segunda responsabilidad.

Peter
fuente
"Hacer un seguimiento de la identidad de un empleado es una responsabilidad suficiente para que la administración de vacaciones o vacaciones no pueda agregarse sin llamarla una segunda responsabilidad". + El empleado puede tener varios nombres, etc. El objetivo de un modelo es centrarse en los hechos relevantes del mundo real para el problema en cuestión. Existen requisitos para los cuales este modelo es óptimo. En estos requisitos, los empleados solo son interesantes en la medida en que podamos modificar sus vacaciones y no nos interese demasiado administrar otros aspectos de sus aspectos específicos de la vida real.
tobiak777
@reddy "Los empleados solo son interesantes en la medida en que podamos modificar sus vacaciones", lo que significa que debe identificarlos correctamente. Tan pronto como tenga un empleado, puede cambiar su apellido en cualquier momento debido al matrimonio o el divorcio. También pueden cambiar su nombre y género. ¿Despedirá a los empleados si su apellido cambia de modo que su nombre coincida con el de otro empleado? No agregará toda esta funcionalidad en este momento. En cambio, lo agregará cuando lo necesite, lo cual es bueno. Independientemente de cuánto se implemente, la responsabilidad de la identificación sigue siendo la misma.
Peter
3

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

Al igual que los demás, no estoy de acuerdo con esto.

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

La ligereza corre con Mónica
fuente
¡No! SRP no es violado por múltiples piezas de lógica, múltiples piezas de datos o cualquier combinación de ambas. El único requisito es que el objeto debe cumplir con su propósito. Su propósito puede implicar muchas operaciones.
Martin Maat
@ MartininMaat: Muchas operaciones, sí. Una pieza de lógica como resultado. Creo que estamos diciendo lo mismo pero con diferentes términos (y estoy feliz de asumir que los suyos son los correctos ya que no estudio esto a menudo)
Lightness compite con Monica el
2

No me parece útil en estos días debatir sobre qué constituye y qué no constituye una sola responsabilidad o una sola razón para cambiar. Yo 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 (fiabilidad mejorada).
  2. Involucrando solo el código mínimo requerido para hacer algo específico (esto puede incluir la reducción de acoplamientos aferentes).
  3. Simplemente haciendo que el código sea rudo en lo que hace (ver Principio Make Badass).

En cuanto a la dificultad de realizar cambios, aumenta con acoplamientos eferentes. Las pruebas introducen acoplamientos eferentes pero mejoran la fiabilidad. Bien hecho, generalmente hace más bien que mal y es totalmente aceptable y promovido por el Principio Mínimo de Pena.

Haga el principio de Badass: las clases que se usan en muchos lugares deberían ser increíbles. Deben ser confiables, eficientes si eso se vincula con su calidad, etc.

Y el Principio Make Badass está vinculado al Principio Mínimo de Pena, ya que las cosas rudas 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 indicaría que el SRP es altamente dependiente del nivel de granularidad que desea considerar y que si lo lleva lo suficientemente lejos, cualquier clase que contenga más de una propiedad o un método viola eso.

Desde el punto de vista de SRP, una clase que apenas hace algo ciertamente 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 es una violación flagrante del Principio 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 realmente haga algo significativo y rudo. Y esa fuga está bien para algo que solo pretende 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 un solo runmétodo. 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.

Por lo tanto, existe un acto de equilibrio en cuanto a la granularidad o aspereza de los objetos que diseña. A menudo lo juzgo por la cantidad de información que tiene que filtrar al mundo exterior y cuánta funcionalidad significativa puede realizar. A menudo encuentro útil el Principio Make Badass para encontrar el equilibrio mientras lo combino con el Principio Mínimo de Pena.


fuente
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, esta puede crecer 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 de Agente que computa estadísticas de todos sus empleados pueda llamar a un cómputo por días pagados. Y la GUI de la lista de empleados ciertamente no necesita en absoluto el nuevo cálculo de ID agregado utilizado en este agente estadístico (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.

Puede serializar / deserializar con demasiada facilidad 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, son solo datos que pasan. Siempre puede hacer un mapeo 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í se necesita. Dicho sistema es al mismo tiempo funcional para aportar valor comercial y también técnico para propagar los datos donde sea necesario, manteniendo un alcance de responsabilidad adecuado (y el objeto distribuido tampoco resuelve esto).

Si no necesita separar los ámbitos de comportamiento, tiene más libertad para colocar 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 SRP. Entonces, en su ejemplo, es más realista que el Día de pago adicional del Empleado del jefe otorgado a su PayDayAccount. Un empleado es contratado por la empresa, Works, puede estar enfermo o se le puede pedir un consejo, y él tiene una cuenta de Payday (el Jefe puede recuperarlo directamente de él o de un registro de PayDayAccount ...) Pero puede hacer un acceso directo agregado aquí por simplicidad si no quieres demasiada complejidad para un software simple.

Vince
fuente
Gracias por tu aportación Vince. El punto es que no necesita una capa de servicio cuando tiene un dominio rico. Solo hay una clase responsable del comportamiento, y es su entidad de dominio. Las otras capas (capa web, interfaz de usuario, etc.) suelen tratar con DTO y ViewModels, y esto está bien. El modelo de dominio es modelar el dominio, no hacer trabajos de IU o enviar mensajes a través de Internet. Su mensaje refleja esta idea errónea común, que proviene del hecho de que las personas simplemente no saben cómo encajar la POO en su diseño. Y creo que esto es muy triste, para ellos.
tobiak777
No creo que tenga una idea errónea de OOP de dominio rico, he diseñado una gran cantidad de software de esa manera, y es cierto que es realmente bueno para el mantenimiento / evolución. Pero lamento decirte que esto no es una bala de plata.
Vince
No digo que lo sea. Para escribir un compilador probablemente no lo sea, pero para la mayoría de las aplicaciones de línea de negocio / SaaS, creo que es mucho menos arte / ciencia de lo que sugiere. La necesidad de un modelo de dominio puede demostrarse matemáticamente, sus ejemplos me hacen pensar en diseños discutibles en lugar de defectos de limitaciones en OOP
tobiak777
0

Está violando el principio de responsabilidad única (SRP), ya que representa los datos y realiza la lógica en la misma clase.

A mí me parece muy razonable. El modelo podría no tener propiedades públicas si expone acciones. Básicamente es una idea de Separación de consulta de comando. Tenga en cuenta que Command tendrá un estado privado seguro.

Dmitry Nogin
fuente
0

No puede 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.

ZunTzu
fuente
1
Esta no es una respuesta real, pero hubiera sido genial como comentario a la pregunta.
Jay Elston