¡Hola!
En el post anterior vimos un poco cómo funcionan las factorías en Ninject y cómo nos puede ayudar a crearlas automáticamente. Vamos a crear una clase Car y una factoría que nos sirva para crear coches:
1 2 3 4 5 6 7 8 9 10 11 12 | public interface ICarFactory { Car GetCar(); } public class Car { public Car() { Console.WriteLine("Acabamos de crear un coche"); } } |
Ahora en nuestro contenedor vamos a crear la factoría:
1 2 3 4 5 6 7 | public class Composer : NinjectModule { public override void Load() { this.Bind<ICarFactory>().ToFactory(); } } |
Y por último ejecutamos el siguiente código para crear un coche nuevo:
1 2 3 4 5 6 7 8 9 10 | class Program { static void Main(string[] args) { var composer = new StandardKernel(new Composer()); var carFactory = composer.Get<ICarFactory>(); var myCar = carFactory.GetCar(); Console.ReadLine(); } } |
¿Qué ha pasado? Ninject nos lanza una excepción 🙁
Nos está diciendo que no hay Bindings disponibles para Car, mmmm qué raro, vamos a hacer el Binding explícito en el contendor por si acaso:
1 2 3 4 5 6 7 8 | public class Composer : NinjectModule { public override void Load() { this.Bind<ICarFactory>().ToFactory(); this.Bind<Car>().ToSelf(); } } |
En la línea marcada simplemente decimos que cuando se solicite un Binding de Car se devuelva un Car (algo así como decir this.Bind<Car>().To<Car>()). Vale, volvemos a ejecutar el programa y…
Volvamos a fijarnos un momento en la interfaz ICarFactory, simplemente tiene un método que se llama GetCar() y justamente ese es el problema. Realmente no es que sea un problema, sino que es una convención de Ninject y, si no la sabes, puede hacer que estés un buen rato buscando el error. Lo que hace Ninject es que siempre que el método de la factoría empiece por «Get» obtiene el texto que hay a continuación, en nuestro caso sería «Car». Ese texto que ha extraído «Car», lo utiliza para buscar un Binding con ese nombre en concreto. Vamos a cambiar nuestro contenedor por lo siguiente:
1 2 3 4 5 6 7 8 | public class Composer : NinjectModule { public override void Load() { this.Bind<ICarFactory>().ToFactory(); this.Bind<Car>().ToSelf().Named("Car"); } } |
Estamos indicando en el Binding de Car, que se resuelva cuando dicho Binding intente ser resuelto con el nombre «Car». Ahora si ejecutamos el programa sí que va a funcionar correctamente.
Esta característica nos serviría para por ejemplo añadir un método a la factoría que nos crease un coche muy potente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public interface ICarFactory { Car GetCar(); Car GetPowerfulCar(); } public class Car { public Car() { Console.WriteLine("Acabamos de crear un coche"); } } public class PowerfulCar : Car { public PowerfulCar() { Console.WriteLine("Creado coche poderoso"); } } |
Y nuestro contenedor quedaría así:
1 2 3 4 5 6 7 8 9 | public class Composer : NinjectModule { public override void Load() { this.Bind<ICarFactory>().ToFactory(); this.Bind<Car>().ToSelf().Named("Car"); this.Bind<Car>().To<PowerfulCar>().Named("PowerfulCar"); } } |
Ejecutamos lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 | class Program { static void Main(string[] args) { var composer = new StandardKernel(new Composer()); var carFactory = composer.Get<ICarFactory>(); var myCar = carFactory.GetCar(); var myPowerfulCar = carFactory.GetPowerfulCar(); Console.ReadLine(); } } |
Y funciona correctamente.
Con esta convención que tiene Ninject, vamos a aprovechar para ver un poco cómo funcionan las factorías por debajo. Cuando hacemos ToFactory() en el contendor, realmente Ninject utiliza su proveedor por defecto, que en este caso es StandardInstanceProvider. Este proveedor es el que se encarga ajustar y solicitar la creación de nuestro objeto. Si nos fijamos en su método GetName, veremos que comprueba si el nombre empieza por Get para generar un nombre y así buscar ese Binding en concreto. Realmente lo que se acaba haciendo es generar una constraint que busque el Binding adecuado (algo así como binding.Name = name).
1 2 3 4 | protected virtual string GetName(MethodInfo methodInfo, object[] arguments) { return methodInfo.Name.StartsWith("Get") ? methodInfo.Name.Substring(3) : null; } |
Podríamos crear nuestro propio Provider para decir que en vez de usar «Get» como convención, usemos «Create». En otros posts le buscaremos usos más interesantes a StandardInstanceProvider 🙂 Simplemente heredamos de StandardInstanceProvider sobreescribimos el método GetName:
1 2 3 4 5 6 7 | public class DummyInstanceProvider : StandardInstanceProvider { protected override string GetName(MethodInfo methodInfo, object[] arguments) { return methodInfo.Name.StartsWith("Create") ? methodInfo.Name.Substring(6) : null; } } |
Ahora en nuestro composer hacemos lo siguiente:
1 2 3 4 5 6 7 8 9 | public class Composer : NinjectModule { public override void Load() { this.Bind<ICarFactory>().ToFactory(()=>new DummyInstanceProvider()); this.Bind<Car>().ToSelf().Named("Car"); this.Bind<Car>().To<PowerfulCar>().Named("PowerfulCar"); } } |
Y obtendríamos el mismo comportamiento que antes si cambiamos la factoría:
1 2 3 4 5 | public interface ICarFactory { Car CreateCar(); Car CreatePowerfulCar(); } |
Vamos a darle una última vuelta al contenedor. Si nos fijamos tenemos algo tal que así:
1 | this.Bind<Car>().ToSelf().Named("Car"); |
¿Qué pasa aquí? Pues que estamos poniendo un string «Car» y no suele ser demasiada buena idea. Tenemos varias opciones, como sacar ese texto a una constante, dependiendo de qué versión de C# podemos usar nameof(Car)… Pero Ninject nos proporciona una opción mejor, que nos facilitará tanto la legibilidad del código, como futuras refactorizaciones. Eso sí, sólo funciona si nuestra interfaz ICarFactory tiene los métodos con «Get» (como GetCar()):
1 | this.Bind<Car>().ToSelf().NamedLikeFactoryMethod((ICarFactory f) => f.GetCar()); |
Con lo anterior especificamos que resuelva nuestro Binding sólo cuando sea llamado desde el método GetCar de ICarFactory. Internamente está haciendo exactamente lo mismo que hacíamos con el Named(«Car»).
¿Y qué pasaría si Car tuviese que resolver alguna dependencia, por ejemplo ICarPainter? Ninject lo haría sin problema siempre que la definamos en el contenedor. Pero, ¿y si queremos un Binding específico de ese ICarFactory para cuando se cree un coche a través de nuestra factoría? Es posible que estemos complicando demasiado el diseño (o no), pero Ninject nos da la opción de hacerlo gracias a WhenAnyAncestorNamedLikeFactoryMethod. Vamos a verlo en un ejemplo. En nuestra factoría vamos a crear dos métodos, uno que nos devuelva un objeto de tipo Car con el CarPainter normal y otro que nos lo devuelva con otro CarPainter.
1 2 3 4 5 | public interface ICarFactory { Car GetCar(); Car GetWithSpecifiedCarPainter(); } |
Ahora vamos a ver el resto del código, tanto de Car, como de ICarPainter y sus dos 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 25 26 27 28 29 30 31 32 33 34 35 36 | public class Car { private readonly ICarPainter carPainter; public Car(ICarPainter carPainter) { this.carPainter = carPainter; Console.WriteLine("Acabamos de crear un coche"); } public void DrawCar() { this.carPainter.Draw(); } } public interface ICarPainter { void Draw(); } public class CarPainter : ICarPainter { public void Draw() { Console.WriteLine("El coche se ha pintado gracias a nuestro pintador GENÉRICO:)"); } } public class SpecificCarPainter : ICarPainter { public void Draw() { Console.WriteLine("El coche se ha pintado gracias a nuestro pintador ESPECIFICO :)"); } } |
Vamos a ver cómo tenemos que especificar en nuestro contenedor nuestra factoría y los dos Bindings de ICarPainter.
1 2 3 4 5 6 7 8 9 10 11 | public class Composer : NinjectModule { public override void Load() { this.Bind<ICarFactory>().ToFactory(); this.Bind<Car>().ToSelf().NamedLikeFactoryMethod((ICarFactory f) => f.GetCar()); this.Bind<Car>().ToSelf().NamedLikeFactoryMethod((ICarFactory f) => f.GetWithSpecifiedCarPainter()); this.Bind<ICarPainter>().To<CarPainter>(); this.Bind<ICarPainter>().To<SpecificCarPainter>().WhenAnyAncestorNamedLikeFactoryMethod((ICarFactory f) => f.GetWithSpecifiedCarPainter()); } } |
En la primera línea creamos nuestra factoría. En las dos siguientes, indicamos que Car se resuelva (con él mismo) para ambos métodos de la factoría. Lo interesante son las dos últimas líneas. En this.Bind<ICarPainter>().To<CarPainter>() estamos especificando el Binding normal de ICarPainter, que será el utilizado en el método GetCar de la factoría, pero en this.Bind<ICarPainter>().To<SpecificCarPainter>().WhenAnyAncestorNamedLikeFactoryMethod((ICarFactory f) => f.GetWithSpecifiedCarPainter()) estamos especificando que el Binding de ICarPainter se resuelva con SpecificCarPainter cuando sea creado a través de la factoría ICarFactory con el método GetWithSpecifiedCarPainter.
Ejecutamos el siguiente código:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Program { static void Main(string[] args) { var composer = new StandardKernel(new Composer()); var carFactory = composer.Get<ICarFactory>(); var myCar = carFactory.GetCar(); myCar.DrawCar(); var myCarWithSpecifiedCarPainter = carFactory.GetWithSpecifiedCarPainter(); myCarWithSpecifiedCarPainter.DrawCar(); Console.ReadLine(); } } |
Y podemos comprobar que nuestro resultado es correcto.
Como hemos visto son muy flexibles las factorías con Ninject, pero es posible que en los últimos ejemplos el diseño puede que sea demasiado complejo y estemos añadiendo mucha magia que se traduce en complejidad en nuestro contenedor, así que cuidado.
¡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 II”