Inversión de dependencia (DIP) / Sudo Null IT News

La inversión de dependencia es una estrategia que consiste en depender de interfaces o funciones y clases abstractas en lugar de funciones y clases concretas. En pocas palabras, cuando los componentes de nuestro sistema tienen dependencias, no queremos inyectar directamente la dependencia de un componente en otro. En cambio, debemos utilizar algún nivel de abstracción entre ellos.

Ésta es una definición muy difícil. Donde, “En pocas palabras”, sólo confunde más de lo que explica, aunque todo transmite correctamente. Este principio es quizás el más difícil de explicar, aunque su esencia es obvia.

Comenzaré mi explicación con un ejemplo cotidiano. Para que funcione la lámpara de iluminación se necesita un cable y, por supuesto, electricidad. Es lógico integrar esta lámpara en una lámpara que cumplirá una doble función, protectora y decorativa. Qué hacer si la lámpara falla, obviamente quitar la pantalla, desenroscar la lámpara e insertar una nueva, etc., pero esto fue posible cuando aplicamos el principio de inversión de dependencia.

Los diseñadores dividieron la lámpara en partes; en programación se les llama módulos. Cada una de estas partes estaba equipada con interfaces. Se hizo una interfaz para la lámpara: un casquillo. Para la pantalla, la interfaz son los soportes, etc. Ahora es posible quitar la lámpara y reemplazarla por otra, si estas lámparas tienen interfaces compatibles. ¿Qué pasa si la lámpara no es plegable? Tendremos que destruirlo o tirarlo y comprar uno nuevo.

Lo mismo sucede en el software. Si al principio nos dividimos incorrectamente en módulos y no realizamos la inversión de dependencias, con los más mínimos cambios tendremos que reescribir una gran sección del código, y esto es bueno si escribimos este código nosotros mismos. Es más fácil de entender.

Además, todas esas lanzas verbales rotas en artículos y comentarios sobre el tema de la refactorización fueron rotas en vano. La división adecuada de módulos y la inversión de dependencias permiten reducir la refactorización a reemplazar un módulo por otro.

El siguiente concepto que es necesario discutir son las dependencias mismas. Qué depende de qué. Volvamos al ejemplo de la lámpara. La lámpara está diseñada para iluminar. Esta es la lógica empresarial. No importa cuál sea la fuente de luz, una antorcha, una vela o una lámpara eléctrica. La lógica empresarial de la lámpara no cambia.

Y esto es importante en sí mismo. La lógica empresarial de casi todos los programas se describió hace mucho tiempo; no creo que el proceso empresarial de negociación haya cambiado desde los tiempos de los antiguos sumerios, por mucho que el cliente insistiera en su singularidad.

Por tanto, la lógica empresarial es la base inmutable de cualquier aplicación. Luego viene la división en módulos, que se dividen en otros más pequeños, y así sucesivamente. Desafortunadamente, este artículo no trata sobre diseño, por lo que nos centraremos en el ejemplo de una lámpara.

La lámpara se divide en base y farol. Además, cada uno de estos módulos se divide en sus partes y así sucesivamente hasta que las partes no sean desmontables.

Es decir, el resultado es una estructura del programa en forma de árbol, donde los módulos más estables se componen de otros menos estables.

Este es el objetivo del principio de inversión de dependencia. Las partes más estables de un programa no pueden depender de otras menos estables. La lámpara no puede depender del tornillo.

Para ilustrar, tomemos un ejemplo muy simple y realicemos una inversión de dependencia en él.

enum Classifier { BREAD, BISCUITS, CROISSANTS };

class Product {
private:
    std::string name;          // Наименование товара
    Classifier category;       // Классификатор товара
    float weight;              // Вес нетто
    double price;              // Цена
public:
    Product(std::string name, Classifier category, float weight, double price):
        name(name), category(category), weight(weight), price(price) {}
    Classifier getClassifier() { return category; }
    float getWeight() { return weight; }
    double getPrice() { return price; }
};


int main() {
    std::list<Product> products{
        {"Хлеб", Classifier::BREAD, 0.9f, 100},
        {"Другой хлеб", Classifier::BREAD, 1.0f, 100},
        {"Печенье", Classifier::BISCUITS, 1.0f, 200}
    };

    Product findWhat{ "", Classifier::BREAD, 1.0f, 100 };

    auto result = std::ranges::find_if(products, (&findWhat)(Product prd){
        return findWhat.getClassifier() == prd.getClassifier()
            and findWhat.getWeight() == prd.getWeight()
            and findWhat.getPrice() == prd.getPrice();
        }
    );

    if (result != products.end()) {
        std::cout << "Test passed!!!" << std::endl;
    }
}

El ejemplo se divide en tres partes.

  • El método std::ranges::find_if es lógica empresarial.

  • std::list productos es un almacén de datos.

  • Búsqueda de productos¿Qué es una plantilla de búsqueda?

La esencia del programa es simple. En el almacén de datos, según la plantilla, necesitamos encontrar un producto. Condiciones de búsqueda, es decir, el algoritmo de búsqueda en sí está escrito en una expresión lambda.

diagrama de clases

diagrama de clases

Como puede verse en el código, la lógica empresarial se ha vuelto dependiente del producto. Es decir, si necesitamos cambiar un producto o las condiciones de búsqueda de un producto, necesitaremos cambiar la lógica de negocio (expresión lambda).

Puedes ir por otro camino. Mueva las condiciones a la propia clase de producto. Implementando las condiciones, a través de los operadores == o (). Pero este es un enfoque aún más sin salida. En primer lugar, incluso para nuestro ejemplo con un límite de tres campos, necesitaremos 8 variantes de condiciones iguales/no iguales para la clase de producto, y si hay más campos de búsqueda, tendrán diferentes expresiones lógicas.

enum Classifier { BREAD, BISCUITS, CROISSANTS };

class Product {
private:
    std::string name;          // Наименование товара
    Classifier category;       // Классификатор товара
    float weight;              // Вес нетто
    double price;              // Цена
public:
    Product(std::string name, Classifier category, float weight, double price):
        name(name), category(category), weight(weight), price(price) {}
    inline bool operator== (const Product &obj) const {
        return this->category == obj.category
            and this->weight == obj.weight
            and this->price == obj.price;
    }
    inline bool operator()(const Product& obj) const {
        return this->category == obj.category
            and this->weight == obj.weight
            and this->price == obj.price;
    }
};

El segundo inconveniente es que la clase de producto comienza a violar la regla S: el principio de responsabilidad única SÓLIDO. El producto es responsable del almacenamiento de datos y de las condiciones para su procesamiento.

Queda claro que las condiciones, llamémoslas algoritmos de procesamiento de datos, deben separarse tanto de los datos como de la lógica empresarial.

Diagrama de dependencia

Diagrama de dependencia

Ha mejorado un poco, ahora podemos cambiar las condiciones independientemente del producto, pero la lógica empresarial aún depende de las condiciones. En realidad, la lógica empresarial dependerá tanto de los datos como de los algoritmos.

class Product {...};

class Comparer {
public:
    bool operator()(Product obj) {...}
};

int main() {
    std::list<Product> list{ ... };
    Comparer compare;
    std::ranges::find_if(list, compare);
}

Para explicar el problema, tomemos un ejemplo de la vida real. Tienes un smartphone de una determinada marca, esta es tu lógica de negocio. Tiene entorno, carga, auriculares, estuche, etc. Los consideraremos algoritmos. Si sigues nuestro esquema, cuando la lógica empresarial depende de algoritmos, es lo mismo que si tuvieras que comprar un nuevo teléfono inteligente si tu cargador o tus auriculares fallaran.

Cambiemos la dirección de la dependencia, dejemos que los algoritmos no dependan de la lógica empresarial.

Diagrama de dependencia

Diagrama de dependencia

Para realizar la inversión es necesario reemplazar en la lógica de negocios todas las referencias a una clase con algoritmos con una interfaz que será la base para crear implementaciones específicas.

class IComparer {
public:
    virtual bool equal(const Product& first, const Product& second) const = 0;
};

Pero nuestra imitación de la lógica empresarial basada en el algoritmo std::ranges::find_if toma un predicado unario como parámetro y necesitamos dos valores para comparar. Como siempre, en casos complejos y no obvios, la respuesta hay que buscarla en los patrones de diseño. En este caso, el patrón “Diputado” (proxy) es el más adecuado.

class Comparison {
private:
    Product product;
    std::shared_ptr<IComparer> predicate;
public:
    Comparison(const Product& product, std::shared_ptr<IComparer> predicate) {
        this->product = product;
        this->predicate = predicate;
    }
    inline bool operator()(const Product& obj) const {
        return predicate->equal(this->product, obj);
    }
};

Como puede ver en el código, el objeto toma un valor para comparar y una referencia al algoritmo de comparación en forma de interfaz IComparer. Como resultado de tales transformaciones, tenemos un sustituto que imita un predicado unario, que puede sustituirse en nuestra lógica de negocios basada en std::ranges::find_if.

Ahora necesitas realizar una inversión de dependencia para trabajar con los datos. Aquí todo se hace exactamente igual. Se agrega una abstracción a partir de la cual se crea una clase con datos, y en los lugares donde se llama directamente a la clase con datos, se inserta un enlace a la abstracción.

Diagrama de dependencia

Diagrama de dependencia

El resultado es el siguiente código.

class Comparison {
private:
    std::shared_ptr<Product> product;
    std::shared_ptr<IComparer> predicate;
public:
    Comparison(const std::shared_ptr<Product> product, std::shared_ptr<IComparer> predicate) {
        this->product = product;
        this->predicate = predicate;
    }
    inline bool operator()(const std::shared_ptr<Product>& obj) const {
        return predicate->equal(*this->product, *obj);
    }
};

Se cumplen todas las condiciones, podemos ampliar fácilmente las capacidades de almacenamiento de datos y los algoritmos de procesamiento de datos sin cambiar la lógica empresarial.

enum Classifier { NONE, BREAD, BISCUITS, CROISSANTS };

class Product {
private:
    std::string name;          // Наименование товара
    Classifier category;       // Классификатор товара
    float weight;              // Вес нетто
    double price;              // Цена
public:
    Product() : category(Classifier::NONE), weight(0), price(0) {}
    Product(std::string name, Classifier category, float weight, double price) :
        name(name), category(category), weight(weight), price(price) {}
    Classifier getCategory() const { return category; }
    float getWeight() const { return weight; }
    double getPrice() const { return price; }
};

class Manufacturer {};

class SpecificProduct : public Product {
private:
    Manufacturer manufactured; // Производитель
public:
    SpecificProduct(std::string name, Classifier category, float weight, double price, Manufacturer manufactured) :
        Product(name, category, weight, price) {
        this->manufactured = manufactured;
    }
    Manufacturer getManufactured() const { return manufactured; }
};

class IComparer {
public:
    virtual bool equal(const Product& first, const Product& second) const = 0;
};

class ClearMatch : public IComparer {
public:
    virtual bool equal(const Product& first, const Product& second) const {
        return first.getCategory() == second.getCategory()
            and first.getWeight() == second.getWeight()
            and first.getPrice() == second.getPrice();
    }
};

class FuzzyMatch : public IComparer {
public:
    virtual bool equal(const Product& first, const Product& second) const {
        return first.getCategory() == second.getCategory()
            and first.getWeight() == second.getWeight()
            and (first.getPrice() < 120
                or first.getPrice() > 80);
    }
};

class Comparison {
private:
    std::shared_ptr<Product> product;
    std::shared_ptr<IComparer> predicate;
public:
    Comparison(const std::shared_ptr<Product> product, std::shared_ptr<IComparer> predicate) {
        this->product = product;
        this->predicate = predicate;
    }
    inline bool operator()(const std::shared_ptr<Product>& obj) const {
        return predicate->equal(*this->product, *obj);
    }
};

int main() {
    std::list<std::shared_ptr<Product>> products{
        std::make_shared<SpecificProduct>("Хлеб", Classifier::BREAD, 0.9f, 100, Manufacturer()),
        std::make_shared<SpecificProduct>("Другой хлеб", Classifier::BREAD, 1.0f, 100, Manufacturer()),
        std::make_shared<SpecificProduct>("Печенье", Classifier::BISCUITS, 1.0f, 200, Manufacturer())
    };

    std::shared_ptr<Product> findWhat = std::make_shared<SpecificProduct>("", Classifier::BREAD, 1.0f, 100, Manufacturer());
    Comparison compare(findWhat, std::make_shared<ClearMatch>());

    auto result = std::ranges::find_if(products, compare);

    if (result != products.end()) {
        std::cout << "Test passed!!!" << std::endl;
    }
}

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *