Principio de responsabilidad unica - FernandoCalmet/csharp-essential GitHub Wiki
Mientras desarrollamos un proyecto, nos esforzamos por escribir código mantenible y legible (además de la parte de trabajo 😀). Para lograr esto, cada clase debe hacer su propia tarea y hacerlo bien.
Sí, es muy importante que una clase no tenga más de una tarea. Si lo hace, nuestro código se vuelve más difícil de mantener, debido al hecho de que es responsable de la ejecución de varias tareas diferentes y, por lo tanto, es más probable que cambie en el futuro.
Esto es completamente opuesto a lo que establece el Principio de responsabilidad única (SRP).
El Principio de Responsabilidad Única establece que nuestras clases deben tener una sola razón para cambiar o, en otras palabras, debe tener una sola responsabilidad.
Creación del proyecto inicial
Vamos a empezar con una sencilla aplicación de consola.
Imagínese si tenemos una tarea para crear una función de informe de trabajo que, una vez creada, se puede guardar en un archivo y tal vez cargar en la nube o usar para algún otro propósito.
Así que vamos a comenzar con una clase de modelo simple:
public class WorkReportEntry
{
public string ProjectCode { get; set; }
public string ProjectName { get; set; }
public int SpentHours { get; set; }
}
El siguiente paso es crear una clase WorkReport que manejará todas las características requeridas para nuestro proyecto:
public class WorkReport
{
private readonly List<WorkReportEntry> _entries;
public WorkReport()
{
_entries = new List<WorkReportEntry>();
}
public void AddEntry(WorkReportEntry entry) => _entries.Add(entry);
public void RemoveEntryAt(int index) => _entries.RemoveAt(index);
public override string ToString() =>
string.Join(Environment.NewLine, _entries.Select(x => $"Code: {x.ProjectCode}, Name: {x.ProjectName}, Hours: {x.SpentHours}"));
}
En esta clase, hacemos un seguimiento de las entradas de nuestro informe de trabajo al agregarlas y eliminarlas de una lista. Además, solo estamos anulando el método ToString() para ajustarlo a nuestros requisitos.
Debido a que tenemos nuestra clase WorkReport, está bastante bien agregarle nuestras características adicionales, como guardar en un archivo:
public class WorkReport
{
private readonly List<WorkReportEntry> _entries;
public WorkReport()
{
_entries = new List<WorkReportEntry>();
}
public void AddEntry(WorkReportEntry entry) => _entries.Add(entry);
public void RemoveEntryAt(int index) => _entries.RemoveAt(index);
public void SaveToFile(string directoryPath, string fileName)
{
if(!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
File.WriteAllText(Path.Combine(directoryPath, fileName), ToString());
}
public override string ToString() =>
string.Join(Environment.NewLine, _entries.Select(x => $"Code: {x.ProjectCode}, Name: {x.ProjectName}, Hours: {x.SpentHours}"));
}
Problemas con este código
Podemos agregar aún más funciones en esta clase, como los métodos Load o UploadToCloud porque todos están relacionados con nuestro WorkReport, pero el hecho de que podamos no significa que tengamos que hacerlo.
En este momento, hay un problema con la clase WorkReport.
Tiene más de una responsabilidad.
Su trabajo no es solo realizar un seguimiento de las entradas de nuestro informe de trabajo, sino también guardar todo el informe de trabajo en un archivo. Esto significa que estamos violando el SRP y nuestra clase tiene más de una razón para cambiar en el futuro.
La primera razón para cambiar esta clase es si queremos modificar la forma en que hacemos un seguimiento de nuestras entradas. Pero si queremos guardar un archivo de una manera diferente, esa es una razón completamente nueva para cambiar nuestra clase. E imagine cómo se vería esta clase si le agregáramos funcionalidades adicionales. Tendríamos tantas partes de código no relacionadas en una sola clase.
Entonces, para evitar eso, refactoricemos el código.
Refactorización hacia SRP
Lo primero que tenemos que hacer es separar la parte de nuestro código que es diferente a los demás. En nuestro caso, ese es obviamente el método SaveToFile, por lo que lo moveremos a otra clase que sea más apropiada:
public class FileSaver
{
public void SaveToFile(string directoryPath, string fileName, WorkReport report)
{
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
File.WriteAllText(Path.Combine(directoryPath, fileName), report.ToString());
}
}
public class WorkReport
{
private readonly List<WorkReportEntry> _entries;
public WorkReport()
{
_entries = new List<WorkReportEntry>();
}
public void AddEntry(WorkReportEntry entry) => _entries.Add(entry);
public void RemoveEntryAt(int index) => _entries.RemoveAt(index);
public override string ToString() =>
string.Join(Environment.NewLine, _entries.Select(x => $"Code: {x.ProjectCode}, Name: {x.ProjectName}, Hours: {x.SpentHours}"));
}
En este caso, hemos separado nuestras responsabilidades en dos clases. La clase WorkReport ahora es responsable de realizar un seguimiento de las entradas del informe de trabajo y la clase FileSaver es responsable de guardar un archivo.
Habiendo hecho esto, hemos separado las preocupaciones de cada clase, haciéndolas también más legibles y fáciles de mantener. Como resultado, si queremos cambiar la forma en que guardamos un archivo, solo tenemos una razón para hacerlo y un lugar para hacerlo, que es la clase FileSaver.
Podemos comprobar que todo funciona como se supone que debe hacerlo:
class Program
{
static void Main(string[] args)
{
var report = new WorkReport();
report.AddEntry(new WorkReportEntry { ProjectCode = "123Ds", ProjectName = "Project1", SpentHours = 5 });
report.AddEntry(new WorkReportEntry { ProjectCode = "987Fc", ProjectName = "Project2", SpentHours = 3 });
Console.WriteLine(report.ToString());
var saver = new FileSaver();
saver.SaveToFile(@"Reports", "WorkReport.txt", report);
}
}
Hacer el código aún mejor
Si observamos nuestro método SaveToFile, vemos que hace su trabajo, que es guardar un informe de trabajo en un archivo, pero ¿puede hacerlo aún mejor? Este método está estrechamente relacionado con la clase WorkReport, pero ¿qué pasa si queremos crear una clase Scheduler que realice un seguimiento de sus tareas programadas? Todavía nos gustaría guardarlo en un archivo.
Bueno, en ese caso, vamos a crear algunos cambios en nuestro código:
public interface IEntryManager<T>
{
void AddEntry(T entry);
void RemoveEntryAt(int index);
}
El único cambio en la clase WorkReport es implementar esta interfaz:
public class WorkReport: IEntryManager<WorkReportEntry>
Finalmente, tenemos que cambiar la firma del método SaveToFile :
public void SaveToFile<T>(string directoryPath, string fileName, IEntryManager<T> workReport)
Después de estas modificaciones, vamos a tener el mismo resultado, pero ahora si tenemos una tarea para implementar Scheduler, será bastante simple implementar eso:
public class ScheduleTask
{
public int TaskId { get; set; }
public string Content { get; set; }
public DateTime ExecuteOn { get; set; }
}
public class Scheduler : IEntryManager<ScheduleTask>
{
private readonly List<ScheduleTask> _scheduleTasks;
public Scheduler()
{
_scheduleTasks = new List<ScheduleTask>();
}
public void AddEntry(ScheduleTask entry) => _scheduleTasks.Add(entry);
public void RemoveEntryAt(int index) => _scheduleTasks.RemoveAt(index);
public override string ToString() =>
string.Join(Environment.NewLine, _scheduleTasks.Select(x => $"Task with id: {x.TaskId} with content: {x.Content} is going to be executed on: {x.ExecuteOn}"));
}
class Program
{
static void Main(string[] args)
{
var report = new WorkReport();
report.AddEntry(new WorkReportEntry { ProjectCode = "123Ds", ProjectName = "Project1", SpentHours = 5 });
report.AddEntry(new WorkReportEntry { ProjectCode = "987Fc", ProjectName = "Project2", SpentHours = 3 });
var scheduler = new Scheduler();
scheduler.AddEntry(new ScheduleTask { TaskId = 1, Content = "Do something now.", ExecuteOn = DateTime.Now.AddDays(5) });
scheduler.AddEntry(new ScheduleTask { TaskId = 2, Content = "Don't forget to...", ExecuteOn = DateTime.Now.AddDays(2) });
Console.WriteLine(report.ToString());
Console.WriteLine(scheduler.ToString());
var saver = new FileSaver();
saver.SaveToFile(@"Reports", "WorkReport.txt", report);
saver.SaveToFile(@"Schedulers", "Schedule.txt", scheduler);
}
}
Después de ejecutar este código, tendremos nuestro archivo guardado en una ubicación requerida en un horario definido.
Vamos a dejarlo así. Ahora cada clase que tenemos es responsable de una cosa y solo de una cosa.
Beneficios del principio de responsabilidad única
-
Nuestro código ha mejorado de varias maneras al implementar SRP. El primero es que se ha vuelto menos complejo. Debido a que estamos tratando de realizar solo una tarea en nuestra clase, se han vuelto libres de desorden y fáciles de leer. A medida que reducimos la complejidad del código, nuestro código se vuelve legible y, por lo tanto, mantenible.
-
Como pudimos ver en nuestro ejemplo, si nuestra clase hace bien su trabajo, podemos reutilizar su lógica en un proyecto. Además, con dicho código, las pruebas también se vuelven más fáciles.
-
Cuando implementamos SRP en nuestro código, nuestros métodos se vuelven altamente relacionados (coherentes). Significa que se unen diferentes métodos para hacer una cosa y hacerlo bien.
-
Finalmente, nuestras clases son menos dependientes entre sí (desacopladas), lo cual es una de las cosas más importantes que se deben lograr mientras se trabaja en un proyecto.
Desventajas potenciales de SRP
No existe una regla estricta que establezca cuál es esa „razón única para cambiar“ en nuestra clase. Cada uno interpreta esto subjetivamente o más bien como siente que debería implementarse. Las reglas no son claras sobre dónde debemos trazar la línea, por lo que potencialmente podemos encontrar diferentes "formas correctas" de implementar la misma característica.
Pero aún así, la conclusión es que no importa lo que alguien piense sobre cuál es la razón para cambiar, todos debemos esforzarnos por escribir un código legible y mantenible, implementando así el Principio de Responsabilidad Única a nuestra manera.
Una de las posibles desventajas es que en proyectos que ya están escritos, es difícil implementar SRP. No decimos que no sea posible, solo que llevará más tiempo y requerirá más recursos también.
La implementación de SRP también conduce a la escritura de clases compactas con métodos pequeños. Y a primera vista, esto se ve genial. Pero tener una clase grande descompuesta en muchas clases pequeñas crea un riesgo organizacional. Si esas clases no están bien organizadas y agrupadas, en realidad podría aumentar la cantidad de trabajo necesario para cambiar un sistema y comprenderlo, lo cual es lo contrario de lo que queríamos lograr en primer lugar.
Conclusión
La implementación del principio de responsabilidad única debe estar siempre en nuestra mente al escribir código. Puede ser difícil escribir el código de acuerdo con SRP desde cero, pero puede escribir su código de forma iterativa y volver a las partes que necesitan atención más adelante. La refactorización es una práctica común y nadie escribe el código perfectamente de inmediato. Así que refactorice hacia el SRP más tarde si no está seguro de qué clase hace qué en ese momento. No solo lo ayudará a usted, sino también a los otros desarrolladores que necesitan mantener su código más adelante.