piątek, 27 lutego 2009

IoC oraz ASP .NET MVC

logo W tej notce postaram się przedstawić rozwiązanie pewnego częstego problemu z jakim spotykamy się podczas tworzenia aplikacji w modelu MVC.

Zapewne rzadko kiedy kontroler może obyć się bez dostarczenia mu obiektu reprezentującego repozytorium danych. Nieważne, czy jest to lista użytkowników, produktów, czy inna podobna inna lista, operacje na niej (choćby wyświetlanie danych) są wykonywane przez logikę biznesową zawartą właśnie w kontrolerze. Przyjmijmy, że chcielibyśmy zasilić nasz HomeController właśnie obiektem reprezentującym listę produktów. Jak tego dokonać? Jest kilka sposobów. Pierwszym z nich jest utworzenie własnego ControllerFactory. Jednak w naszym przypadku oprogramowanie zajęło by za dużo czasu :-)

Z pomocą przychodzi nam mechanizm IoC, który świetnie nadaje się do naszego MVC-problemu. Nie będę zajmował się tutaj wyjaśnianiem samego mechanizmu inversion of control, gdyż zakładam, że czytający ten tekst ma przynajmniej podstawowe pojęcie w tej dziedzinie. Ja wykorzystam do tego celu kontener Ninject.

Strategy pattern Zanim zaczniemy co kolwiek kodować warto rozejrzeć się "po okolicy" i zaplanować najlepszy sposób rozwiązania naszego problemu.

public class TestController : Controller 
{
  private ClientRepository _clientRepository;
  public TestController() {
    _clientRepository = new ClientRepository();
  }
  public void Add(string clientName) {
    _clientRepository.InsertNewClient(clientName);
  }
}


Powyższy fragment kodu (który na razie traktujmy jako pewien pseudokod) wydaje się być w miarę dobrym podejściem do utworzenia naszego repozytorium klientów. Podczas tworzenia kontrolera w jego konstruktorze powołujemy do życia obiekt typu ClientRepository i przechowujemy w prywatnej zmiennej.

Wady! Zdecydowaną wadą tego rozwiązania jest właśnie zależność. TestController ma zależność co do ClientRepository. Nie jesteśmy w stanie przekazać do naszej zmiennej repozytorium _clientRepository innego typu repozytorium, nawet gdyby ich kontrakt był taki sam (w przypadku repozytoriów często przecież tak jest) - bez zmiany kodu konstruktora: tightly coupled (ps. jak to przetłumaczyć? :-) ).


I dopiero po tak przydługim wstępie na scenę wchodzi StrategyPattern (w wolnym tłumaczeniu: wzorzec strategii).



strategy_simple 
Refaktoryzujemy nasz kod. Czas, aby wykorzystać wspomnianą bibliotekę Ninject.



Chcemy użyć mechanizmu wstrzykiwania przez konstruktor (oczywiście nic nie stoi na przeszkodzie aby zrobić to przez właściwość, czy metodę - jednak to już każdy w domu może sobie zrobić :-) ). Ninject ma kilka metod, które świetnie działają w połączeniu z kontrolerami. Ale po kolei. Pierwsze co musimy zrobić, to dodać dziedziczenie (w Global.asax) dla klasy MvcApplication z klasy NinjectHttpApplication.



public class MvcApplication : NinjectHttpApplication

Następnie przeciążamy dwie metody: RegisterRoutes oraz CreateKernel. Pierwsza z metod może być implementowana w taki sam sposób jak to było zaimplementowane dotychczas. Ważna jest wspomniana metoda CrateKernel.

protected override IKernel CreateKernel()
{
    IModule[] modules = new IModule[]
    {
 new AutoControllerModule("Testowa.Controllers"),
        new ServiceModule()
    };
    return new StandardKernel(modules);
}

internal class ServiceModule : StandardModule
{
    public override void Load()
    {
        Bind<Testowa.Controllers.IRepository>().To<Testowa.Controllers.ClientRepository>();
    }
}


Czyli po kolei: Tworzymy tablicę modułów (można to zrobić bez tablicy także). Za pomocą AutoControllerModule "przypinamy" się do zewnętrznej biblioteki klas kontrolerów znajdującej się w Testowa.Controllers. Ja preferuję wydzielenie kontrolerów do zewnętrznej biblioteki - jeśli jednak u Ciebie kontrolery znajdują się w tej samej assembly co aplikacja Web, to możesz użyć metody Assembly.GetExecutingAssembly() przekazanej jako parametr do konstruktora AutoControllerModule. Natomiast metoda Load naszego modułu binduje implementację interfejsu IRepository jako obiekt typu ClientRepository wszędzie tam, gdzie będziemy potrzebować typu IRepository.

Konsumpcja

Tworzymy nasz HomeController wiedząc, że wszystkie zależności automagicznie rozwiąże za nas Ninject. W tym celu w naszym przeprostym kontrolerze piszemy coś na wzór:



public class HomeController : Controller
{
    private readonly IRepository _service;
    public HomeController(IRepository clientRepository)
    {
        this._service = clientRepository;
    }
}


Zależności zostały zerwane, obiekty powoływane są do życia "same". Robimy sobie kawę.

3 komentarze:

marekblotny pisze...

Fajny post Darek!

Ostatnio slyszalem kilka dobrych rzeczy o Ninject, fajnie zobaczyc jak go mozna wykorzystac w praktyce.

Bawiles sie kiedys Springiem? Ciekaw jestem jak wypada porownanie Nijecta ze Springiem wlasnie.

Dariusz Tarczyński pisze...

@marek
Może napiszę dlaczego Ninject, a nie właśnie Spring czy Castle Winsor/Microkernel?
Otórz przedewszytkim poszukiwałem czegoś lekkiego, szybkiego i prostego. Takie założenia przyświecają Ninjectowi. Choć nie robiłem porównania pod wzgl. performance, to Ninject wydaje się być pod tym względem w 100% satysfakcjonującym wyborem. Druga sprawa, to konfiguracja. Ninject oferuje możliwość jego konfigurowania min. za pomocą fluent interface, a że ostatnio spodobał mi sie FluentNHibernate, to wybór też przemawiał za Ninjectem. Ten prosty przykład wymagał jedynie jednej lininki kodu konfiguracyjnjego - bez zbędnych XML-i w których struktura zajmuje więcej niż zawartość.

Anonimowy pisze...

Każdy ładnie pisze o IoC w kontrolerach... A co z IoC w filtrach czy "user kontrolkach:("

PS http://blog.ashmind.com/index.php/2008/08/19/comparing-net-di-ioc-frameworks-part-1/ macie porównanie wydajności