Ninject. Crear factorías III

¡Hola!
Después de dar un repaso general a las diferentes características de las factorías de Ninject (aquí y aquí), vamos a ver un caso que se acerca más al mundo real. Vamos a ver cómo crear una factoría para generar comandos basado en el nombre del tipo del comando concreto. ¿Suena un poco lioso? Es muy sencillo, lo vemos a continuación.

Nos vamos a imaginar que tenemos un par de servicios, IUserRepository y IConsoleWriter con sus correspondientes implementaciones y, a través de un comando UserWriterCommand, queremos recuperar todos los usuarios e imprimirlos por pantalla. Vamos por partes, primero echamos un vistazo a los dos servicios y sus implementaciones:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public interface IUserRepository
    {
        IList<string> GetUsers();
    }
 
    public class UserRepository : IUserRepository
    {
        public IList<string> GetUsers()
        {
            return new List<string>() { "Pepe", "Juan", "Luis" };
        }
    }
 
    public interface IConsoleWriter
    {
        void Write(string text);
    }
    public class ConsoleWriter : IConsoleWriter
    {
        public void Write(string text)
        {
            Console.WriteLine(text);
        }
    }

Como podemos ver, todo muy tonto sencillo. Ahora echamos un vistazo a la interfaz ICommand y el comando concreto para pintar los usuarios:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   public interface ICommand
    {
        void Execute();
    }
 
    public class UserWriteCommand : ICommand
    {
        private readonly IUserRepository userRepository;
        private readonly IConsoleWriter consoleWriter;
 
        public UserWriteCommand(IUserRepository userRepository, IConsoleWriter consoleWriter)
        {
            this.userRepository = userRepository;
            this.consoleWriter = consoleWriter;
        }
 
        public void Execute()
        {
            foreach (var user in this.userRepository.GetUsers())
            {
                this.consoleWriter.Write(user);
            }
        }
    }

Como decíamos al principio de la entrada, el objetivo será crear una factoría que, dado el nombre del tipo del comando concreto, nos lo genere. Es decir, que digamos a la factoría «Créame un comando del tipo UserWriteCommand». La factoría tendría este aspecto:

1
2
3
4
    public interface ICommandFactory
    {
        ICommand CreateCommand(string commandTypeName);
    }

Y esto sería todo lo que tendríamos que hacer en nuestro código de la aplicación… bueno, casi. Ahora el encargado de orquestar será nuestro contenedor Ninject. ¿Cuáles son los pasos a seguir?, vamos por partes.
En primer lugar, en nuestro contenedor tenemos que añadir los Bindings de los dos servicios:

1
2
3
4
5
6
7
8
    public class Composer : NinjectModule
    {
        public override void Load()
        {
            this.Bind<IConsoleWriter>().To<ConsoleWriter>();            this.Bind<IUserRepository>().To<UserRepository>();                  }
    }

¿Qué debemos hacer ahora? Pues crear nuestra factoría para la interfaz ICommandFactory. En este caso vamos a hacer uso de lo que vimos en la anterior entrada relacionado con StandardInstanceProvider. Vamos a sobreescribir el método GetName para especificar que se use el string que pasamos como argumento en el método CreateCommand de la factoría, para utilizarlo como constraint a la hora de resolver qué comando se debe crear. Vemos el código y lo explicamos a continuación:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    public class Composer : NinjectModule
    {
        public override void Load()
        {
            this.Bind<IConsoleWriter>().To<ConsoleWriter>();
            this.Bind<IUserRepository>().To<UserRepository>();    
            this.Bind<ICommandFactory>().ToFactory(() => new NameInstanceProvider());              }
    }
 
    public class NameInstanceProvider : StandardInstanceProvider    {        protected override string GetName(MethodInfo methodInfo, object[] arguments)        {            return (string)arguments[0];        }    }

Si nos fijamos en la línea 7, estamos utilizando un proveedor personalizado para generar las instancias de los comandos que solicitemos a la factoría, en concreto NameInstanceProvider. Este NameInstaceProvider hereda de StandardInstanceProvider y en el método GetName decimos que utilice el primer argumento que reciba. Esa lista de argumentos que recibe el método GetName, es la lista de argumentos que recibirá en la llamada al método CreateCommand de la factoría ICommandFactory, que en nuestro caso sólo contiene un parámetro que es commandTypeName. En el ejemplo que estamos siguiendo, commandTypeName sería el nombre del tipo del comando que queremos generar, que es «UserWriteCommand». ¿Y qué se va a hacer con ese texto «UserWriteCommand» que devuelve el método GetName? En la entrada anterior ya dijimos cómo funciona :), pero lo explico de nuevo rápidamente. Lo que va a hacer es buscar un Binding específico cuyo Name sea UserWriteCommand. Esto se especifica diciendo, this.Bind<ILoQueSea>().To<LoQueSeaEspecifico>().Named("TextoEspecífico")
, por lo que en nuestro caso, vamos a tener que crear un Binding específico para nuestro comando UserWriteCommand que diga que se resuelva con el nombre «UserWriteCommand». Esto nos lleva a completar la definición de nuestro contenedor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public class Composer : NinjectModule
    {
        public override void Load()
        {
            this.Bind<IConsoleWriter>().To<ConsoleWriter>();
            this.Bind<IUserRepository>().To<UserRepository>();
 
            this.Bind<ICommandFactory>().ToFactory(() => new NameInstanceProvider());
            this.Bind<ICommand>().To<UserWriteCommand>().Named("UserWriteCommand");        }
    }
 
 
    public class NameInstanceProvider : StandardInstanceProvider
    {
        protected override string GetName(MethodInfo methodInfo, object[] arguments)
        {
            return (string)arguments[0];
        }
    }

