Back-end9 minute read

Principio de Responsabilidad Única: Una Receta para el Gran Código

Un código mantenible es algo que todos deseamos y no hay escasez de principios de codificación que lo prometan. No siempre es evidente lo tremendamente útil que pueden ser estos principios durante las primeras etapas del desarrollo. Sin embargo, el esfuerzo realizado para garantizar estas cualidades sin duda vale la pena, a medida que el proyecto crece y el desarrollo continúa. En este artículo, el ingeniero de Toptal Adel Fayzrakhmanov, analiza cómo el principio de responsabilidad única es uno de los aspectos más importantes en la escritura de un buen código mantenible.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Un código mantenible es algo que todos deseamos y no hay escasez de principios de codificación que lo prometan. No siempre es evidente lo tremendamente útil que pueden ser estos principios durante las primeras etapas del desarrollo. Sin embargo, el esfuerzo realizado para garantizar estas cualidades sin duda vale la pena, a medida que el proyecto crece y el desarrollo continúa. En este artículo, el ingeniero de Toptal Adel Fayzrakhmanov, analiza cómo el principio de responsabilidad única es uno de los aspectos más importantes en la escritura de un buen código mantenible.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Adel Fayzrakhmanov
Verified Expert in Engineering

Adel (MCE) has 15+ years in software development, focused on web technology and quality architecture. PHP and .NET are his forté.

Read More

PREVIOUSLY AT

Cornell University
Share

Independientemente de lo que consideramos un gran código, siempre requiere una calidad simple: el código debe ser mantenible. La sangría adecuada, nombres de variables limpios, cobertura de prueba del 100%, entre otros, solo puede funcionar hasta cierto punto. Cualquier código que no se puede mantener y no pueda adaptarse con relativa facilidad a los requisitos cambiantes es un código que sólo espera ser obsoleto. Es posible que no tengamos que escribir un gran código cuando estamos tratando de construir un prototipo, una prueba de concepto o un producto mínimo viable, pero en todos los demás casos siempre debemos escribir código que sea mantenible. Esto es algo que debe ser considerado como una calidad fundamental de la ingeniería de software y diseño.

Principio de Responsabilidad Única: Una Receta para el Gran Código

En este artículo voy a discutir cómo el principio de responsabilidad única y algunas técnicas que giran en torno a él puede darle a tu código esta calidad. Escribir un gran código es un arte, pero algunos principios siempre pueden ayudar a dar a tu trabajo de desarrollo la dirección que necesita para dirigirse hacia la producción de software fuerte y fácil de mantener.

El Modelo lo es Todo

Casi todos los libros sobre algún nuevo framework MVC (MVP, MVVM u otro M**) están llenos de ejemplos de código incorrecto. Estos ejemplos intentan mostrar lo que el marco tiene que ofrecer. Pero también terminan proporcionando un mal consejo para los principiantes. Ejemplos como “digamos que tenemos este ORM X para nuestros modelos, el motor de plantillas Y para nuestras vistas y así tendremos controladores para manejarlo todo” no logran nada más que controladores gigantescos. Sin embargo, en defensa de estos libros, los ejemplos están destinados a demostrar la facilidad con la que puedes utilizar su framework. No están destinados a enseñar diseño de software. Pero los lectores que siguen estos ejemplos se dan cuenta, sólo después de años, de lo contraproducente que es tener trozos monolíticos de código en su proyecto.

Los modelos son el corazón de tu aplicación. Si tienes modelos separados del resto de la lógica de la aplicación, el mantenimiento será mucho más fácil, sin importar que tan complicada se pueda volver la aplicación. Incluso para aplicaciones complicadas, la buena implementación del modelo puede resultar en un código extremadamente expresivo. Y para lograrlo debes comenzar por asegurarte de que tus modelos hagan sólo lo que están destinados a hacer y no se preocupen por lo que la aplicación construida a su alrededor hace. Además, no se ocupa de cuál es la capa de almacenamiento de datos subyacente: ¿tu aplicación depende de una base de datos SQL o almacena todo en archivos de texto?

A medida que continuamos este artículo, te darás cuenta que un gran código se trata de la separación de preocupación.

Principio de Responsabilidad Única

Probablemente has escuchado hablar de los principios SOLID: responsabilidad única, abierto-cerrado, sustitución liskov, la segregación de interfaz y la inversión de dependencia. La primera letra, S, representa el Principio de Responsabilidad Única (SRP) y su importancia no puede ser exagerada. Incluso diría que es una condición necesaria e importante para un buen código. De hecho, en cualquier código que está mal escrito, siempre se puede encontrar una clase que tiene más de una responsabilidad - form1.cs o index.php, el contener unas miles de líneas de código no es algo extraño y todos nosotros probablemente lo hemos visto o hecho.

Echemos un vistazo a un ejemplo en C# (ASP.NET MVC y Entity framework). Incluso si no eres un desarrollador C#, con un poco de experiencia OOP podrás avanzar con facilidad.

public class OrderController
{
...

   	public ActionResult CreateForm()
   	{
       	/*
       	* View data preparations
       	*/

       	return View();
   	}

   	[HttpPost]
   	public ActionResult Create(OrderCreateRequest request)
   	{
       	if (!ModelState.IsValid)
       	{
           	/*
            	* View data preparations
           	*/

           	return View();
       	}

       	using (var context = new DataContext())
       	{
                  var order = new Order();
                   // Create order from request
                   context.Orders.Add(order);

                   // Reserve ordered goods
                   …(Huge logic here)...

                  context.SaveChanges();

                  //Send email with order details for customer
       	}

       	return RedirectToAction("Index");
   	}

... (many more methods like Create here)
}

Esta es una clase OrderController usual y se muestra su método Create. En controladores como este, veo a menudo casos en los que la propia clase Order se utiliza como un parámetro de petición. Pero prefiero usar clases de solicitud especial. Por supuesto, una vez más, ¡SRP!

Observa en el fragmento de código que se mostró arriba cómo el controlador sabe demasiado acerca de “hacer un pedido”, incluyendo, pero no limitado a, almacenar el objeto Order, enviar correos electrónicos, etc. Esto es simplemente demasiado trabajo para una sola clase. Por cada pequeño cambio, el desarrollador necesita cambiar el código de todo el controlador. Y sólo en caso de que otro controlador también tenga que crear órdenes, la mayoría de las veces los desarrolladores recurren a copiar y pegar el código. Los controladores sólo deben controlar el proceso global y no albergar cada trozo de la lógica del proceso.

¡Pero hoy es el día en que dejamos de escribir estos controladores gigantescos!

Primero extraigamos toda la lógica de negocios del controlador y vamos a moverla a una clase OrderService:

public class OrderService
{
   public void Create(OrderCreateRequest request)
   {
       // all actions for order creating here
   }
}

public class OrderController
{
   public OrderController()
   {
       this.service = new OrderService();
   }
   
   [HttpPost]
   public ActionResult Create(OrderCreateRequest request)
   {
       if (!ModelState.IsValid)
       {
           /*
            * View data preparations
           */

           return View();
       }

       this.service.Create(request);

       return RedirectToAction("Index");
  }

Con esto hecho, el controlador ahora sólo hace lo que debe hacer: controlar el proceso. Sólo conoce las vistas, las clases OrderService y OrderRequest - el conjunto mínimo de información necesaria para que realice su trabajo, lo cual es la gestión de solicitudes y el envío de respuestas.

Así, en pocas ocasiones se cambiará el código del controlador. Otros componente como vistas, objetos de solicitud y servicios, pueden cambiar ya que están vinculados a los requisitos empresariales, pero no a los controladores.

De esto se trata SRP, y hay muchas técnicas para escribir código que cumple con este principio. Un ejemplo de esto es la inyección de dependencia (algo que también es útil para escribir código comprobable).

Inyección de Dependencia

Es difícil imaginar un proyecto grande basado en el Principio de Responsabilidad Única sin Inyección de Dependencia. Echemos un vistazo nuevamente a nuestra clase OrderService:

public class OrderService
{
  public void Create(...)
  {
      // Creating the order(and let’s forget about reserving here, it’s not important for following examples)
      
      // Sending an email to client with order details
      var smtp = new SMTP();
      // Setting smtp.Host, UserName, Password and other parameters
      smtp.Send();
  }
}

Este código funciona pero no es muy ideal. Para entender cómo funciona el método crear clase OrderService, se ven forzados a comprender las complejidades de SMTP. Y, de nuevo, copiar y pegar es la única manera de replicar este uso de SMTP donde sea necesario. Pero con un poco de refactorización eso puede cambiar:

public class OrderService
{
   private SmtpMailer mailer;
   public OrderService()
   {
       this.mailer = new SmtpMailer();
   }

   public void Create(...)
   {
       // Creating the order
       
       // Sending an email to client with order details
       this.mailer.Send(...);
   }
}

public class SmtpMailer
{
   public void Send(string to, string subject, string body)
   {
       // SMTP stuff will be only here
   }
}

¡Mucho mejor! Pero la clase OrderService todavía sabe mucho sobre el envío de correos electrónicos. Necesita precisamente la clase SmtpMailer para enviar un correo electrónico. ¿Qué pasa si queremos cambiarlo mas adelante? ¿Qué ocurre si queremos imprimir el contenido del correo electrónico que se envía a un archivo de registro especial en lugar de enviarlos en nuestro entorno de desarrollo? ¿Qué pasa si queremos probar nuestra clase OrderService? Continuemos con la refactorización creando una interfaz IMailer:

public interface IMailer
{
   void Send(string to, string subject, string body);
}

SmtpMailer implementará esta interfaz. Además, nuestra aplicación utilizará un contenedor IoC y podemos configurarlo para que IMailer sea implementado por la clase SmtpMailer. OrderService puede cambiarse de la siguiente manera:

public sealed class OrderService: IOrderService
{
   private IOrderRepository repository;
   private IMailer mailer;
   public OrderService(IOrderRepository repository, IMailer mailer)
   {
       this.repository = repository;
       this.mailer = mailer;
   }

   public void Create(...)
   {
       var order = new Order();
       // fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.)
       this.repository.Save(order);

       this.mailer.Send(<orders user email>, <subject>, <body with order details>);
   }
}

¡Ahora estamos avanzando! Tomé esta oportunidad para hacer otro cambio. OrderService ahora se basa en la interfaz IOrderRepository para interactuar con el componente que almacena todas nuestras órdenes. Ya no se preocupa por cómo se implementa esa interfaz ni por qué tecnología de almacenamiento la está alimentando. Ahora la clase OrderService sólo tiene código que se ocupa de la lógica empresarial de los pedidos.

De esta manera, si un probador encontrara algo que no actúa correctamente en el envío de correos electrónicos, el desarrollador sabe exactamente dónde buscar: clase SmtpMailer. Si algo andaba mal con los descuentos, el desarrollador, una vez mas, sabe dónde buscar: el código de clase OrderService (o en caso de que hayas aceptado SRP de corazón, entonces puede ser DiscountService).

Arquitectura Impulsada por Eventos

Sin embargo, todavía no me gusta el método OrderService.Create:

   public void Create(...)
   {
       var order = new Order();
       ...
       this.repository.Save(order);

       this.mailer.Send(<orders user email>, <subject>, <body with order details>);
   }

Enviar un correo electrónico, en realidad, no es una parte del flujo de creación del pedido principal. Incluso si la aplicación no logra enviar el correo electrónico, la orden sigue creándose correctamente. Además imagina una situación en la que tienes que agregar una nueva opción en el área de configuración de usuario que les permite optar por no recibir un correo electrónico después de realizar un pedido con éxito. Para incorporar esto en nuestra clase OrderService, tendremos que introducir una dependencia: IUserParametersService. Agrega la localización y así ya tienes una nueva dependencia, ITranslator (para producir mensajes de correo electrónico correctos en el idioma que elija el usuario). Varias de estas acciones son innecesarias, especialmente la idea de agregar tantas dependencias y terminar con un constructor que no encaja en la pantalla. Encontré un gran ejemplo de esto en la base de código de Magento (un CMS de comercio electrónico popular escrito en PHP) en una clase que tiene 32 dependencias!

A veces es difícil imaginar cómo separar esta lógica y la clase de Magento es probablemente una víctima de uno de esos casos. Por eso me gusta la manera “impulsada por eventos”:

namespace <base namespace>.Events
{
[Serializable]
public class OrderCreated
{
   private readonly Order order;

   public OrderCreated(Order order)
   {
       this.order = order;
   }

   public Order GetOrder()
   {
       return this.order;
   }
}
}

Cada vez que se crea una orden, en lugar de enviar un correo electrónico directamente desde la clase OrderService, se crea la clase de eventos especiales OrderCreated y se genera un evento. En algún lugar de los controladores de eventos de aplicación se configurará. Uno de ellos enviará un correo electrónico al cliente.

namespace <base namespace>.EventHandlers
{
public class OrderCreatedEmailSender : IEventHandler<OrderCreated>
{
   public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator)
   {
       // this class depend on all stuff which it need to send an email.
   }

   public void Handle(OrderCreated event)
   {
       this.mailer.Send(...);
   }
}
}

La clase OrderCreated está marcada como Serializable a propósito. Podemos gestionar este evento de forma inmediata o almacenarlo serializado en una fila (Redis, ActiveMQ o cualquier otra cosa) y procesarlo en un proceso/hilado separado al que maneja las solicitudes web. En este artículo, el autor explica en detalle qué es la arquitectura impulsada por eventos (No prestes atención a la lógica de negocios dentro de OrderController).

Algunos pueden argumentar que ahora es difícil entender lo que sucede al crear el pedido. Pero eso no es nada cierto. Si te sientes así, simplemente aprovecha la funcionalidad de tu IDE. Al encontrar todos los usos de la clase OrderCreated en el IDE, podemos ver todas las acciones asociadas con el evento.

Pero, ¿cuándo debo usar la inyección de dependencia y cuándo debo usar un enfoque basado en eventos? No siempre es fácil responder esta pregunta, pero una regla simple que puede ayudarte es usar la Inyección de Dependencia para todas tus actividades principales dentro de la aplicación y el enfoque orientado a eventos para todas las acciones secundarias. Por ejemplo, usa la Inyección de Dependencia con cosas como, crear un pedido dentro de la clase OrderService con IOrderRepository, al igual que delegar el envío de correos electrónicos, algo que no es una parte crucial del flujo de creación de orden principal, a algún controlador de eventos.

Conclusión

Comenzamos con un controlador muy importante, solo una clase, y terminamos con una colección de clases elaborada. Las ventajas de estos cambios son algo aparentes gracias a los ejemplos. Sin embargo, todavía hay muchas formas de mejorar estos ejemplos. Por ejemplo, el método OrderService.Create se puede mover a una clase propia: OrderCreator. Dado que la creación de orden es una unidad independiente de lógica de negocios, la cual sigue el Principio de Responsabilidad Única, es natural que tenga su propia clase con su propio set de dependencias. De la misma manera, la eliminación y cancelación de una orden pueden ser implementadas en sus propias clases.

Cuando escribí códigos altamente emparejados, algo similar al primer ejemplo en este artículo, cualquier cambio, aunque pequeño, en los requisitos podría llevar a muchos cambios en otras partes del código. SRP ayuda a los desarrolladores a escribir código que esta “desemparejado”, donde cada clase tiene su propio trabajo. Si alguna especificación de este trabajo cambia, el desarrollador hace cambios sólo a esa clase específica. Es menos probable que el cambio rompa toda la aplicación, ya que otras clases deberían estar haciendo su trabajo como antes, a menos que se hayan roto inicialmente.

Desarrollar código usando estas técnicas y seguir el Principio de Responsabilidad Única puede parecer una tarea escalofriante, pero los esfuerzos valdrán la pena a medida que el proyecto crece y continúa el desarrollo.

Hire a Toptal expert on this topic.
Hire Now
Adel Fayzrakhmanov

Adel Fayzrakhmanov

Verified Expert in Engineering

Kazan, Tatarstan, Russia

Member since October 19, 2015

About the author

Adel (MCE) has 15+ years in software development, focused on web technology and quality architecture. PHP and .NET are his forté.

Read More
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

PREVIOUSLY AT

Cornell University

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.