SOLID Parte 1 - El principio de responsabilidad única

Responsabilidad única (SRP), Abrir / Cerrar, Sustitución de Liskov, Segregación de interfaz y Inversión de dependencia. Cinco principios ágiles que deberían guiarte cada vez que escribas un código..

La definición

Una clase debe tener una sola razón para cambiar.

Definido por Robert C. Martin en su libro Desarrollo ágil de software, principios, patrones y prácticas, y posteriormente publicado en la versión C # del libro Principios, patrones y prácticas ágiles en C #, es uno de los cinco principios ágiles SÓLIDOS. Lo que dice es muy simple, sin embargo, lograr esa simplicidad puede ser muy complicado. Una clase debe tener una sola razón para cambiar.

¿Pero por qué? ¿Por qué es tan importante tener una sola razón para el cambio??

En lenguajes estáticos y compilados de manera estática, varios motivos pueden llevar a varias redistribuciones no deseadas. Si hay dos razones diferentes para cambiar, es posible que dos equipos diferentes puedan trabajar en el mismo código por dos razones diferentes. Cada uno tendrá que implementar su solución, que en el caso de un lenguaje compilado (como C ++, C # o Java), puede llevar a módulos incompatibles con otros equipos u otras partes de la aplicación..

Aunque no utilice un lenguaje compilado, es posible que deba volver a probar la misma clase o módulo por diferentes motivos. Esto significa más trabajo de control de calidad, tiempo y esfuerzo..

La audiencia

Determinar la única responsabilidad que debe tener una clase o módulo es mucho más complejo que solo mirar una lista de verificación. Por ejemplo, una pista para encontrar nuestras razones para el cambio es analizar la audiencia de nuestra clase. Los usuarios de la aplicación o sistema que desarrollamos a quienes atiende un módulo en particular serán los que soliciten cambios en él. Los atendidos pedirán cambio. Aquí hay un par de módulos y sus posibles audiencias..

  • Módulo de Persistencia - La audiencia incluye DBAs y arquitectos de software.
  • Módulo de informes - La audiencia incluye empleados, contadores y operaciones.
  • Módulo de cálculo de pagos para un sistema de nómina - La audiencia puede incluir abogados, gerentes y contadores.
  • Módulo de búsqueda de libros para un sistema de gestión de bibliotecas - La audiencia puede incluir al bibliotecario y / oa los propios clientes.

Roles y actores

Asociar a personas concretas a todos estos roles puede ser difícil. En una pequeña empresa, una sola persona puede necesitar cumplir varios roles, mientras que en una gran compañía puede haber varias personas asignadas a un solo rol. Así que parece mucho más razonable pensar en los roles. Pero los roles por sí mismos son bastante difíciles de definir. ¿Qué es un rol? ¿Cómo lo encontramos? Es mucho más fácil imaginar a los actores haciendo esos roles y asociando a nuestra audiencia con esos actores..

Entonces, si nuestra audiencia define las razones del cambio, los actores definen a la audiencia. Esto nos ayuda enormemente a reducir el concepto de personas concretas como "Juan el arquitecto" a la Arquitectura, o "María el referente" a las Operaciones..

Entonces, una responsabilidad es una familia de funciones que sirve a un actor en particular. (Robert C. Martin)

Fuente de cambio

En el sentido de este razonamiento, los actores se convierten en una fuente de cambio para la familia de funciones que les sirve. A medida que cambian sus necesidades, esa familia específica de funciones también debe cambiar para adaptarse a sus necesidades.

Un actor para una responsabilidad es la única fuente de cambio para esa responsabilidad. (Robert C. Martin)

Ejemplos clásicos

Objetos que se pueden "imprimir" a sí mismos

Digamos que tenemos un Libro Clase que encapsula el concepto de un libro y sus funcionalidades..

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntero a la página siguiente function printCurrentPage () echo "contenido de la página actual"; 

Esto puede parecer una clase razonable. Tenemos libro, puede proporcionar su título, autor y puede pasar la página. Finalmente, también puede imprimir la página actual en la pantalla. Pero hay un pequeño problema. Si pensamos en los actores involucrados en la operación del Libro objeto, ¿quiénes podrían ser? Podemos pensar fácilmente en dos actores diferentes aquí: Administración de libros (como el bibliotecario) y Mecanismo de presentación de datos (como la forma en que queremos entregar el contenido al usuario: en la pantalla, interfaz de usuario gráfica, interfaz de usuario de solo texto, tal vez impresión) . Estos son dos actores muy diferentes..

Mezclar la lógica de negocios con la presentación es malo porque va en contra del Principio de Responsabilidad Única (SRP). Echa un vistazo al siguiente código:

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntero a la página siguiente function getCurrentPage () devolver "contenido de la página actual";  interface Printer function printPage ($ page);  la clase PlainTextPrinter implementa Printer function printPage ($ page) echo $ page;  la clase HtmlPrinter implementa Printer function printPage ($ page) echo '
'. $ página. '
';

Incluso este ejemplo muy básico muestra cómo separar la presentación de la lógica empresarial y respetar el SRP brinda grandes ventajas en la flexibilidad de nuestro diseño..

Objetos que pueden "salvarse" a sí mismos

Un ejemplo similar al anterior es cuando un objeto puede guardarse y recuperarse de la presentación..

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntero a la página siguiente function getCurrentPage () devolver "contenido de la página actual";  guardar función () $ nombre de archivo = '/ documentos /'. $ this-> getTitle (). '-'. $ this-> getAuthor (); file_put_contents ($ nombre de archivo, serializar ($ esto)); 

Podemos, nuevamente, identificar a varios actores como Sistema de Gestión de Libros y Persistencia. Cuando queremos cambiar la persistencia, necesitamos cambiar esta clase. Cuando deseamos cambiar la forma en que pasamos de una página a la siguiente, tenemos que modificar esta clase. Hay varios ejes de cambio aquí..

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntero a la página siguiente function getCurrentPage () devolver "contenido de la página actual";  class SimpleFilePersistence function save (Book $ book) $ filename = '/ documents /'. $ libro-> getTitle (). '-'. $ libro-> getAuthor (); file_put_contents ($ nombre de archivo, serializar ($ libro)); 

Mover la operación de persistencia a otra clase separará claramente las responsabilidades y tendremos la libertad de intercambiar métodos de persistencia sin afectar nuestra Libro clase. Por ejemplo implementando un Base de datos de persistencia La clase sería trivial y nuestra lógica de negocios basada en operaciones con libros no cambiará.

Una vista de nivel superior

En mis artículos anteriores mencionaba y presentaba con frecuencia el esquema arquitectónico de alto nivel que se puede ver a continuación..


Si analizamos este esquema, puede ver cómo se respeta el principio de responsabilidad única. La creación de objetos se separa a la derecha en Fábricas y el punto de entrada principal de nuestra aplicación, un actor y una responsabilidad. La persistencia también se cuida en la parte inferior. Un módulo separado para la responsabilidad separada. Finalmente, a la izquierda, tenemos la presentación o el mecanismo de entrega si lo desea, en forma de MVC o cualquier otro tipo de IU. SRP respetó de nuevo. Todo lo que queda es averiguar qué hacer dentro de nuestra lógica empresarial.

Consideraciones de diseño de software

Cuando pensamos en el software que necesitamos escribir, podemos analizar muchos aspectos diferentes. Por ejemplo, varios requisitos que afectan a la misma clase pueden representar un eje de cambio. Estos ejes de cambio pueden ser una pista para una sola responsabilidad. Existe una alta probabilidad de que los grupos de requisitos que afectan al mismo grupo de funciones tengan razones para cambiar o que se especifiquen en primer lugar.

El principal valor del software es la facilidad de cambio. La secundaria es la funcionalidad, en el sentido de satisfacer tantos requisitos como sea posible, satisfaciendo las necesidades del usuario. Sin embargo, para lograr un alto valor secundario, un valor primario es obligatorio. Para mantener nuestro valor primario alto, debemos tener un diseño que sea fácil de cambiar, extender, acomodar nuevas funcionalidades y garantizar que se respete el SRP.

Podemos razonar paso a paso:

  1. El valor primario alto lleva en el tiempo al valor secundario alto.
  2. Valor secundario significa necesidades de los usuarios..
  3. Necesidades de los usuarios significa necesidades de los actores..
  4. Las necesidades de los actores determinan las necesidades de los cambios de estos actores..
  5. Necesidades de cambio de actores define nuestras responsabilidades..

Entonces cuando diseñamos nuestro software debemos:

  1. Encuentra y define a los actores..
  2. Identificar las responsabilidades que sirven a esos actores..
  3. Agrupe nuestras funciones y clases para que cada uno tenga una sola responsabilidad asignada..

Un ejemplo menos obvio

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntero a la página siguiente function getCurrentPage () devolver "contenido de la página actual";  function getLocation () // devuelve la posición en la biblioteca // ie. número de estante y número de habitación

Ahora esto puede parecer perfectamente razonable. No tenemos ningún método para tratar la persistencia, o la presentación. Tenemos el nuestro turnPage () Funcionalidad y algunos métodos para proporcionar información diferente sobre el libro. Sin embargo, podemos tener un problema. Para averiguarlo, podríamos analizar nuestra aplicación. La función getLocation () puede ser el problema.

Todos los métodos de la Libro Las clases son sobre lógica empresarial. Así que nuestra perspectiva debe ser desde el punto de vista del negocio. Si nuestra solicitud está escrita para ser utilizada por bibliotecarios reales que están buscando libros y nos están entregando un libro físico, entonces SRP podría ser violado..

Podemos razonar que las operaciones del actor son las que están interesadas en los métodos. getTitle (), getAuthor () y getLocation (). Los clientes también pueden tener acceso a la aplicación para seleccionar un libro y leer las primeras páginas para tener una idea del libro y decidir si lo quieren o no. Así que los lectores actores pueden estar interesados ​​en todos los métodos excepto getLocations (). A un cliente común no le importa dónde se guarda el libro en la biblioteca. El bibliotecario entregará el libro al cliente. Por lo tanto, de hecho tenemos una violación de SRP.

class Book function getTitle () return "A Great Book";  function getAuthor () return "John Doe";  function turnPage () // puntero a la página siguiente function getCurrentPage () devolver "contenido de la página actual";  clase BookLocator función localizar (Book $ libro) // devuelve la posición en la biblioteca // es decir. número de estante y número de habitación $ libraryMap-> findBookBy ($ book-> getTitle (), $ book-> getAuthor ()); 

Presentando el BookLocator, El bibliotecario estará interesado en el BookLocator. El cliente estará interesado en el Libro solamente. Por supuesto, hay varias maneras de implementar un BookLocator. Puede utilizar el autor y el título o un objeto de libro y obtener la información requerida de la Libro. Siempre depende de nuestro negocio. Lo que es importante es que si se cambia la biblioteca, y el bibliotecario tendrá que encontrar libros en una biblioteca organizada de manera diferente, el Libro El objeto no se verá afectado. De la misma manera, si decidimos proporcionar un resumen precompilado a los lectores en lugar de dejarlos navegar por las páginas, eso no afectará al bibliotecario ni al proceso de encontrar el estante en el que se encuentran los libros..

Sin embargo, si nuestro objetivo es eliminar al bibliotecario y crear un mecanismo de autoservicio en nuestra biblioteca, entonces podemos considerar que SRP se respeta en nuestro primer ejemplo. Los lectores también son nuestros bibliotecarios, necesitan ir a buscar el libro y luego verificarlo en el sistema automatizado. Esto también es una posibilidad. Lo que es importante recordar aquí es que siempre debe considerar su negocio cuidadosamente.

Pensamientos finales

El principio de responsabilidad única siempre debe considerarse cuando escribimos un código. El diseño de clase y módulo se ve muy afectado por él y conduce a un diseño acoplado bajo con menos dependencias y más ligeras. Pero como toda moneda, tiene dos caras. Es tentador diseñar desde el principio de nuestra aplicación con SRP en mente. También es tentador identificar tantos actores como queramos o necesitamos. Pero esto es realmente peligroso, desde el punto de vista del diseño, para tratar de pensar en todas las partes desde el principio. La consideración excesiva de SRP puede conducir fácilmente a una optimización prematura y, en lugar de un mejor diseño, puede llevar a uno disperso donde las responsabilidades claras de las clases o los módulos pueden ser difíciles de entender.

Por lo tanto, siempre que observe que una clase o módulo comienza a cambiar por diferentes motivos, no dude, tome las medidas necesarias para respetar el SRP, sin embargo, no lo deje porque la optimización prematura puede engañarlo fácilmente..