Con esto, cuando se vaya a buscar el Binding de ICommand con el Name (que lo hemos especificado en el método GetName de NameInstanceProvider) «UserWriteCommand», va a saber que tiene que resolverlo usando el Binding ICommand => UserWriteCommand, por lo que nos devolverá una instancia de UseWriteCommand (por supuesto, resolverá las dependencias IConsoleWriter y IUserRepository). Con esto ya estaría creada nuestra factoría, pero vamos a darle una pequeña vuelta más. Si nos fijamos, estamos harcodeando el texto «UserWriteCommand»; podríamos utilizar (depende que versión de C# utilices) nameof(UserWriteCommand), pero si tenemos un montón de comandos, tendremos que crear muchísimas líneas de Bindings. Vamos a automatizarlo un poco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public class Composer : NinjectModule
    {
        public override void Load()
        {
            this.Bind<IConsoleWriter>().To<ConsoleWriter>();
            this.Bind<IUserRepository>().To<UserRepository>();
 
            this.Bind<ICommandFactory>().ToFactory(() => new NameInstanceProvider());
 
            foreach (var commandType in Assembly.GetAssembly(typeof(ICommand)).GetTypes().                                    Where(type=>typeof(ICommand).IsAssignableFrom(type)))            {                this.Bind<ICommand>().To(commandType).Named(commandType.Name);            }        }
    }    
    public class NameInstanceProvider : StandardInstanceProvider
    {
        protected override string GetName(MethodInfo methodInfo, object[] arguments)
        {
            return (string)arguments[0];
        }
    }

Hay muchas formas de hacer esto. Lo que estamos haciendo es obtener todos los tipos que implementen la interfaz ICommand y hacer el Binding de ICommand a cada uno de esos tipos indicado que el Name del Binding sea el nombre del tipo de comando. Esto se traduciría en this.Bind<ICommand>().To<UserWriteCommand>().Named(typeof(UserWriteCommand).Name)
. Un detalle importante es que estamos obteniendo todos los tipos dentro del ensamblado de ICommand, y es más que posible que los comandos estén en diferente ensamblado que la definición de ICommand, pero eso es otro tema 🙂
Le damos una última vuelta de tuerca. Ninject nos proporciona una forma de hacer esto de forma «más directa», aunque es posible que para lo que estamos haciendo sea más compleja. Sería de la siguiente forma.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public class Composer : NinjectModule
    {
        public override void Load()
        {
            this.Bind<IConsoleWriter>().To<ConsoleWriter>();
            this.Bind<IUserRepository>().To<UserRepository>();
 
            this.Bind<ICommandFactory>().ToFactory(() => new NameInstanceProvider());
 
            this.Bind(c => c.FromThisAssembly().SelectAllClasses().InheritedFrom<ICommand>().                    BindAllInterfaces().Configure((syntax, type) => syntax.Named(type.Name)));        }
    }
 
    public class NameInstanceProvider : StandardInstanceProvider
    {
        protected override string GetName(MethodInfo methodInfo, object[] arguments)
        {
            return (string)arguments[0];
        }
    }

En el código anterior, estamos haciendo uso de las convenciones de Ninject. Decimos que, para el ensamblado actual, seleccione todas las clases que «hereden» de ICommand, y haga el Binding del interfaz ICommand para cada una de las clases que haya seleccionado previamente, en concreto haciendo que ese Binding se haga por el nombre del tipo de cada una de las clases. A mí me chirría un poco la parte de InheritFrom; con los nombres tan expresivos (y largos) que utiliza Ninject, utilizar lo mismo para indicar que algo hereda de una clase que para indicar que algo implementa una interfaz, no sé, se me hace un poco raro. Como detalle, volvemos a lo que dijimos antes, estamos utilizando FromThisAssembly() y en este caso, casi seguro que no tenemos los comandos en el mismo ensamblado que el contenedor. Hay otras opciones que podemos utilizar para especificar el ensamblado, aquí las podéis ver. Como podemos observar, esta última opción para hacer los Bindings de los comandos quizá sea un poco compleja o, más que compleja, necesita más conocimiento de la herramienta (Ninject), para lo que realmente nos está aportando.
Finalmente, vamos a probar que funcione:

1
2
3
4
5
6
7
8
9
10
    class Program
    {
        static void Main(string[] args)
        {
            var composer = new StandardKernel(new Composer());
            ICommand command = composer.Get<ICommandFactory>().CreateCommand(nameof(UserWriteCommand));
            command.Execute();
            Console.ReadLine();
        }
    }

Ejecutamos y el resultado es el esperado.

Aquí terminamos la serie de factorías en Ninject, espero que a alguien le pueda ser de utilidad 🙂
¡Un saludo!

Otros post:
Ninject. Crear factorías I
Ninject. Crear factorías II
Ninject. Crear factorías III

One thought on “Ninject. Crear factorías III”

Deja un comentario

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