Escribir programas informáticos es muy divertido. A menos que tenga que trabajar con el código de otras personas.

Si ha trabajado como desarrollador profesional durante más de tres días, sabrá que nuestros trabajos son cualquier cosa menos creativos y emocionantes. Una parte de la razón es la dirección de la empresa (léase: gente que nunca lo entiende), mientras que la otra parte es la complejidad del código con el que tenemos que trabajar. Ahora bien, mientras que no podemos hacer absolutamente nada sobre lo primero, sí podemos hacer mucho sobre lo segundo.

Entonces, ¿por qué las bases de código son tan complejas que nos entran ganas de destriparlas? Sencillamente porque la gente que escribió la primera versión tenía prisa, y los que vinieron después no hicieron más que aumentar el desorden. El resultado final: un lío espeso que muy pocos quieren tocar y que nadie entiende.

Bienvenido al primer día de trabajo

«Nadie dijo que sería tan difícil…»

Pero no tiene por qué ser así.

Escribir buen código, un código modular y fácil de mantener, no es tan difícil. Tan sólo cinco principios sencillos, establecidos desde hace tiempo y bien conocidos, si se siguen con disciplina, harán que su código sea legible, para los demás y para usted cuando lo mire seis meses después. 😂

Estos principios rectores están representados por el acrónimo SOLID. Quizás haya oído hablar antes del término «principios SOLID», quizás no. En caso de que sí lo haya hecho pero haya estado aplazando este aprendizaje para «algún día», pues bien, ¡vamos a asegurarnos de que hoy sea ese día!

Así que, sin más preámbulos, veamos de qué va todo esto de SOLID y cómo puede ayudarnos a escribir un código ajustado y realmente limpio.

«S» es de Responsabilidad Única

Si busca en diferentes fuentes que describan el Principio de Responsabilidad Única, obtendrá alguna variación en su definición. Sin embargo, en términos más sencillos, se reduce a esto: Cada clase de su base de código debe tener un papel muy específico; es decir, no debe ser responsable más que de un único objetivo. Y siempre que sea necesario un cambio en esa clase, se deduce que tendremos que cambiarla sólo porque ha cambiado esa única responsabilidad específica.

Cuando me encontré con esto la primera vez, la definición que me presentaron fue: «Debería haber una, y sólo una razón para que una clase cambie». Me quedé en plan: «¡¿Qué?! ¿Cambiar? ¿Qué cambio? ¿Por qué cambiar?», por eso dije antes que si lee sobre esto en diferentes sitios, obtendrá definiciones relacionadas pero algo diferentes y potencialmente confusas.

En fin, basta ya. Es hora de algo serio: si es como yo, probablemente se estará preguntando: «Vale, todo bien. Pero, ¿por qué demonios debería importarme? No voy a empezar a escribir código en un estilo totalmente diferente a partir de mañana sólo porque un loco que una vez escribió un libro (y ahora está muerto) lo diga»

¡Excelente!

Y ése es el espíritu que debemos mantener si queremos aprender cosas de verdad. Entonces, ¿por qué importa toda esta cantinela sobre la «responsabilidad única»? Diferentes personas lo explican de forma diferente, pero para mí, este principio consiste en aportar disciplina y concentración a su código.

Enfoque, muchacho. Enfoque

Veamos un ejemplo antes de explicar mi interpretación. A diferencia de otros recursos encontrados en la web que proporcionan ejemplos que sí entiende pero luego le dejan preguntándose cómo le ayudarían en casos del mundo real, vamos a sumergirnos en algo específico, un estilo de codificación que vemos una y otra vez, y quizás incluso escribimos en nuestras aplicaciones Laravel.

Cuando una aplicación Laravel recibe una solicitud web, la URL se compara con las rutas que ha definido en web.php y api.php, y si hay una coincidencia, los datos de la solicitud llegan al controlador. He aquí el aspecto de un método controlador típico en aplicaciones reales a nivel de producción:

class UserController extends Controller {
    función pública store(Solicitud $request)
    { 
        $validator = Validator::make($petición->todos(), [
           'first_name' => 'required',
           'apellido' => 'requerido',
           'email' => 'requerido|email|único:usuarios',
           'teléfono' => 'nullable'
       ]);
        
       if ($validator->fails()) {
            Session::flash('error', $validator->messages()->first());
            return redirect()->back()->conEntrada();
       }
       
       // crear nuevo usuario
       $user = Usuario::crear([
           'first_name' => $request->first_name,
           'apellido' => $solicitud->apellido,
           'email' => $request->email,
           'teléfono' => $solicitud->teléfono,
       ]);
        
       return redirect()->route('login');
    }
}

Todos hemos escrito código como éste. Y es fácil ver lo que hace: registrar nuevos usuarios. Se ve bien y funciona bien, pero hay un problema: no es a prueba de futuro. Y por a prueba de futuro, quiero decir que no está preparado para manejar cambios sin crear un lío.

¿Por qué?

Se puede decir que la función está pensada para rutas definidas en el archivo web. php; es decir, páginas tradicionales, renderizadas en el servidor. Pasan unos días, y ahora su cliente/empleador está desarrollando una aplicación móvil, lo que significa que esta ruta no servirá para que los usuarios se registren desde dispositivos móviles. ¿Qué debe hacer? ¿Crear una ruta similar en el archivo api.php y escribir una función de controlador basada en JSON para ella? Bien, ¿y después qué? ¿Copiar todo el código de esta función, hacer algunos cambios y darlo por terminado? Esto es de hecho lo que muchos desarrolladores están haciendo, pero se están preparando para el fracaso.

El problema es que HTML y JSON no son los únicos formatos de API del mundo (consideremos que las páginas HTML son una API por el bien de la discusión). ¿Qué pasa con un cliente que tiene un sistema heredado que funciona con el formato XML? Y luego hay otro para SOAP. Y gRPC. Y Dios sabe qué más vendrá al día siguiente.

Aún así, podría plantearse crear un archivo independiente para cada uno de estos tipos de API y copiar el código existente, modificándolo ligeramente. Seguro que son diez archivos, argumentaría usted, pero todo funciona bien así que, ¿por qué quejarse? Pero entonces llega el puñetazo en el estómago, la némesis del desarrollo de software: el cambio. Supongamos ahora que las necesidades de su cliente/empleador han cambiado. Ahora quieren que, en el momento del registro del usuario, registremos la dirección IP, así como que añadamos una opción para un campo que indique que han leído y entendido los términos y condiciones.

Ahora tenemos diez archivos que editar y debemos asegurarnos de que la lógica se maneja exactamente igual en todos ellos. Incluso un solo error puede causar grandes pérdidas empresariales. Y ahora imagine el horror en las aplicaciones SaaS a gran escala, ya que la complejidad del código ya es bastante elevada.

Maldita sea…

¿Cómo hemos llegado a este infierno?

La respuesta es que el método del controlador que parece tan inofensivo está haciendo en realidad una serie de cosas diferentes: está validando la solicitud entrante, está gestionando redireccionamientos y está creando nuevos usuarios.

¡Está haciendo demasiadas cosas! Y sí, como se habrá dado cuenta, saber cómo crear nuevos usuarios en el sistema no debería ser en realidad un trabajo de los métodos del controlador. Si sacáramos esta lógica de la función y la pusiéramos en una clase separada, ahora tendríamos dos clases, cada una con una sola responsabilidad que manejar. Mientras que estas clases pueden ayudarse mutuamente llamando a sus métodos, no se les permite saber lo que está pasando dentro de la otra.

class UserController extends Controller {
    public function almacenar(Solicitud $request)
    { 
        $validator = Validator::make($petición->todos(), [
           'first_name' => 'required',
           'apellido' => 'requerido',
           'email' => 'requerido|email|único:usuarios',
           'teléfono' => 'nullable'
       ]);
        
       if ($validator->fails()) {
            Session::flash('error', $validator->messages()->first());
            return redirect()->back()->conEntrada();
       }
       
       UserService::createNewUser($request->all());
       return redirect()->ruta('login');
    }
}

Mire el código ahora: mucho más compacto, fácil de entender . . . y lo más importante, adaptable al cambio. Continuando con nuestra discusión anterior en la que teníamos diez tipos diferentes de API, ahora cada uno de ellos llama a una única función UserService::createNewUser($request->all()); y listo. Si se necesitan cambios en la lógica de registro de usuarios, la clase UserService se encargará de ello mientras que los métodos del controlador no necesitan cambiar en absoluto. Si es necesario establecer la confirmación por SMS tras el registro del usuario, el UserService se encargará de ello (llamando a alguna otra clase que sepa cómo enviar SMS), y de nuevo los controladores quedan intactos.

Esto es a lo que me refería con enfoque y disciplina: enfoque en el código (una cosa haciendo una sola cosa) y disciplina por parte del desarrollador (no caer en soluciones a corto plazo).

Bueno, ¡ha sido todo un recorrido! Y sólo hemos cubierto uno de los cinco principios. ¡Sigamos adelante!

«O» de Abierto-Cerrado

Debo decir que quienquiera que ideara las definiciones de estos principios no pensaba ciertamente en los desarrolladores menos experimentados. Lo mismo ocurre con el principio Abierto-Cerrado, y los que vienen a continuación van un paso por delante en rareza. 😂😂

En cualquier caso, veamos la definición que todo el mundo ha encontrado para este principio: Las clases deben estar abiertas a la ampliación pero cerradas a la modificación. ¿Eh? Sí, a mí tampoco me hizo gracia cuando me topé con ella por primera vez, pero con el tiempo he llegado a comprender -y admirar- lo que esta regla trata de decir: el código, una vez escrito, no debería tener que modificarse.

En un sentido filosófico, esta regla es genial — si el código no cambia, seguirá siendo predecible y no se introducirán nuevos errores. Pero, ¿cómo es posible siquiera soñar con un código que no cambie cuando todo lo que hacemos como desarrolladores es perseguir los faldones del cambio todo el tiempo?

Bueno, en primer lugar, el principio no significa que no se permita cambiar ni una sola línea del código existente; eso sería sacado directamente de un país de las hadas. El mundo cambia, los negocios cambian y, por tanto, el código cambia: no hay vuelta de hoja. Pero lo que sí significa este principio es que restringimos al máximo la posibilidad de cambiar el código existente. Y también dice más o menos cómo hacerlo: las clases deben estar abiertas a la extensión y cerradas a la modificación.

«Extensión» significa aquí reutilización, tanto si la reutilización se produce en forma de clases hijas que heredan funcionalidad de una clase padre, como si otras clases almacenan instancias de una clase y llaman a sus métodos.

Así pues, volvamos a la pregunta del millón: ¿cómo escribir código que sobreviva a los cambios? Y aquí, me temo, nadie tiene una respuesta clara. En la programación orientada a objetos, se han descubierto y perfeccionado varias técnicas para lograr este objetivo, desde estos principios SOLID que estamos estudiando hasta patrones de diseño comunes, patrones empresariales, patrones arquitectónicos y demás. No existe una respuesta perfecta, por lo que un desarrollador debe ir cada vez más arriba, reunir tantas herramientas como pueda e intentar hacerlo lo mejor posible.

Con esto en mente, veamos una de esas técnicas. Supongamos que necesitamos añadir la funcionalidad de convertir un contenido HTML dado (¿quizá una factura?) en un archivo PDF y además forzar una descarga inmediata en el navegador. Supongamos también que disponemos de la suscripción de pago de un hipotético servicio llamado MilkyWay, que se encargará de la generación real del PDF. Podríamos acabar escribiendo un método controlador como este

class FacturaController extends Controlador {
    public function generarDescargaPDF(Solicitud $solicitud) {
        $pdfGenerator = nuevo MilkyWay();
        $pdfGenerator->apiKey = env('MILKY_WAY_API_KEY');
        $pdfGenerator->setContent($request->content); // Formato HTML
        $pdfFile = $pdfGenerator->generateFile('factura.pdf');

        return respuesta()->descargar($archivo_dfdf, [
            content-Type' => 'application/pdf',
        ]);
    }
}

He omitido la validación de la solicitud, etc., para centrarme en la cuestión principal. Notará que este método hace un buen trabajo siguiendo el Principio de Responsabilidad Única: no intenta recorrer el contenido HTML que se le pasa y crear un PDF (de hecho, ni siquiera sabe que se le ha dado HTML); en su lugar, pasa esa responsabilidad a la clase especializada MilkyWay, y presenta lo que obtiene, como una descarga.

