9 principios de diseño de aplicaciones que deberías conocer

Reglas y recomendaciones que te ayudarán a diseñar aplicaciones mantenibles y mejor optimizadas

Gerardo Fernández
May 3 · 10 min read

Recientemente he releído el libro Head First Design Patterns, en donde se explican algunos de los principales patrones de diseño y los principios de diseño a los que responden (los cuales habitualmente se resumen tras el acrónimo SOLID). Tras terminarlo, me he decidido a escribir este artículo en donde hablaré de estos principios de diseño explicándolos mediante ejemplos ya que creo que a la hora de desarrollar aplicaciones complejas que sean posteriormente mantenibles y escalables es muy interesante familiarizarse con ellos.

Sin más introducción, vamos a ello!

1. Encapsular aquello que está sujeto a muchos cambios

Cuando planteamos la arquitectura de nuestras aplicaciones es interesante acostumbrarnos a tratar de identificar aquellas partes que cambiarán habitualmente, de modo que podamos aislarlas del resto de piezas que componen el diseño.

Esto nos permitirá realizar modificaciones sobre dichas partes de forma mucho más fácil y rápido, ya que solo tendremos que dirigirnos a una parte de nuestra aplicación en vez de tener que modificar multitud de ficheros.

Además, tener encapsuladas este tipo de partes nos permite realizar modificaciones de forma mucho más segura, porque… ¿quién no ha tenido que modificar decenas de archivos para introducir un cambio a la vez que cruzaba los dedos para que todo siguiera funcionando?

Patrón de diseño relacionado: Estrategia

Define una familia de algoritmos, encapsula cada uno de ellos y los hace intercambiables lo cual permite que el cliente pueda modificar su comportamiento sin tener que modificar su código.

Patrón de diseño: Estrategia

2. Programar a interfaces, no a implementaciones

Para mí este es uno de los principios de diseño más poderosos que podemos adoptar cuando estamos diseñando aplicaciones. Lo que nos sugiere es que adoptemos la costumbre de programar empleando interfaces en vez de tipos concretos de clases de cara a:

  • conseguir que nuestras clases puedan intercambiarse en tiempo de ejecución
  • el cliente que emplea dichos objetos no tenga que saber a qué tipo concreto pertenecen, sino que emplee una interfaz (supertipo) común para todos ellos.

Por ejemplo, supongamos que tenemos distintas clases que implementan distintos tipos de animales: Dog , Cat … Si no estuviéramos adoptando este principio de diseño probablemente nos encontraríamos con cosas similares a las siguientes:

Dog dog = new Dog();dog.bark();Cat cat = new Cat();cat.meow();

Sin embargo, si estuviéramos programando a interfaces lo correcto sería poseer una interfaz o supertipo denominado Animal (por ejemplo) con un método abstracto llamado makeSound el cual sería implementado por las clases concretas Dog , Cat … de modo que los clientes que empleen estos objetos no tengan que saber si están recibiendo un objeto de tipo Dog o Cat sino que emplearán directamente la interfaz Animal .

public function aClientMethod(Animal animal) {  animal.makeSound();}

3. Favorece la composición frente a la herencia

Desde que la programación orientada a objetos se popularizó, es muy habitual añadir funcionalidad a nuestras clases mediante herencia, pues parece coherente que si poseemos una superclase, una clase la extienda para añadir comportamiento concreto.

Sin embargo, esto provoca que nuestros diseños sean mucho menos flexibles: conforme queramos añadir más comportamiento seguiremos recurriendo a la herencia lo cual puede desembocar en algo similar a lo siguiente:

Por contra, recurrir a la composición tiene múltiples ventajas (entendiendo composición como objetos que poseen instancias de otras clases), pero la más importante es la posibilidad de modificar comportamiento en tiempo de ejecución algo que es bastante más complicado de hacer si nuestros diseños priman el concepto IS-A en vez del principio HAS-A.

Patrón de diseño relacionado: Decorador

El patrón de diseño decorador nos permite añadir funcionalidad a clases sin necesidad de extenderlas.

Como su propio nombre indica, lo que haremos será decorar nuestras clases mediante composición. De este modo, la clase decoradora implementará la misma interfaz pero añadiendo comportamiento extra sobre los métodos de dicho supertipo.

4. Plantea diseños poco acoplados entre objetos que interactúan

El acomplamiento entre clases/objetos que interactúan es uno de los principales problemas que nos podemos encontrar a la hora de crear aplicaciones mantenibles y flexibles en el tiempo.

Por contra, cuando dos clases están poco acopladas, pueden seguir interactuando pero sin tener conocimiento exacto de la forma en que cada una de ellas están implementadas, por lo que introducir cambios en una de ellas no obliga a modificar la otra.

Patrón de diseño relacionado: Observer

Este es uno de los patrones de diseño más conocidos y probablemente uno de los que mejor responden a este principio. Define una dependencia uno-muchos entre objetos, de modo que cuando el subject modifica su estado, todos los observers (que implementan una misma interfaz) son notificados.

Aplicando este patrón obtendremos diseños poco acoplados ya que:

  • la clase subject solo conoce que los observers implementan una misma interfaz
  • podemos añadir en cualquier momento nuevos observers
  • para añadir un nuevo observer al subject no es necesario modificar dicha clase, sino que basta con que implemente la interfaz Observer
  • si queremos modificar el subject esto no afecta a los observers y viceversa

5. Principio Abierto / Cerrado

O lo que es lo mismo, las clases que escribamos deberían estar cerradas a la modificación pero abiertas a la extensión de su comportamiento. Es decir:

  • Cuando decimos que una clase debe estar abierta a su extensión, queremos decir que dichas deben poder alterar su comportamiento bien por extensión de las mismas bien por composición sin que nos veamos obligados a modificar su código interno
  • Por otra parte, cerrado a la modificación significa que no es buena práctica permitir que terceros desarrolladores modifiquen el código interno de la clase para añadir comportamiento, ya que esto puede redundar en la introducción de bugs y comportamientos inesperados.

Patrón de diseño: Proxy

El patrón de diseño Proxy es muy similar al patrón Decorador, salvo que su objetivo es delegar el acceso a los objetos de la clase en una clase que implementa su misma interfaz.

Esto por ejemplo nos permite tener objetos huecos que actúan como placeholders de otros objetos que pueden ser muy costosos de instanciar hasta el momento de tener que usarlos.

Patrón Proxy

6. Principio de inversión de dependencias

Probablemente éste sea uno de los principios de diseño más abstractos para un programador junior y, sin embargo, más importantes si queremos conseguir un código mantenible y fácilmente testeable.

Uno de sus posibles enunciados sería el siguiente: los componentes de alto nivel no deberían depender de los de bajo nivel sino que ambos deberían hacerlo de abstracciones.

Nota. Entendemos por componentes de alto nivel clases cuyo comportamiento está definido en términos de otras clases, las clases de bajo nivel.

Este principio es la versión fuerte del principio que vimos al comienzo de este artículo que nos instaba a programar con interfaces en vez de clases concretas siempre que pudiéramos.

Por ejemplo, supongamos que poseemos una clase ( Veterinarian) encargada de trabajar con distintos tipos (clases) de animales ( Dog , Cat , etc.). Si no aplicamos este principio, la clase Veterinarian trabajará directamente con instancias concretas de las clases Dog , Cat . Puesto que cualquier cambio en estas clases concretas podría suponer un cambio en la clase Veterinarian , diremos que dicha clase depende de las distintas clases de animales.

Sin inversión de dependencias

Por el contrario, atendiendo al principio de inversión de dependencias, escribiremos nuestro código de modo que tanto los componentes de bajo nivel ( Dog , Cat , etc.) como los de alto nivel ( Veterinarian ) dependan de abstracciones. Para ello, declararemos la interfaz abstracta Animal , que será con la que trabaje la clase Veterinarian (eliminando su dependencia por tanto de las clases concretas) y que extenderán todos los tipos de animales.

Inversión de dependencias

Como veis, el sentido de las flechas ha cambiado lo cual simboliza que hemos aplicado el principio.

Patrón de diseño relacionado: Factoría

Uno de los patrones que nos pueden ayudar a responder a este principio es el patrón Factoría, el cual nos permite delegar la creación de clases concretas a un clase denominada Factoría, de modo que el componente de alto nivel no dependa de las implementaciones concretas de las clases.

Este patrón tiene distintas formas de presentarse, bien en lo que se conoce como Factoría Abstracta bien como Método Factoría. Os dejo los enlaces por si queréis echar un ojo a ambos patrones:

7. Principio de menos conocimiento

Este es uno de esos principios que nos servirá para escribir código mucho más limpio con el fin de evitar encadenar llamadas a los métodos de los objetos con tal de obtener cierto dato.

Supongamos que tenemos el método de cierto objeto, el principio de menos conocimiento nos insta a sólo invocar métodos que pertenezcan a:

  • el objeto en sí mismo
  • objetos pasados como parámetros al método
  • cualquier objeto creado dentro del método
  • cualquier componente del objeto

De este modo, el principio nos permite reducir el conocimiento que los objetos tienen de otros objetos reduciendo en parte la dependencia y acoplamiento entre ambos.

Por ejemplo, sin aplicar este principio tendríamos:

public function getFirstTeamCaptain() {  $firstTeam = $this->match->getFirstTeam();  $captain = $firstTeam->getCaptain();  return $captain;}

Como podéis observar, existen dos niveles en la llamada lo cual no responde al principio de menos conocimiento. Si quisiéramos aplicar este principio, deberíamos delegar la llamada al objeto match para obtener el capitán del primer equipo:

public function getFirstTeamCaptain() {  $captain = $this->match->getFirstTeamCaptain();  return $captain;}

Esto, que puede parecer farragoso en un primer momento evita tener que conocer los objetos a dos niveles de distancia desde la clase que necesita su información, de modo que un cambio en ese segundo nivel no afectará al objeto que está empleando su información.

8. Principio de Hollywood

Este principio, muy relacionado con el de inversión de dependencias, nos insta a evitar que los componentes de bajo nivel llamen a los de alto nivel. Dicho de otro modo, los componentes de alto nivel siempre son los que llaman a los de bajo nivel y no al revés o como dirían en Hollywod:

No nos llames, nosotros lo haremos

Esto evitará la creación de interdependencias entre los componentes de bajo nivel y los de alto nivel algo que dificulta la comprensión de como una aplicación está diseñada.

Patrón de diseño relacionado: Template Method

Suponed que tenéis una aplicación con Paypal y Stripe como plataformas de pago. Probablemente, tendréis dos clases que implementan el proceso de pago, si bien ambas clases compartirán código similar (marcar la factura como pagada, enviar el email de compra al cliente, etc.).

El patrón template method nos permite abstraer el código común a una clase abstracta delegando detalles concretos de la implementación en clases concretas:

Template method

En este caso, el componente de alto nivel sería la clase Worker que en su método DailyRoutine llama a la implementación del método work en las clases concretas para llevar a cabo el algoritmo. De este modo, es la clase de alto nivel la que llama a las clases de bajo nivel y no al revés.

9. Responsabilidad única

Cada clase o función debería tener un único motivo para el cambio. Es decir, cualquier clase que declaremos o función deberán ocuparse de realizar una única tarea delegando en otras clases el resto de tareas bien por composición (lo preferible, recordad) bien por herencia.

Si tenemos en mente este principio lograremos escribir código mucho más limpio y que podremos mantener / testear de forma mucho más sencilla pues cada vez que modifiquemos un fichero sabremos que sólo estamos afectando a una única parte de la funcionalidad de la aplicación y no a múltiples.

Otra de las ventajas de seguir este principio es que conseguiremos ficheros mucho más cortos pues dentro de ellos no tendremos cientos de líneas donde se lleven a cabo otras tantas tareas.

Y conclusiones

Estos son los 9 principios básicos que he extraído del libro que os comentaba al principio. Tened en mente que tan sólo son principios no normas, de modo que queda en vuestra responsabilidad determinar cuando aplicarlos buscando el equilibrio entre la complejidad que deriva de su aplicación y la consecución de arquitecturas mantenibles en el largo plazo.

¿Quieres recibir más artículos como este?

Suscríbete a nuestra newsletter:

Gerardo Fernández

Written by

Entre paseo y paseo con Simba desarrollo en Symfony y React

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade