📘 Principio de Responsabilidad Única (SRP)

Estos apuntes estan basados en Typescript

  • Definición: Cada clase o módulo debe tener una sola razón para cambiar. Esto significa que una clase debe realizar solo una tarea o tener una responsabilidad.

  • Aplicación: En el contexto de TypeScript o cualquier otro lenguaje orientado a objetos, el SRP nos guía para modularizar el código de manera que cada clase maneje únicamente los aspectos de la aplicación estrechamente relacionados con su propósito.

🔄 Composición frente a Herencia

  • Definición:

    • Herencia: Extiende una clase base para crear una clase derivada que hereda propiedades y comportamientos, pero puede llevar a jerarquías complejas y código rígido.

    • Composición: Construye objetos más complejos combinando objetos simples y delegando tareas a ellos, lo que promueve una mayor flexibilidad y reutilización del código.

  • Preferencia: Se recomienda priorizar la composición sobre la herencia para evitar complejidades y dependencias innecesarias entre clases.

🧱 Ejemplo Práctico: Refactorización hacia Composición

  • Problema Identificado: Clases extendidas múltiples veces (Person ➡️ User ➡️ UserSettings), lo que complica el mantenimiento y la comprensión del código.

  • Solución:

    1. Separar responsabilidades: Define clases independientes con sus propias responsabilidades únicas (Person, User, UserSettings).

    2. Utilizar Composición: Crear una nueva clase ConfiguracionUsuario que contenga instancias de Persona, Usuario y Configuración como propiedades.

    3. Mejorar la legibilidad: Usar interfaces para definir propiedades esperadas en los constructores, facilitando la inicialización de clases compuestas.

🔹Antes:

// Persona.ts
class Person {
    constructor(public name: string, public gender: string, public birthdate: Date) {}
}

// User.ts - Extiende de Persona
class User extends Person {
    constructor(name: string, gender: string, birthdate: Date, public email: string, public role: string) {
        super(name, gender, birthdate);
    }
}

// UserSettings.ts - Extiende de Usuario, agregando configuraciones específicas de usuario
class UserSettings extends User {
    constructor(name: string, gender: string, birthdate: Date, email: string, role: string, public workingDirectory: string, public lastOpenFolder: string) {
        super(name, gender, birthdate, email, role);
    }
}


const userSettings = new UserSettings(
    "Juan Pérez",      // name from Person
    "M",               // gender from Person
    new Date('1990-01-01'), // birthdate from Person
    "juan@example.com",// email from User
    "admin",           // role from User
    "/users/juan",     // workingDirectory from UserSettings
    "/users/juan/docs" // lastOpenFolder from UserSettings
);

🔹Después:

Definición de Interfaces

Vamos a definir interfaces para Person, User, y Settings que especifiquen las propiedades esperadas para cada una. Además, agregaremos una para UserProfile que compone las tres anteriores.

// Person.ts
interface IPerson {
    name: string;
    gender: string;
    birthdate: Date;
}

// User.ts
interface IUser {
    email: string;
    role: string;
}

// Settings.ts
interface ISettings {
    workingDirectory: string;
    lastOpenFolder: string;
}

// UserProfile.ts
interface IUserProfile {
    person: IPerson;
    user: IUser;
    settings: ISettings;
}

Clase UserProfile con Composición

Luego ajustamos la clase UserProfile para que acepte un objeto que cumpla con la interfaz IUserProfile, lo que nos permite inicializar UserProfile con composición de una manera más estructurada y segura.

class UserProfile {
    person: Person;
    user: User;
    settings: Settings;

    constructor({ person, user, settings }: IUserProfile) {
        this.person = new Person(person.name, person.gender, person.birthdate);
        this.user = new User(user.email, user.role);
        this.settings = new Settings(settings.workingDirectory, settings.lastOpenFolder);
    }
}


const userProfile = new UserProfile({
    person: {
        name: "Juan Pérez",
        gender: "M",
        birthdate: new Date('1990-01-01')
    },
    user: {
        email: "juan@example.com",
        role: "admin"
    },
    settings: {
        workingDirectory: "/users/juan",
        lastOpenFolder: "/users/juan/docs"
    }
});

Podemos aplicar este principio en todos los lenguajes orientados a objetos, por ejemplo C#

🔹Antes:

public class Person {
    public string Name { get; set; }
    public string Gender { get; set; }
    public DateTime Birthdate { get; set; }

    public Person(string name, string gender, DateTime birthdate) {
        Name = name;
        Gender = gender;
        Birthdate = birthdate;
    }
}

public class User : Person {
    public string Email { get; set; }
    public string Role { get; set; }

    public User(string name, string gender, DateTime birthdate, string email, string role)
        : base(name, gender, birthdate) {
        Email = email;
        Role = role;
    }
}


public class UserSettings : User {
    public string WorkingDirectory { get; set; }
    public string LastOpenFolder { get; set; }

    public UserSettings(string name, string gender, DateTime birthdate, string email, string role, string workingDirectory, string lastOpenFolder)
        : base(name, gender, birthdate, email, role) {
        WorkingDirectory = workingDirectory;
        LastOpenFolder = lastOpenFolder;
    }
}

🔹Después:

public class Person {
    public string Name { get; set; }
    public string Gender { get; set; }
    public DateTime Birthdate { get; set; }

    public Person(string name, string gender, DateTime birthdate) {
        Name = name;
        Gender = gender;
        Birthdate = birthdate;
    }
}

public class User {
    public string Email { get; set; }
    public string Role { get; set; }

    public User(string email, string role) {
        Email = email;
        Role = role;
    }
}

public class Settings {
    public string WorkingDirectory { get; set; }
    public string LastOpenFolder { get; set; }

    public Settings(string workingDirectory, string lastOpenFolder) {
        WorkingDirectory = workingDirectory;
        LastOpenFolder = lastOpenFolder;
    }
}

public class UserProfile {
    public Person Person { get; set; }
    public User User { get; set; }
    public Settings Settings { get; set; }

    public UserProfile(Person person, User user, Settings settings) {
        Person = person;
        User = user;
        Settings = settings;
    }
}

❗Algunas razones para ser cauteloso con la herencia y preferir composición en muchos casos:

  1. Acoplamiento Fuerte: La herencia crea una dependencia directa entre la clase base y la subclase. Esto significa que cambiar la clase base puede tener efectos no deseados en las subclases, lo que puede ser problemático en sistemas grandes y complejos.

  2. Jerarquías Complejas: Una profunda jerarquía de herencia puede hacer que el código sea difícil de entender y mantener. Puede ser complicado seguir el flujo de la lógica a través de múltiples niveles de clases.

  3. Flexibilidad: La composición ofrece mayor flexibilidad en cómo se pueden combinar y reutilizar los objetos. Permite cambiar el comportamiento de los objetos en tiempo de ejecución agregando, cambiando o quitando componentes, lo cual no es posible con la herencia fija.

  4. Reutilización de Código: Aunque la herencia permite la reutilización de código, este beneficio también puede lograrse mediante la composición, a menudo de manera más clara y flexible.

  5. Polimorfismo: Aunque la herencia se usa para lograr polimorfismo, interfaces y clases abstractas en lenguajes como C# y Java también permiten polimorfismo sin forzar una relación de herencia directa.

Cuándo Usar Herencia

Usa herencia cuando necesites modelar una relación clara de "es un(a)" entre objetos, donde subclases específicas comparten y extienden comportamientos y atributos de una clase base común. La herencia es ideal para:

  • Aplicar polimorfismo, permitiendo que objetos de diferentes clases sean tratados como objetos de una clase base.

  • Centralizar código común, facilitando la mantenibilidad y reduciendo la duplicación.

  • Extender funcionalidades de clases base de manera jerárquica y organizada.

Cuándo Usar Composición

Opta por composición para construir clases más flexibles y mantenibles, combinando objetos de otras clases sin crear una dependencia jerárquica rígida. La composición es preferible cuando:

  • No existe una relación clara de "es un(a)" o la relación es más bien de "tiene un(a)".

  • Quieres cambiar o combinar comportamientos en tiempo de ejecución.

  • Buscas reducir el acoplamiento entre clases, facilitando el testeo y la extensión del código sin alterar las clases existentes.

  • Necesitas reutilizar funcionalidades de múltiples fuentes.

Última actualización