Responsabilidad única (SRP), Open / Closed (OCP), Sustitución de Liskov, Segregación de interfaz y Inversión de dependencia. Cinco principios ágiles que deben guiarlo cada vez que necesite escribir código.
Las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para extensión, pero cerradas para modificación.
El Principio Abierto / Cerrado, OCP en breve, se acredita a Bertrand Mayer, un programador francés, que lo publicó por primera vez en su libro n Orientated Object Software Construction en 1988..
El principio creció en popularidad a principios de la década de 2000 cuando se convirtió en uno de los principios SOLID definidos por Robert C. Martin en su libro Agile Software Development, Principles, Patterns, and Practices, y más tarde publicado en la versión C # del libro Agile Principles, Patterns. , y practicas en C #.
Básicamente, estamos hablando de diseñar nuestros módulos, clases y funciones de manera que cuando se necesite una nueva funcionalidad, no debemos modificar nuestro código existente, sino escribir un código nuevo que será utilizado por el código existente. Esto suena un poco extraño, especialmente si estamos trabajando en lenguajes como Java, C, C ++ o C # donde se aplica no solo al código fuente en sí, sino también al binario. Queremos crear nuevas funciones de manera que no sea necesario que volvamos a implementar los binarios, ejecutables o DLL existentes.
A medida que avanzamos con estos tutoriales, podemos poner cada nuevo principio en el contexto de los ya discutidos. Ya hablamos sobre la responsabilidad única (SRP) que establecía que un módulo debería tener solo una razón para cambiar. Si pensamos en OCP y SRP, podemos observar que son complementarios. El código diseñado específicamente con SRP en mente estará cerca de los principios de OCP o será fácil para que respete esos principios. Cuando tenemos un código que tiene una sola razón para cambiar, la introducción de una nueva función creará una razón secundaria para ese cambio. Entonces, tanto SRP como OCP serían violados. De la misma manera, si tenemos un código que solo debe cambiar cuando cambia su función principal y debe permanecer sin cambios cuando se le agrega una nueva característica, respetando así el OCP, en su mayoría también respetará el SRP.
Esto no significa que SRP siempre lleve a OCP o viceversa, pero en la mayoría de los casos, si uno de ellos es respetado, lograr el segundo es bastante simple.
Desde un punto de vista puramente técnico, el principio abierto / cerrado es muy simple. Una relación simple entre dos clases, como la de abajo, viola el OCP..
los Usuario
clase usa el Lógica
clase directamente Si necesitamos implementar un segundo Lógica
clase de una manera que nos permitirá utilizar tanto la actual como la nueva, la existente Lógica
la clase tendrá que ser cambiada. Usuario
está directamente ligado a la implementación de Lógica
, No hay manera de que proporcionemos una nueva Lógica
Sin afectar al actual. Y cuando hablamos de idiomas tipificados estáticamente, es muy posible que el Usuario
La clase también requerirá cambios. Si estamos hablando de lenguajes compilados, sin duda tanto el Usuario
ejecutable y el Lógica
La biblioteca ejecutable o dinámica requerirá la recompilación y la redistribución para nuestros clientes, un proceso que queremos evitar siempre que sea posible.
Basado solo en el esquema anterior, uno puede deducir que cualquier clase que use directamente otra clase violaría el Principio Abierto / Cerrado. Y eso es correcto, estrictamente hablando. Me pareció bastante interesante encontrar los límites, el momento en que dibuja la línea y decide que es más difícil respetar el OCP que modificar el código existente, o que el costo arquitectónico no justifica el costo de cambiar el código existente.
Digamos que queremos escribir una clase que pueda proporcionar progreso como porcentaje para un archivo que se descarga a través de nuestra aplicación. Tendremos dos clases principales, una Progreso
y un Expediente
, y me imagino que querremos usarlos como en la prueba a continuación.
function testItCanGetTheProgressOfAFileAsAPercent () $ file = new File (); $ archivo-> longitud = 200; $ archivo-> enviado = 100; $ progress = new Progress ($ file); $ this-> assertEquals (50, $ progress-> getAsPercent ());
En esta prueba somos usuarios de Progreso
. Queremos obtener un valor como porcentaje, independientemente del tamaño real del archivo. Usamos Expediente
Como fuente de información para nuestra Progreso
. Un archivo tiene una longitud en bytes y un campo llamado expedido
que representa la cantidad de datos enviados a la persona que realiza la descarga. No nos importa cómo se actualizan estos valores en la aplicación. Podemos asumir que hay una lógica mágica que lo está haciendo por nosotros, por lo que en una prueba podemos establecerlos explícitamente.
archivo de clase public $ length; $ público enviado;
los Expediente
clase es solo un objeto de datos simple que contiene los dos campos. Por supuesto, en la vida real, probablemente también contenga otra información y comportamiento, como el nombre del archivo, la ruta, la ruta relativa, el directorio actual, el tipo, los permisos, etc..
clase Progreso archivo privado $; función __construct (File $ file) $ this-> file = $ file; función getAsPercent () return $ this-> file-> sent * 100 / $ this-> file-> length;
Progreso
es simplemente una clase tomando una Expediente
En su constructor. Para mayor claridad, especificamos el tipo de la variable en los parámetros del constructor. Hay un solo método útil en Progreso
, getAsPercent ()
, que tomará los valores enviados y la longitud de Expediente
y transformarlos en un porcentaje. Sencillo, y funciona..
Las pruebas comenzaron a las 5:39 PM ... PHPUnit 3.7.28 por Sebastian Bergmann ... Tiempo: 15 ms, Memoria: 2.50Mb OK (1 prueba, 1 aserción)
Este código parece correcto, sin embargo, viola el Principio Abierto / Cerrado. ¿Pero por qué? Y cómo?
Cada aplicación que se espera evolucione en el tiempo necesitará nuevas características. Una nueva característica para nuestra aplicación podría ser permitir la transmisión de música, en lugar de solo descargar archivos. Expediente
La longitud de 'se representa en bytes, la duración de la música en segundos. Queremos ofrecer una buena barra de progreso a nuestros oyentes, pero ¿podemos reutilizar la que ya tenemos??
No podemos. Nuestro progreso está obligado a Expediente
. Comprende solo los archivos, aunque también se puede aplicar al contenido de música. Pero para hacer eso tenemos que modificarlo, tenemos que hacer Progreso
saber sobre Música
y Expediente
. Si nuestro diseño respetara OCP, no tendríamos que tocar Expediente
o Progreso
. Podríamos simplemente reutilizar el existente Progreso
y aplicarlo a Música
.
Los idiomas tipificados dinámicamente tienen las ventajas de adivinar los tipos de objetos en tiempo de ejecución. Esto nos permite eliminar el tipo de sugerencia Progreso
'constructor y el código seguirá funcionando.
clase Progreso archivo privado $; función __construct ($ file) $ this-> file = $ file; función getAsPercent () return $ this-> file-> sent * 100 / $ this-> file-> length;
Ahora podemos lanzar cualquier cosa a Progreso
. Y por cualquier cosa, quiero decir literalmente cualquier cosa:
clase de musica public $ length; $ público enviado; artista $ público public $ album; public $ releaseDate; function getAlbumCoverFile () return 'Images / Covers /'. $ este-> artista. '/'. $ este-> album. '.png';
Y un Música
clase como la de arriba funcionará bien. Podemos probarlo fácilmente con una prueba muy similar a Expediente
.
function testItCanGetTheProgressOfAMusicStreamAsAPercent () $ music = new Music (); $ música-> longitud = 200; $ música-> enviado = 100; $ progress = new Progress ($ music); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Así que básicamente, cualquier contenido medible puede ser usado con el Progreso
clase. Quizás deberíamos expresar esto en código cambiando también el nombre de la variable:
clase Progreso privado $ mensableContent; function __construct ($ measuringableContent) $ this-> measuringableContent = $ measuringableContent; función getAsPercent () return $ this-> mensurableContenido-> enviado * 100 / $ esto-> mensuradaContenido-> longitud;
Bien, pero tenemos un gran problema con este enfoque. Cuando teniamos Expediente
Especificados como una sugerencia de tipo, fuimos positivos acerca de lo que nuestra clase puede manejar. Era explícito y si algo más entraba, un buen error nos lo dijo.
El argumento 1 pasado a Progress :: __ construct () debe ser una instancia de File, una instancia de Music dado.
Pero sin la sugerencia de tipo, debemos confiar en el hecho de que lo que ocurra tendrá dos variables públicas de algunos nombres exactos como "longitud
"y"expedido
". De lo contrario tendremos un legado rechazado..
Legado rechazado: una clase que anula un método de una clase base de tal manera que la clase derivada no respeta el contrato de la clase base. ~ Fuente Wikipedia.
éste es uno de el código huele presentado con mucho más detalle en el curso premium Detecting Code Smells. En resumen, no queremos terminar tratando de llamar a métodos o acceder a campos en objetos que no cumplen con nuestro contrato. Cuando tuvimos una sugerencia de tipo, el contrato fue especificado por él. Los campos y métodos de la Expediente
clase. Ahora que no tenemos nada, podemos enviar cualquier cosa, incluso una cadena y daría como resultado un error feo.
function testItFailsWithAParameterThatDoesNotRespectTheImplicitContract () $ progress = new Progress ('some string'); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Una prueba como esta, donde enviamos una cadena simple, producirá un legado rechazado:
Tratar de obtener la propiedad de no-objeto.
Si bien el resultado final es el mismo en ambos casos, lo que significa que el código se rompe, el primero produjo un mensaje agradable. Este, sin embargo, es muy oscuro. No hay forma de saber cuál es la variable, una cadena en nuestro caso, y qué propiedades se buscaron y no se encontraron. Es difícil depurar y resolver el problema. Un programador necesita abrir el Progreso
Clase y léelo y entiéndelo. El contrato, en este caso, cuando no especificamos explícitamente el tipo de sugerencia, se define por el comportamiento de Progreso
. Es un contrato implícito, conocido solo por Progreso
. En nuestro ejemplo, está definido por el acceso a los dos campos., expedido
y longitud
, en el getAsPercent ()
método. En la vida real, el contrato implícito puede ser muy complejo y difícil de descubrir con solo buscar unos segundos en la clase..
Esta solución se recomienda solo si ninguna de las otras sugerencias a continuación se puede implementar fácilmente o si infligirían cambios arquitectónicos serios que no justifiquen el esfuerzo..
Esta es la solución más común y, probablemente, la más adecuada para respetar OCP. Es simple y efectivo.
El Patrón de Estrategia simplemente introduce el uso de una interfaz. Una interfaz es un tipo especial de entidad en Programación Orientada a Objetos (OOP) que define un contrato entre un cliente y una clase de servidor. Ambas clases se adherirán al contrato para garantizar el comportamiento esperado. Puede haber varias clases de servidor no relacionadas que respeten el mismo contrato, por lo que pueden servir a la misma clase de cliente.
interface Measurable function getLength (); función getSent ();
En una interfaz solo podemos definir el comportamiento. Por eso, en lugar de usar directamente variables públicas, tendremos que pensar en usar adeptos y definidores. Adaptar las otras clases no será difícil en este punto. Nuestro IDE puede hacer la mayor parte del trabajo..
function testItCanGetTheProgressOfAFileAsAPercent () $ file = new File (); $ archivo-> setLength (200); $ archivo-> setSent (100); $ progress = new Progress ($ file); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Como de costumbre, comenzamos con nuestras pruebas. Necesitaremos usar setters para establecer los valores. Si se considera obligatorio, estos colocadores también pueden definirse en el Mensurable
interfaz. Sin embargo, ten cuidado con lo que pones allí. La interfaz es definir el contrato entre la clase cliente. Progreso
y las diferentes clases de servidor como Expediente
y Música
. Hace Progreso
¿Necesitas establecer los valores? Probablemente no. Por lo tanto, es muy poco probable que los definidores sean necesarios para ser definidos en la interfaz. Además, si definiera los configuradores allí, forzaría a todas las clases del servidor a implementarlos. Para algunos de ellos, puede ser lógico tener establecedores, pero otros pueden comportarse de manera totalmente diferente. ¿Qué pasa si queremos utilizar nuestra Progreso
¿Clase para mostrar la temperatura de nuestro horno? los Temperatura del horno
La clase puede inicializarse con los valores en el constructor, u obtener la información de una tercera clase. ¿Quién sabe? Tener setters en esa clase sería extraño.
class File implementa Measurable private $ length; $ privado enviado; public $ filename; propietario $ público; función setLength ($ length) $ this-> length = $ length; function getLength () return $ this-> length; función setSent ($ enviado) $ esto-> enviado = $ enviado; función getSent () return $ this-> send; function getRelativePath () return dirname ($ this-> filename); función getFullPath () return realpath ($ this-> getRelativePath ());
los Expediente
La clase se modifica ligeramente para adaptarse a los requisitos anteriores. Ahora implementa el Mensurable
Interfaz y tiene configuradores y captadores para los campos que nos interesan.. Música
Es muy similar, puedes consultar su contenido en el código fuente adjunto. Casi terminamos.
clase Progreso privado $ mensableContent; function __construct (Measurable $ measuringableContent) $ this-> measuringableContent = $ measuringableContent; function getAsPercent () return $ this-> measuringableContent-> getSent () * 100 / $ this-> measuringableContent-> getLength ();
Progreso
También necesitaba una pequeña actualización. Ahora podemos especificar un tipo, usando tipografía, en el constructor. El tipo esperado es Mensurable
. Ahora tenemos un contrato explícito.. Progreso
puede estar seguro de que los métodos a los que se accede siempre estarán presentes porque están definidos en el Mensurable
interfaz. Expediente
y Música
También puede estar seguro de que pueden proporcionar todo lo que se necesita para Progreso
simplemente implementando todos los métodos en la interfaz, un requisito cuando una clase implementa una interfaz.
Este patrón de diseño se explica con mayor detalle en el curso de Patrones de diseño ágil..
La gente tiende a nombrar interfaces con mayúscula. yo
delante de ellos, o con la palabra "Interfaz
"adjunto al final, como IFIL
o FileInterface
. Esta es una notación de estilo antiguo impuesta por algunos estándares obsoletos. Hemos superado las notaciones húngaras o la necesidad de especificar el tipo de una variable u objeto en su nombre para poder identificarlo más fácilmente. Los IDE identifican cualquier cosa en una fracción de segundo para nosotros. Esto nos permite concentrarnos en lo que realmente queremos abstraer..
Las interfaces pertenecen a sus clientes. Sí. Cuando quiera nombrar una interfaz, debe pensar en el cliente y olvidarse de la implementación. Cuando nombramos nuestra interfaz Medible, lo hicimos pensando en Progreso. Si yo fuera un progreso, ¿qué necesitaría para poder proporcionar el porcentaje? La respuesta es simple, algo que podemos medir. De ahí el nombre medible..
Otra razón es que la implementación puede ser de varios dominios. En nuestro caso, hay archivos y música. Pero podemos muy bien reutilizar nuestra Progreso
en un simulador de carreras. En ese caso, las clases medidas serían Velocidad, Combustible, etc. Agradable, ¿no es así??
El patrón de diseño del Método de plantilla es muy similar a la estrategia, pero en lugar de una interfaz utiliza una clase abstracta. Se recomienda utilizar un patrón de Método de plantilla cuando tenemos un cliente muy específico para nuestra aplicación, con una reutilización reducida y cuando las clases del servidor tienen un comportamiento común.
Este patrón de diseño se explica con mayor detalle en el curso de Patrones de diseño ágil..
Entonces, ¿cómo está afectando todo esto a nuestra arquitectura de alto nivel??
Si la imagen de arriba representa la arquitectura actual de nuestra aplicación, agregar un nuevo módulo con cinco clases nuevas (las azules) debería afectar nuestro diseño de manera moderada (clase roja).
En la mayoría de los sistemas, no puede esperar absolutamente ningún efecto en el código existente cuando se introducen nuevas clases. Sin embargo, respetar el principio abierto / cerrado reducirá considerablemente las clases y los módulos que requieren un cambio constante..
Al igual que con cualquier otro principio, trate de no pensar en todo desde antes. Si lo haces, terminarás con una interfaz para cada una de tus clases. Tal diseño será difícil de mantener y entender. Por lo general, la forma más segura de hacerlo es pensar en las posibilidades y si puede determinar si habrá otros tipos de clases de servidor. Muchas veces puede imaginar fácilmente una nueva característica o puede encontrar una en el registro del proyecto que producirá otra clase de servidor. En esos casos, agregue la interfaz desde el principio. Si no puede determinar, o si no está seguro, la mayoría de las veces, simplemente omítalo. Deje que el siguiente programador, o incluso usted mismo, agregue la interfaz cuando necesite una segunda implementación.
Si sigue su disciplina y agrega interfaces tan pronto como se necesite un segundo servidor, las modificaciones serán pocas y fáciles. Recuerde, si el código requerido cambia una vez, existe una alta posibilidad de que se requiera un cambio nuevamente. Cuando esa posibilidad se convierta en realidad, OCP le ahorrará mucho tiempo y esfuerzo..
Gracias por leer.