Open/Closed Principle: Method template, Strategy y otras alternativas

La O de SOLID, el Open/Closed Principle, es para mi uno de los principios más importantes a la hora de diseñar software y, a veces, uno de los más difíciles de llevar a cabo.

El Open/Closed Principle, que podríamos traducir como Princicio de Abierto/Cerrado, dice lo siguiente:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

Las entidades del software (clases, módulos, funciones, etc.) deben estar abiertas para la extensión pero cerrados para la modificación

La idea es bastante sencilla. En OOP, normalmente se suele aplicar a clases y lo que quiere decir es que, una vez que hemos terminado de implementar una clase, la clase debe quedar cerrada para la modificación (no se debe tocar excepto para corregir errores), pero abierta para la extensión (debe ser posible añadirle nueva funcionalidad o modificar la ya existente).

Al aplicar este principio conseguimos una gran estabilidad en la aplicación. Puesto que evitamos tocar lo que funciona (la clase que tenemos implementada), es más difícil que al evolucionar la aplicación se rompa lo que ya teníamos. Además, al tener diseñadas desde un principio nuestras clases para permitirnos cambiar su comportamiento sin necesidad de tocar su código, muchas veces el añadir nueva funcionalidad es más sencillo, porque basta con implementar pequeñas partes de la aplicación y utilizarlas con la parte ya existente.

En un lenguaje de estático, como C#, hay varias técnicas que nos pueden ayudar a implementar este principio. Veamos algunas de ellas.

Template method (método plantilla)

El patrón template method consiste en definir la estructura de un proceso en una clase base y permitir redefinir uno o varios métodos (los métodos plantilla) en las clases derivadas para personalizar el comportamiento del proceso.

Un ejemplo rápido sería algo así:

public abstract class OrderProcessorBase
{
    public void Process(IEnumerable<Order> orders)
    {
        foreach (var order in orders)
        {
            // TODO: procesar de alguna forma order
            NotifiyOrderProcessed(order);
        }
    }
    protected abstract void NotifyOrderProcessed(Order order);
}

public class ConsoleOrderProcessor : OrderProcessorBase
{
   protected override void NotifyOrderProcessed(Order order)
   {
       Console.Out.WriteLine("Processed Order: {0}", order.Number);
   }
}

En este caso, el algoritmo encapsulado en OrderProcessorBase estaría cerrado para modificación y no deberíamos tocarlo. Sin embargo, existen puntos de extensión para cambiar la forma en que se notifica que Order ha sido procesado. Para ello basta con crear clases derivadas que definan el método NotifiyOrderProcessed.

Esto puede ser inconveniente si tenemos muchas variaciones en el comportamiento que se usan cada una en un único punto de la aplicación, porque nos llevará a definir muchas clases derivadas que sólo tendrán un uso. Aquí se echan en falta las clases anónimas de Java.

Esta técnica la he utilizado en innumerables ocasiones, pero debo reconocer que, con el paso del tiempo, cada vez me gusta menos y prefiero usar alternativas que no requieran herencia.

Patrón Estrategia

El patrón estrategia podríamos considerarlo como una vuelta de tuerca al template method que veíamos antes. Si en el template method usábamos herencia para conseguir aplicar el OCP, en el patrón estrategia usaremos composición. La idea es similar a la de antes, definimos en una clase la estructura del proceso (prefiero no llamarlo algoritmo para que esto suene menos teórico) y hacemos que partes de ese proceso queden delegadas a métodos de otra clase.

El ejemplo anterior quería así:

public interface IOrderProcessedNotifier
{
    void Notify(Order order);
}

public class OrderProcessor
{
    public void Process(IEnumerable<Order> orders, IOrderProcessedNotifier notifier)
    {
        foreach (var order in orders)
        {
            // TODO: procesar de alguna forma order
            notifier.Notifiy(order);
        }
    }
}

public class ConsoleOrderProcessorNotifier : IOrderProcessorNotifier
{
   public void void Notify(Order order)
   {
       Console.Out.WriteLine("Processed Order: {0}", order.Number);
   }
}

// Al utilizarlo
var processor = new OrderProcessor();
processor.Process(orders, new ConsoleOrderNotifier());

En este caso, la clase OrderProcessor sigue definiendo la estructura del proceso, pero está abierta para la extensión implementando distintas estrategias de notificación, es decir, creando nuevas implementaciones de IOrderProcessorNotifier.

Al igual que en el caso anterior, si tenemos muchas variaciones de comportamiento que se usan cada una en un único punto de la aplicación, acabaremos con muchas implementaciones de IOrderProcessorNotifier que sólo se usan una vez.

Aunque en este caso estamos pasando la estrategia en el método que la necesita, también podríamos inyectarla en el constructor de OrderProcessor, lo que nos permite una gran flexibilidad si estamos usando un contenedor de inversión de control para hacer inyección de dependencias.

Personalmente me gusta más esta alternativa que el template method, sobre todo porque no requiere herencia y evitar la herencia suele llevar a diseños más flexibles. Además el hecho de que sea más cómodo de utilizar con contenedores de inversión de control lo hace aún más atractivo para mi.

Inyectar Funcs o Actions

Aunque era posible hacerlo con delegates, la inclusión en C# 3 de las expresiones lambda, facilita mucho usar otra técnica para conseguir aplicar el OCP.

Esta alternativa se parece mucho al patrón estrategia, pero en lugar de definir un interface con los métodos a los que se delega parte del proceso, se utilizan directamente Funcs o Actions con la signatura de esos métodos. De esta forma no es necesario definir nuevas implementaciones del interface, sino que se pueden crear esas funciones “al vuelo”.

Veamos cómo sería el ejemplo anterior con esta técnica:

public class OrderProcessor
{
    public void Process(IEnumerable<Order> orders, Action<Order>notifyOrderProcessed)
    {
        foreach (var order in orders)
        {
            // TODO: procesar de alguna forma order
            notifyOrderProcessed(order);
        }
    }
}

// Al utilizarlo
var processor = new OrderProcessor();
processor.Process(orders, 
                  order => Console.Out.WriteLine("Processed Order: {0}", order.Number));

La principal ventaja de este método es que no es necesario definir nuevos interfaces cada vez que queremos variar el comportamiento. A cambio, es un poco menos claro que el patrón estrategia porque la única forma que tenemos de saber qué representa cada acción es el nombre del parámetro en el método (o en el contructor) de la clase que la utiliza. Esto se puede evitar en parte utilizando delegates tipados:

public delegate void NotifyOrderProcessed(Order order);

public class OrderProcessor
{
    public void Process(IEnumerable<Order> orders, NotifyOrderProcessed notifyOrderProcessed)
    {
        ...
    }
}

Esta técnica funciona bastante bien cuando sólo hay que inyectar un método dentro del proceso que estamos haciendo, tal y como sucede en el ejemplo anterior. Si hace falta definir varios pasos del proceso y, por tanto, se necesitan varios métodos, suele ser más claro utilizar directamente un patrón estrategia.

Aun así, existe una alternativa que explicaré próximamente alternativa a method template y strategy que, a mi parecer, permite alcanzar un punto de equilibrio para no acabar con muchas clases que sólo se usan una vez (como puede ocurrir con template method o estrategia) y a la vez mantener una estructura clara de código sin tener que pasar varios Action<T>/Func<T, R> cuya responsabilidad pueda resultar confusa.

Conclusiones

Lo que hemos visto en este post son 3 alternativas que nos pueden permitir respetar el OCP en C#. Como decía al principio del post, aplicar correctamente este principio no siempre es sencillo, sobre todo porque es bastante difícil detectar a priori qué partes de la aplicación son las que más van a cambiar y, por tanto, aquellas en las que debemos hacer un mayor esfuerzo para plantear diseños como los que he explicado.

En general, lo mejor suele ser esperar hasta que se presenta la necesidad (YAGNI) y, en ese momento, refactorizar hacia alguna de estas alternativas, pero siempre es bueno tenerlas presentes porque pueden ser una herramienta muy valiosa.

3 comentarios en “Open/Closed Principle: Method template, Strategy y otras alternativas

  1. Pingback: Estrategias dinámicas en C# « Koalite's blog

  2. Pingback: AOP con Castle Windsor: IInterceptor « Koalite's blog

  3. Pingback: Object-oriented design – ¿Qué estrategias usas para aplicar el Principio Open/Closed? | Code 2 Read

Comentarios cerrados.