wtorek, 22 listopada 2011

Inversion of Control/Dependency Injection & StructureMap cz.2

Wyobraźmy sobie sytuację w której mamy jakąś aplikację, która jest sklepem internetowym. W aplikacji takiej powinniśmy móc dodać coś do koszyka zakupowego i oczywiście zobaczyć w każdej chwili co znajduje się w naszym koszyku.
public class ShoppingCartController{  private ShoppingCartService _shoppingCartService;  public ShoppingCartController()  {      _shoppingCartService = new ShoppingCartService();  }  public ShoppingCart GetCart()  {      ShoppingCart cart = _shoppingCartService.GetContents();      return cart;  }  public ShoppingCart AddItemToCart(int itemId, int quantity)  {      ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);      return cart;  }}

Zakładamy że za pomocą klasy ShoppingCartService aplikacja łączy się z jakąś usługą. Przestowanie tego kawałka kodu polegałoby na napisaniu metody testującej w której wywołujemy po prostu metody klasy ShoppingCartController. Załóżmy teraz, że chcelibyśmy napisać testy, które nie będą łączyć się z usługą. Co trzeba by było zrobić. Najprostszym sposobem jest stworzenie fake'a klasy ShoppingCartService - nazwijmy ją ShoppingCartServiceFake. Zmodyfikujmy kod aby zadanie to było możliwe do wykonania. Na początku, stwórzmy interfejst IShoppingCartService i zmodyfikujmy nieco kod tak jak poniżej:
public interface IShoppingCartService{  ShoppingCart GetContents();  ShoppingCart AddItemToCart(int itemId, int quantity);}public class ShoppingCartController{  private IShoppingCartService _shoppingCartService;  public ShoppingCartController()  {      _shoppingCartService = new ShoppingCartService();  }  public ShoppingCart GetCart()  {      ShoppingCart cart = _shoppingCartService.GetContents();      return cart;  }  public ShoppingCart AddItemToCart(int itemId, int quantity)  {      ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);      return cart;  }}

Zakładamy teraz że ShoppingCartService implementuje interface IShoppingCartService. Teraz dopiszmy klasę fake:
public class ShoppingCartServiceFake : IShoppingCartService{  public ShoppingCart GetContents()  {      return new SoppingCart(){Items = ...tu tworzymy sobie jakąś listę elementów};  }  public ShoppingCart AddItemToCart(int itemId, int quantity)  { return new SoppingCart(){Items = ...tu tworzymy sobie jakąś listę elementów};  }}

Ostatnim elementem jest dopisanie nowego konstruktora do klasy ShoppingCartController:
public ShoppingCartController(IShoppingCartService shoppingCartService){  _shoppingCartService = shoppingCartService;}

Powyższy konstruktor nazywa się: "Constructor Injection" a zabieg, który do tej pory zastosowaliśmy nazwany jest wzorcem Dependency Injection. Głównym paradygmatem tego wzorca jest usuwanie zależności pomiędzy komponentami (tak zwane tworzenie luźnych powiązań - z ang: loose coupling) a także tworzenie zewnętrznych połączeń pomiędzy komponentami. Wzorzec ten w dzisiejszych czasach jest jednym z najczęściej stosowanych wzorców projektowych. Główną zaletą wzorca Dependency Injection jest tworzenie obiektów łatwo testowalnych więc napiszmy teraz klase testującą:
[TestClass]public class ShoppingCartControllerTests{  [TestMethod]  public void GetCartSmokeTest()  {            ShoppingCartController controller =         new ShoppingCartController(new ShoppingCartServiceFake());            ActionResult result = controller.GetCart();            Assert... //sprawdzamy sobie tutaj jakiś warunek np. ilośc itemów  }}

Jak widać na potrzeby testu jednostkowego tworzymy instancję obiektu ShoppingCartController z fałszywym obiektem, który udaje klasę łączącą się z usługą. Zaletą wzorca Dependency Injection jest nie tylko tworzenie obiektów łatwo testowalnych. Stosowanie tego wzorca w znacznym stopniu ułatwia refaktoryzacje kodu. Spójrzmy raz jeszcze na kod:
public interface IShoppingCartService{  ShoppingCart GetContents();  ShoppingCart AddItemToCart(int itemId, int quantity);}public class ShoppingCartController{  private IShoppingCartService _shoppingCartService;  public ShoppingCartController(IShoppingCartService shoppingCartService)  {      _shoppingCartService = shoppingCartService;  }  public ShoppingCart GetCart()  {      ShoppingCart cart = _shoppingCartService.GetContents();      return cart;  }  public ShoppingCart AddItemToCart(int itemId, int quantity)  {      ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);      return cart;  }}

Co daje programiście zastosowanie zamiast normalnego obiektu ShoppingCartService obiektu, który musi dziedziczyć po interfejsie IShoppingCartService. Otóż, w każdej chwili możemy do obiektu ShoppingCartController przekazać różne obiekty - muszą one tylko dziedziczyć po IShoppingCartService. Kolejnym plusem jest to, że jeśli przekażemy do kontruktora klasy ShoppingCartController np. nowo stworzony obiekt kompilator nie wyrzuci nam błędu. Ważne jest tylko to, żeby nie usuwać żadnych metod z interfejsu IShoppingCartService.

Teraz załóżmy sytuację w której chcielibyśmy móc w jednym miejscu skonfigurować, która klasa – czy ta która się łączy z usługą czy fałszywa – jest podawana do konstruktora:
public ShoppingCartController(IShoppingCartService shoppingCartService){  _shoppingCartService = shoppingCartService;}

I z pomocą tutaj przychodzi nam StructureMap. Dll'ke można pobrać ze strony:
https://github.com/structuremap/structuremap/downloads
Po pobraniu wystarczy dodać referencję do pliku StructureMap.dll. Teraz mając konstruktor przedstawiony powyżej piszemy nowy bezparametrowy:
public ShoppingCartController() : this(ObjectFactory.GetInstance< IshoppingCartService>{  _shoppingCartService = shoppingCartService;}

Teraz aby ObjectFaktory zwrócił nam odpowiedni obiekt klasy musimy napisać BootStrapper – specjalną klasę, która odpowiednio skonfiguruje nam StructureMap:
public class DependencyRegistry : Registry  {      public DependencyRegistry()      {          For<IShoppingCartService>().Use<ShoppingCartServiceFake>(); //Lub jeśli będziemy chceli używać klasy, która naprawdę łączy się z usługą:        // For<IShoppingCartService>().Use<ShoppingCartService>();      }  }  public static class BootStrapper  {      public static void Bootstrap()      {          ObjectFactory.Initialize(x => x.AddRegistry(new DependencyRegistry()));               }  }

Klasa DependencyRegistry musi dziedziczyć po klasie Registry.
Teraz w metodzie main klasy program wystarczy wpisać: BootStrapper.BootStrap() i podczas wywołania nowego kontruktora do klasy zostanie przekazany odpowiedni obiekt klasy dziedziczącej po interfejsie IShoppingCartService.
StructureMap jest tak zwanym kontenerem IoC - z angielskiego Inversion of Controle. Jak widać z przykładu kontenery te umożliwiają przekazanie odpowiednich obiektów w zależności od określonego interfejsu.
Pojęcia Inversion of Controle często kojarzone jest z Dependency Injection jednak to nie jest do końca prawda ponieważ Dependency Injection jest tylko jednym z sposobów realizacji Inversion of Controle.

Mam nadzieje, że stworzony przeze mnie artykuł chociaż w części uzmysławia czym jest wzorzec Inversion of Controle i dlaczego warto go stosować.

1 komentarze:

Krzysiek pisze...

Mam nadzieję, że po lekturze artykułu o Mockach każdy łatwo zauważy, że w teście GetCartSmokeTest(), można zastosować MockRepository.GenerateStrictMock(); lub inną z metod do tworzenia mocków. Otrzymujemy w ten sposób sztuczną instancję tegoż interfejsu i zyskujemy możliwość dowolnego definiowania jego działania w teście - nie trzeba już definiować Fake'a.

Prześlij komentarz

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Online Project management