Pero hay un pequeño problema.

Nuestro método controlador depende demasiado de la clase MilkyWay. Si la próxima versión de la API MilkyWay cambia la interfaz, nuestro método dejará de funcionar. Y si algún día deseamos utilizar algún otro servicio, tendremos que hacer literalmente una búsqueda global en nuestro editor de código y cambiar todos los fragmentos de código que mencionen a MilkyWay. ¿Y por qué es eso malo? Porque aumenta enormemente la posibilidad de cometer un error y es una carga para la empresa (tiempo de los desarrolladores dedicado a arreglar el desaguisado).

Todo este despilfarro porque creamos un método que no estaba cerrado al cambio.

¿Podemos hacerlo mejor?

Sí, ¡podemos!

En este caso, podemos aprovechar una práctica que va más o menos así: programar para interfaces, no para implementaciones.

Sí, lo sé, es otro de esos OOPSismos que no tienen sentido a la primera. Pero lo que viene a decir es que nuestro código debe depender de tipos de cosas, y no de cosas concretas en sí mismas. En nuestro caso, necesitamos liberarnos de tener que depender de la clase MilkyWay, y en su lugar depender de un genérico, un tipo de clase PDF (todo quedará claro en un segundo).

Ahora bien, ¿qué herramientas tenemos en PHP para crear nuevos tipos? A grandes rasgos, tenemos la Herencia y las Interfaces. En nuestro caso, crear una clase base para todas las clases PDF no será una gran idea porque es difícil imaginar que diferentes tipos de motores/servicios PDF compartan el mismo comportamiento. Tal vez puedan compartir el método setContent(), pero incluso ahí, el proceso de adquisición de contenido podría ser diferente para cada clase de servicio PDF, por lo que escribirlo todo en una jerarquía de Herencia empeorará las cosas.

Entendido esto, creemos una interfaz que especifique qué métodos queremos que contengan todas nuestras clases de motor PDF:

interfaz IPDFGenerator {
    public function setup(); // Claves API, etc.
    public function setContent($contenido);
    public function generatePDF($nombrearchivo = null);
}

Entonces, ¿qué tenemos aquí?

A través de esta interfaz, estamos diciendo que esperamos que todas nuestras clases PDF tengan al menos esos tres métodos. Ahora bien, si el servicio que queremos utilizar (MilkyWay, en nuestro caso) no sigue esta interfaz, es nuestro trabajo escribir una clase que lo haga. Un esbozo de cómo podríamos escribir una clase envoltorio para nuestro servicio MilkyWay es el siguiente:

class MilkyWayPDFGenerator implements IPDFGenerator {
    función pública __construct() {
        $this->configurar();
    }

    public function setup() {
        $this->generador = nuevo MilkyWay();
        $this->generator->api_key = env('MILKY_WAY_API_KEY');
    }

    public function setContent($content) {
        $this->generator->setContent($content);
    }

    public function generarPDF($nombreArchivo) {
        return $this->generador->generarArchivo($nombreArchivo);
    }
}

Y así, cada vez que tengamos un nuevo servicio PDF, escribiremos una clase envoltorio para él. Como resultado, todas esas clases se considerarán de tipo IPDFGenerator.

Entonces, ¿cómo se relaciona todo esto con el Principio Abierto-Cerrado y Laravel?

Para llegar a ese punto debemos conocer otros dos conceptos clave: Los enlaces de contenedor de Laravel, y una técnica muy común llamada inyección de dependencia. De nuevo, palabras mayores, pero inyección de dependencia significa simplemente que en lugar de crear objetos de clases usted mismo, los menciona en argumentos de funciones y algo los creará por usted automáticamente. Esto le libera de tener que escribir código como $account = new Account(); todo el tiempo y hace que el código sea más comprobable (un tema para otro día). Este «algo» que he mencionado toma la forma del Contenedor de Servicio en el mundo Laravel.

Por ahora, sólo piense en ello como algo que puede crear nuevas instancias de clase para nosotros. Vamos a ver cómo esto ayuda.

En el Contenedor de Servicio de nuestro ejemplo, podemos escribir algo como esto:

$this->app->bind('App\Interfaces\IPDFGenerator', 'App\Services\PDF\MilkyWayPDFGenerator');

Esto básicamente está diciendo, cada vez que alguien le pida un IPDFGenerator, entréguele la clase MilkyWayPDFGenerator. Y después de toda esa cantinela, señoras y señores, llegamos al punto en el que todo encaja y ¡se revela el Principio Abierto-Cerrado en funcionamiento!

Armados con todo este conocimiento, podemos reescribir nuestro método controlador de descarga de PDF de la siguiente manera:

class FacturaController extends Controlador {
    public function generatePDFDownload(Solicitud $request, IPDFGenerator $generator) {
        $generator->setContent($petición->contenido);
        $pdfFile = $generator->generatePDF('factura.pdf');

        return respuesta()->descargar($archivodfdf, [
            content-Type' => 'application/pdf',
        ]);
    }
}

¿Nota la diferencia?

En primer lugar, estamos recibiendo nuestra instancia de clase generadora de PDF en el argumento de la función. Esto está siendo creado y pasado a nosotros por el contenedor de servicio, como e discutió anteriormente. El código también es más limpio y no hay mención de claves API, etc. Pero, lo más importante, no hay rastro de la clase MilkyWay. Esto también tiene el beneficio secundario añadido de hacer que el código sea más fácil de leer (alguien que lo lea por primera vez no dirá: «¡Vaya! ¿WTF es esto MilkyWay?» y se preocupará constantemente por ello en el fondo de su cabeza).

¿Pero la mayor ventaja de todas?

Este método ya no admite modificaciones ni cambios. Permítame que se lo explique. Supongamos que mañana nos parece que el servicio MilkyWay es demasiado caro (o, como suele ocurrir, su atención al cliente se ha vuelto una mierda); como resultado, probamos otro servicio llamado SilkyWay y queremos pasarnos a él. Todo lo que tenemos que hacer ahora es escribir una nueva clase envolvente IPDFGenerator para SilkyWay y cambiar la vinculación en nuestro código del contenedor de servicios:

$this->app->bind('App\Interfaces\IPDFGenerator', 'App\Services\PDF\SilkyWayPDFGenerator');

¡¡Eso es todo!!

No hay que cambiar nada más, porque nuestra aplicación está escrita según una interfaz (la interfaz IPDFGenerator) en lugar de una clase concreta. El requisito de negocio cambió, se añadió algo de código nuevo (una clase envoltorio) y sólo se cambió una línea de código — todo lo demás permanece intacto y todo el equipo puede irse a casa con confianza y dormir tranquilo.

¿Quiere dormir tranquilo? ¡Siga el principio abierto-cerrado! 🤭😆

«L» de Sustitución de Liskov

¿Liskov-qué?

Esto parece sacado directamente de un libro de texto de Química Orgánica. Puede que incluso le haga arrepentirse de haber elegido el desarrollo de software como carrera porque pensaba que era todo práctica y nada de teoría.

«¡Rápido, chico! ¿Cuál es el mayor número primo que puede expresarse como suma de dos números primos?»

¡Pero aguante un momento! Créame, este principio es tan fácil de entender como intimidante es su nombre. De hecho, puede que sea el principio más fácil de entender de los cinco (bueno, err… si no el más fácil, al menos tendrá la explicación más breve y directa).

Esta regla simplemente dice que el código que funciona con clases padre (o interfaces) no debe romperse cuando esas clases se sustituyen por clases hijo (o clases que implementan interfaces). El ejemplo que acabamos justo antes de esta sección es una gran ilustración: si sustituyo la clase genérica IPDFGenerator en el argumento del método por la instancia específica MilkyWayPDFGenerator, ¿esperaría que el código se rompiera o que siguiera funcionando?

Seguiría funcionando, ¡por supuesto! Eso es porque la diferencia está sólo en los nombres, y tanto la interfaz como la clase tienen los mismos métodos funcionando de la misma manera, por lo que nuestro código funcionará como antes.

Entonces, ¿cuál es el gran problema de este principio? Bueno, en términos más sencillos, eso es todo lo que este principio está diciendo: asegúrese de que sus subclases implementan todos los métodos exactamente como se requiere, con el mismo número y tipo de argumentos, y el mismo tipo de retorno. Si incluso un solo parámetro difiriera, sin darnos cuenta seguiríamos construyendo más código encima, y un día tendremos el tipo de lío apestoso cuya única solución sería derribarlo.

Ya está. Ahora no ha estado tan mal, ¿verdad? 😇

Se puede decir mucho más sobre la Sustitución de Liskov (busque su teoría y lea sobre Tipos Covariantes si se siente realmente valiente), pero en mi opinión, esto es suficiente para el desarrollador medio que se encuentra por primera vez con estas misteriosas tierras de patrones y principios.

«I» de Segregación de interfaces

Segregación de interfaces… hmm, no suena tan mal, ¿verdad? Parece que tiene algo que ver con segregar . . . umm, separar . . . interfaces. Sólo me pregunto dónde y cómo.

Si estaba pensando en lo mismo, créame, casi ha terminado de entender y utilizar este principio. Si los cinco principios SOLID fueran vehículos de inversión, éste sería el que aportaría el mayor valor a largo plazo para aprender a codificar bien (vale, me doy cuenta de que digo eso de todos los principios, pero ya sabe, capta la idea).

Despojado de jerga pomposa y destilado hasta su forma más básica, el principio de Segregación de Interfaces dice lo siguiente: Cuanto más numerosas y especializadas sean las interfaces de su aplicación, más modular y menos raro será su código.

Veamos un ejemplo muy común y práctico. Todo desarrollador de Laravel se topa en su carrera con el llamado Patrón de Repositorio, tras lo cual pasa las siguientes semanas atravesando una fase cíclica de altos y bajos, y finalmente descarta el patrón. ¿Por qué? En todos los tutoriales que cubren el Patrón de Repositorio, se le aconseja crear una interfaz común (llamada Repositorio) que definirá los métodos necesarios para acceder o manipular datos. Esta interfaz base podría parecerse a esto

interfaz IRepository {
    función pública getOne($id);
    función pública getAll();
    public function crear(array $datos);
    public function actualizar(array $datos, $id);
    public function delete($id);
}

Y ahora, para su modelo de Usuario, se supone que debe crear un UserRepository que implemente esta interfaz; luego, para su modelo de Cliente, se supone que debe crear un CustomerRepository que implemente esta interfaz; ya se hace una idea.

Ahora bien, sucedió en uno de mis proyectos que se suponía que algunos de los modelos no podían ser escritos por nadie más que el sistema. Antes de que empiece a poner los ojos en blanco, considere que el registro o el mantenimiento de una pista de auditoría es un buen ejemplo del mundo real de este tipo de modelos de «sólo lectura». El problema al que me enfrentaba era que, como se suponía que debía crear repositorios que implementaran todos la interfaz IRepository, digamos el LoggingRepository, al menos dos de los métodos de la interfaz, update() y delete( ) no me servían de nada.

Sí, una solución rápida sería implementarlos de todas formas y dejarlos en blanco o lanzar una excepción, pero si depender de esas soluciones de cinta aislante me pareciera bien, ¡no estaría siguiendo el patrón de repositorios en primer lugar!

¡Ayuda! Estoy atascada :'(

¿Significa eso que todo es culpa del patrón Repositorio?

No, ¡en absoluto!

De hecho, un Repositorio es un patrón bien conocido y aceptado que aporta consistencia, flexibilidad y abstracción a sus patrones de acceso a datos. El problema es que la interfaz que hemos creado — o debería decir la interfaz que se ha popularizado en prácticamente todos los tutoriales — es demasiado amplia.

A veces esta idea se expresa diciendo que la interfaz es «gorda», pero significa lo mismo — la interfaz hace demasiadas suposiciones, y por lo tanto añade métodos que son inútiles para algunas clases pero aún así esas clases se ven obligadas a implementarlos, dando como resultado un código frágil y confuso. Nuestro ejemplo podría haber sido un poco más sencillo, pero imagine el lío que se puede crear cuando varias clases han implementado métodos que no querían, o los que querían pero faltaban en la interfaz.

La solución es sencilla, y es también el nombre del principio que estamos discutiendo: Segregación de interfaces.

