Introdución a los Principios SOLID

2.536 palabras · 11 minutos

16/08/2015

Estás leyendo la traducción al castellano que he hecho del artículo 'An Introduction to the SOLID Principles of OO Design', escrito originalmente por David Tchepak. Puedes acceder al original pulsando en este enlace.

Habría sido muy complicado ignorar que los principios SOLID de Robert "Uncle Bob" Martin son para mí la mejor herramienta con la que realizar diseños orientados a objetos. Hay desarrolladores que parecen tener un talento natural con el que realizar diseños orientados a objetos, disfrutando de una asimilación innata de estos principios sin requerir los formalismos de SOLID. Yo, en mi caso, los encuentro indispensables.

Por desgracia, los nombres utilizados usan el críptico "acrónimo de acrónimos"[1] y no permiten ser fácilmente aprendidos (Bob Martin y Scott hablaron sobre ello en el episodio de Hanselminutes sobre SOLID, el cual merece la pena escuchar). Voy a reflejar mi interpretación de los principios como una guía para mi mismo, de tal forma que pueda servir de introducción para cualquiera que quiera aprender la terminología.

Debo avisar que estoy lejos de ser un gurú de este tema, así que por favor avísame por correo o dejame un comentario ante cualquier información incorrecta, de tal forma que pueda arreglar la entrada y aprender más sobre orientación a objetos :).

Single Responsibility Principle (SPR) - Principio de Reponsabilidad Única

"Nunca debería haber más de un motivo por el que cambiar una clase" — Robert Martin, paper sobre SRP enlazado desde Los Principios del Diseño Orientado a Objetos.

Mi traducción: Una clase debería centrarse en hacer una cosa.

Que puedas no significa que debas.

SRP dice que una clase debería centrarse en hacer sólo una cosa, o tener una única responsabilidad. Esto no significa que deba tener sólo un método, tan sólo que todos los métodos deben estar centrados en un único objetivo (es decir, deben ser cohesivos).

Por ejemplo, una clase Factura puede tener la responsabilidad de calcular varios importes en base a sus datos. En este caso nuestra clase probablemente no debería saber cómo obtener estos datos de una base de datos, o cómo debe presentar la factura a la hora de ser impresa o mostrada por pantalla.

Una clase que siga el SRP será más sencilla de modificar que otra que tenga muchas responsabilidades. Si tenemos la lógica del cálculo, la lógica de la base de datos y la lógica de la presentación mezcladas en una clase puede ser difícil modificar una parte sin estropear las otras. Mezclar resposabilidades hace, además, que la clase sea más difícil de entender, más difícil de probar, e incrementa el riesgo de duplicar lógica en otras partes del diseño (la cohesión decrementa, lo que provoca que la funcionalidad no tenga un lugar claro donde ubicarse).

Las violaciones del SRP son fáciles de detectar: las clases parecen hacer demasiadas cosas, ser demasiado grandes y excesivamente complicadas. La forma más sencilla de arreglarlo es partir la clase.

El mejor truco para cumplir con SRP es decidir cómo definir la responsabilidad única. Hay varias formas de descomponer una funcionalidad en resposabilidades, pero la forma ideal es utilizar responsabilidades que puedan cambiar independientemente, de ahí la descripción oficial: "Nunca debería haber más de un motivo por el que cambiar una clase".

Open Closed Principle (OCP) - Principio de Abierto-Cerrado

"Todas las entidades software (clases, módulos, funciones, etc.) deberían estar abiertas a extensión, pero cerradas a modificación" — Robert Martin, paper sobre OCP enlazado desde Los Principios del Diseño Orientado a Objetos.

Mi traducción: Cambia el comportamiento de una clase utilizando herencia y composición.

No es necesario usar cirugía a corazón abierto para ponerse un abrigo.

En el paper inicial sobre OCP enlazado desde Los Principios del Diseño Orientado a Objetos se le atribuye la idea a Bertrand Meyer, quien dijo que las clases deben estar "abiertas a extensión, pero cerradas a modificación"[2]. La idea radica en usar técnicas de Orientación a Objetos como herencia y composición para cambiar (o extender) el comportamiento de una clase sin tener que modificar la propia clase.

Imagina que tenemos una clase llamada ValidacionPedido con un método largo llamado Validar(Pedido pedido) que contiene todas las reglas necesarias para validar un pedido. Si las reglas cambian necesitamos cambiar la clase ValidacionPedido, con lo que estamos violando el OCP. Si la clase ValidacionPedido incluyera una colección de objetos IReglaValidacion que contuvieran las reglas podríamos hacer que Validar(Pedido pedido) ejecutase todas esas reglas para validar el pedido. De este modo, si las reglas cambian sólo necesitamos crear una nueva IReglaValidacion e incluirla en la instancia de ValidacionPedido en tiempo de ejecución en lugar de hacerlo en la propia definición de la clase.

Cumplir con el OCP debería conseguir que el comportamiento fuese más fácil de cambiar, y además nos ayuda a evitar romper el comportamiento actual mientras se realizan cambios. El OCP además nos hace pensar sobre qué zonas de la clase pueden cambiar, lo cual nos ayuda a elegir abstracciones correctas necesarias para nuestro dieño.

Si ves que necesitas modificar una zona concreta del código constantemente (por ejemplo, las reglas de validación) probablemente sea el momento de utilizar OCP y abstraerte de la parte cambiante del código. Otra señal de una posible violación del OCP es la aparición de estructuras switch que utilizan tipos — si se crea otro tipo nuevo es necesario modificar el switch. En ese caso una saludable dosis de polimorfismo es el mejor tratamiento :). Por lo general veo el OCP como una señal de advertencia de que los patrones Strategy y Template Method deberían ser utilizados.

Liskov Substitution Principle (LSP) - Principio de Substitución de Liskov

"Las funciones que utilicen punteros o referencias a clases base deben ser capaces de usar objetos de clases derivadas sin saberlo" — Robert Martin, paper sobre LSP enlazado desde Los Principios del Diseño Orientado a Objetos.

Mi traducción: Las subclases deberían comportarse correctamente cuando se utilizan en lugar de las clases padre.

Si parece un pato, suena como un pato, pero necesita pilas, probablemente no estés haciendo la abstracción correcta.

LSP es engañosamente sencillo — deberíamos ser capaces de sustituir una instancia de una sublase por su clase padre y todo debería seguir funcionando. ¿Parece fácil? Bueno, realmente, no lo es, lo que probablemente se deba a que por lo general se nos ha aconsejado usar composición en lugar de herencia. Asegurarse de que una subclase funciona en cualquier situación en la que la clase padre también lo hace es realmente complicado, así es una buena idea que tengas el LSP en mente que allá donde vayas a utilizar herencia.

El ejemplo típico de violación de LSP (en realidad es el utilizado en el capítulo de Hanselminutes sobre SOLID mencionado anteriormente) es la relación Cuadrado ES-UN Rectángulo. Matemáticamente un cuadrado es un caso particular de rectángulo ya que tiene todos sus lados iguales, pero esto no encaja bien cuando se modela en el código. ¿Qué debería hacer la función establecerAncho(int ancho) cuando ésta se invoca en un Cuadrado?, ¿debería cambiar el largo también?, ¿Cómo debería funcionar si la tratas como su clase padre, la clase Rectángulo? Si tu código espera un comportamiento pero recibe otro en función al subtipo de la clase en sí puede que te enfrentes a errores muy difíciles de encontrar.

Puede ser fácil pasar por alto las violaciones de LSP hasta que aparece una situación en la cual la jerarquía de herencia se rompe (por ejemplo, la relación Cuadrado ES-UN Rectángulo) La mejor manera de reducir estos problemas es estar atento a LSP cuando se utilice herencia, incluso considerando la posibilidad de usar composición cuando sea apropiado.

Interface Segregation Principle (LIP) - Principio de Separación de Interfaces

"Los clientes no deberían estar obligados a depender de interfaces que no utilicen". — Robert Martin, paper sobre LIP enlazado desde Los Principios del Diseño Orientado a Objetos.

Mi traducción: Mantén interfaces pequeñas y cohesionadas.

¿Dónde quieres que enchufe esto?

ISP trata de mantener las interfaces (tanto las interfaces en sí como las clases abstractas) pequeñas y limitadas únicamente a una necesidad muy concreta (a una única responsabilidad :)). El crear interfaces grandes obliga a desarrollar implementaciones muy extensas a todo aquel que quiera seguir el contrato definido en la interfaz. Peor todavia es hacer clases que sólo dan una implementación real de una parte pequeña de la interfaz grande, lo cual elimina totalmente las ventajas de usar interfaces (observa que todas esas implementaciones parciales violan el LSP, por lo que no se pueden tratar a todas esas clases de la misma forma).

La primera vez que reconocí una violación de ISP fue cuando traté de escribír una pequeña implementación de la clase RoleProvider de ASP.NET, para lo cual necesitaba implementar los siguientes métodos:

public class MyRoleProvider : RoleProvider {

    public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { ... }

    public override void AddUsersToRoles(string[] usernames, string[] roleNames) { ... }

    public override string ApplicationName { get { ... } set { ... }

    public override void CreateRole(string roleName) { ... }

    public override bool DeleteRole(string roleName, bool throwOnPopulatedRole) { ... }

    public override string[] FindUsersInRole(string roleName, string usernameToMatch) { ... }

    public override string[] GetAllRoles() { ... }

    public override string[] GetRolesForUser(string username) { ... }

    public override string[] GetUsersInRole(string roleName) { ... }

    public override bool IsUserInRole(string username, string roleName) { ... }

    public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames) { ... }

    public override bool RoleExists(string roleName) { ... }

}

En mi caso sólo queria usar la utilidad de ASP.NET que permite securizar páginas en base al perfil del usuario usando el fichero web.config, lo que significa que únicamente necesitaba implementar GetRolesForUser(...) e Initialize(...). ¿Podeis imaginar cuál fue la implementación del resto de métodos?. Exacto, throw new NotImplementedException();. El hecho de tener una clase que implemente RoleProvider y no tengamos ni idea de qué métodos realmente implementa es malo. Aparte de esto también ocurre que dejamos un montón de ruido inútil dentro de nuestra clase. (Si te ha gustado RoleProvider, puede que también disfrutes con MembershipProvider).

La manera de arreglar estas violaciones consiste en descomponer esta interfaz en otras encontrando líneas de responsabilidad y aplicando SPR. En el caso de RoleProvider, incluso si la dividiésemos en IRolesForUserLookup e IRoleManagement nos bastaría para implementar únicamente lo que necesitamos. Si necesitásemos todas las características podríamos implementar ambas interfaces, pero no deberíamos obligar a los clientes a hacer implementaciones falsas o a lanzar errores en implementaciones que carecen de sentido para ellos.

Dependency Inversion Principle (DIP) - Principio de Inversión de Dependencias

"A. Los módulos de alto nivel no deberían depender de módulos de bajo nivel. Ambos deben depender de abstracciones.
B. Las abstracciones no deben depender de detalles. Los detalles deben depender de abstracciones." — Robert Martin, paper sobre DIP enlazado desde Los Principios del Diseño Orientado a Objetos.

Mi traducción: Usa muchas interfaces y abstracciones.

¿Soldarías una lámpara directamente a un enchufe eléctrico en la pared?.

DIP dice que si una clase depende de otras clases, ésta relación debería ser de dependencia de interfaces en lugar de dependencia de implementaciones concretas. La idea es aislar nuestra clase detrás de un muro de abstracciones de las que depender. Si los detalles tras las abstracciones cambian nuestra clase se encuentra a salvo. Esto ayuda a mantener un acoplamiento bajo y hace que nuestro diseño sea más fácil de cambiar.

En su ejemplo más sencillo, la diferencia está en referenciar la clase BuscadorEmpleado o la interfaz IBuscadorEmpleado. La clase concreta BuscadorEmpleado puede que acceda a una base de datos o a un archivo, pero realmente a la clase que la utiliza sólo le preocupa lo que indica el contrato de la interfaz IBuscadorEmpleado. Mejor todavía, nuestra clase no tiene por qué estar relacionada de ninguna manera con la clase BuscadorEmpleado, sino que en su lugar puede usar la clase SqlBuscadorEmpleado, XmlBuscadorEmpleado, WebServiceBuscadorEmpleado o MockBuscadorEmpleado.

Donde DIP comienza a ser más útil y un poco más profundo es en un concepto relacionado llamado Inyección de Dependencias. La Inyección de Dependencias consiste en incluir unas clases dentro de otras que las necesitan, de tal forma que no haya que hacer new() de instancias concretas. Esta técnica aisla nuestras clases y consigue que los cambios y la reutilización sean mucho más fáciles de conseguir. (He escrito una introducción en una divagación anterior sobre Inyección de Dependencias).

La otra faceta de DIP se encuentra en las dependencias que hay entre módulos de alto y bajo nivel en aplicaciones que utilizan un diseño basado en capas. Por ejemplo, una clase que acceda a la base de datos no debería depender de un formulario mostrado en la interfaz gráfica con el que mostrar esos datos. En su lugar, la interfaz gráfica debería apoyarse en una abstracción (o abstracciones) sobre la clase que acceda a la base de datos. Las capas tradicionales utilizadas en las aplicaciones (datos, lógia, interfaz gráfico) han sido reemplazadas por el patrón MVC, la arquitectura onion y la arquitectura hexagonal, así que tiendo a pensar en DIP únicamente desde la perspectiva de abstracción de dependencias.

Principios SOLID como un Todo

Probablemente hayas notado que los principios SOLID se superponen mucho. Por ejemplo, SRP facilita una buena forma de partir interfaces para cumplir con ISP. ISP ayuda a quienes vayan a realizar las implementaciones a cumplir con LSP haciendo desarrollos pequeños y cohesivos. Puede que, además, observes que algunos de los principios se contradicen, o al menos empujan en direcciones opuestas, como OCP pidiendo herencia y LSP desaconsejándola[3]. Esta interactuación entre los principios puede servirte de guía cuando estás realizando tu diseño. Creo que no hay diseños perfectos, únicamente compromisos, y que los principios SOLID pueden ayudarte a evaluarlos y a alcanzar un balance equilibrado. El hecho de que haya cierto conflicto entre ellos hace que sea obvio que ninguno de los principios debe ser seguido al pie de la letra ni dogmáticamente. ¿Realmente necesitas hacer una gran descomposición en tus interfaces para cumplir OCP y DIP? Quizá sí, quizá no, pero considerar los principios SOLID puede ayudarte a decidir.

Los principios SOLID encajan naturalmente con la práctica de TDD (o BDD). Por ejemplo, escribir un test unitario realmente efectivo para una clase es mucho más fácil si cumples con DIP y aislas tu clase de sus dependencias. Si estás escribiendo el test primero para así dirigir su diseño, entonces tus clases tenderán a usar DIP naturalmente. Si estás haciendo tests de clases que ya están escritas, probablemente encontrarás que es más dificil hacer tests, y puede que acabes haciendo tests que requieran interacción del usuario, que reescribas clases para seguir DIP o, peor todavia, que las tires a la papelera de las clases demasiado-dificil-de-probar.

Este es el motivo por el cual muchos desarrolladores dicen que TDD y testeabilidad no tratan sobre tests, sino sobre diseño. Scott Bellware ha publicado recientemente un buen artículo sobre diseño, SOLID y testeabilidad en el que entra en más detalle.

Conclusiones

Esta ha sido una introducción rápida a los principios SOLID. Espero que haga que sea más sencillo el adentrarse en los cruentos detalles de todos los principios. Incluso si tienes una asimilación innata de estos principios creo que merece la pena aprenderlos, al menos para tener un lenguaje común con el que poder debatir sobre ellos.

Lecturas y Audios Recomendados

Referencias