La cuestión es que no debemos crear nuestras interfaces a ciegas. Y tampoco deberíamos hacer suposiciones, por muy experimentados o inteligentes que nos creamos. En su lugar, deberíamos crear varias interfaces más pequeñas y especializadas, dejando que las clases implementen las que sean necesarias y dejando fuera las que no lo sean.

En el ejemplo que comentamos, podría haber creado dos interfaces en lugar de una: IReadOnlyRespository (que contiene las funciones getOne() y getAll()), e IWriteModifyRepository (que contiene el resto de las funciones). Para los repositorios normales, diría entonces class UserRepository implements IReadOnlyRepository, IWriteModifyRepository { . .. }. (Nota al margen: Aún podrían surgir casos especiales, y eso está bien porque ningún diseño es perfecto. Puede que incluso quiera crear una interfaz separada para cada método, y eso también estará bien, suponiendo que las necesidades de su proyecto sean tan granulares)

Sí, ahora hay más interfaces, y algunos podrían decir que hay demasiado que recordar o que la declaración de clase ahora es demasiado larga (o tiene un aspecto feo), etc., pero fíjese en lo que hemos ganado: interfaces especializadas, de pequeño tamaño y autocontenidas que pueden combinarse según sea necesario y que no se estorbarán entre sí. Mientras se gane la vida escribiendo software, recuerde que es el ideal al que todos aspiran.

«D» de inversión de dependencia

Si ha leído las partes anteriores de este artículo, posiblemente sienta que entiende lo que este principio trata de decir. Y tendría razón, en el sentido de que este principio es más o menos una repetición de lo que hemos discutido hasta ahora. Su definición formal no asusta demasiado, así que echémosle un vistazo: Los módulos de alto nivel no deberían depender de los módulos de bajo nivel; ambos deberían depender de las abstracciones.

Sí, tiene sentido. Si tengo una clase de alto nivel (alto nivel en el sentido de que utiliza otras clases más pequeñas y especializadas para realizar algo y luego tomar algunas decisiones), no deberíamos tener esa clase de alto nivel dependiendo de una clase particular de bajo nivel para algún tipo de trabajo. Más bien, deberían codificarse para depender de abstracciones (como clases base, interfaces, etc.).

¿Por qué?

Ya vimos un gran ejemplo de ello en la parte anterior de este artículo. Si utilizara un servicio de generación de PDF y su código estuviera plagado de nuevas clases ABCService(), el día en que la empresa decidiera utilizar algún otro servicio sería el día recordado para siempre… ¡por todas las razones equivocadas! Más bien, deberíamos utilizar una forma general de esta dependencia (crear una interfaz para los servicios PDF, es decir), y dejar que otra cosa se encargue de su instanciación y nos la pase (en Laravel, vimos cómo el Contenedor de Servicios nos ayudaba a hacerlo).

En definitiva, nuestra clase de alto nivel, que antes controlaba la creación de instancias de clase de nivel inferior, ahora tiene que recurrir a otra cosa. Las tornas han cambiado, y por eso llamamos a esto una inversión de dependencias.

Si busca un ejemplo práctico, vuelva a la parte de este artículo en la que hablamos de cómo rescatar nuestro código de tener que depender exclusivamente de la clase MilkyWay PDF.

. . .

Adivine qué, ¡ya está! Lo sé, lo sé, ha sido una lectura bastante larga y dura, y quiero disculparme por ello. Pero mi corazón está con el desarrollador medio que ha estado haciendo las cosas intuitivamente (o como se las enseñaron) y no puede entender ni pies ni cabeza los principios SOLID. Y he hecho todo lo posible por mantener los ejemplos lo más cerca posible de la jornada laboral de un desarrollador de Laravel; después de todo, ¿de qué nos sirven los ejemplos que contienen clases Vehicle y Car — o incluso genéricos avanzados, reflexión, etc. — cuando ninguno de nosotros va a ser autor de bibliotecas.

Si le ha resultado útil este artículo, deje un comentario. Esto confirmará mi idea de que los desarrolladores se esfuerzan realmente por dar sentido a estos conceptos «avanzados», y me motivará a escribir sobre otros temas de este tipo. ¡Hasta luego! 🙂