Inject #2Część NR.2Witam w drugim wpisie mojego eksperymentu. Tym razem będę objaśniał wstrzykiwanie zależności do konstruktora  oraz omówię podstawowe użycie Castle.Windsor. Oto seria  wpisów, które będą uczył programowania równocześnie opowiadając przy tym  historię.

W poprzednim odcinku.

W trakcie jazdy samochodem, w moim koszmarze objaśniłem, dlaczego klasa jest specyfikacją danego bytu. Powiedziałem także, dlaczego pola klasy muszą być prywatne. Podczas objaśniania tych funkcji potrąciłem autostopowicza.

Będąc w szoku, aby się odstresować, wydzieliłem swoje cechy i cechy autostopowicza do klasy abstrakcyjnej, która reprezentuje kategorie osoby i wspólnych cech.

Poprzednim odcinku

Sprawdziłem więc życie potrąconej osoby i stwierdziłem, że nie żyje. Potem wyparowała. Nie napisałem takiego zachowania w swoim programie. Co przegapiłem.

Do latarni

Byłem w szoku po tym wypadku. Ledwo mogłem stać na nogach. Postanowiłem zignorować wszystko, co do tej pory się wydarzyło.

Do latarni 2

Byłem bardzo blisko latarni morskiej, gdzie znajdowało się moje miejsce pracy. Tej nocy było wdrożenie. Przeniesienie aplikacji ze środowiska testowego na środowisko produkcyjne. Coś było nie tak i tylko ja mogłem to naprawić.

Samochód odmówił posłuszeństwa. Postanowiłem pójść pieszo.

Przechodząc przez most, odwróciłem się, w oddali, w miejscu, gdzie stał mój samochód zobaczyłem czarną sylwetkę.

Cienista osoba

Nagle czarna istota pojawiła się przede mną. Miała kształty osoby, ale została czymś udekorowana. Była czymś więcej.

Cienista osoba 2

Cienista osoba: Nie poznajesz mnie programisto? Myślisz, że jesteś bogiem? Myślisz, że możesz wymyślać różne rzeczy? Bawić się życiami, ludźmi, tworzyć ich w konstruktorze, a potem ich zabijać, kiedy wykonają już swoje zadania i zostaną potem zabrane przez czyściciela pamięci Garbage Collector.

Nagle zdałem sobie sprawę, że autostopowicz jest jedną z moich klas w grze komputerowej, którą pisałem w C# dla zabawy.

To prawda. W języku C# nie musimy się tak bardzo martwić o pamięć i o jej wycieki. Przykładowo, jeśli wewnątrz metody zostały utworzone obiekty pewnej klasy, to po skończeniu wykonywania tej metody, obiekty te zostaną usunięte z pamięci.

Tak samo dzieje się z obiektami utworzonymi wewnątrz bloku if.

 

Garbage Collector

Gdy program zobaczy, że dany obiekt już nie jest przypisany do żadnej zmiennej, zostaje on wyczyszczony.

Nie dzieje się to natychmiast. Mechanizm “Garbage Collector” sam zdecyduje o tym, kiedy najwydajniej jest mu wyczyścić pamięć z nieużywanych obiektów.

Garbage Collector

Można wyczyszczenie wymusić, używając metody “Collect” statycznej klasy GC.

Nie jest to jednak zalecane.

Miałem jednak dziwne przeczucie, że cienista istota ma za złe dużo więcej.

Cienista osoba: Chcesz dodać więcej dramatu do tego programu. Teraz ty jesteś w programie i sprawię, że będziesz cierpiał. Nauczę cię dobrze programować. Jesteś kiepskim programistą.

Ucieczka

Nie traciłem czasu. Postanowiłem uciekać jak najdalej od swojego zagrożenia. Skakałem, biegłem. Strach dodawał mi skrzydeł.

 zobaczyłem, że mroczna postać trzyma nic innego niż siekierę

Gdy myślałem, że już uciekłem zobaczyłem, że mroczna postać trzyma nic innego niż siekierę niczym Jack Nicolson z filmu Shining.

Siekiera to tylko jedna z broni, której mógł użyć przeciwko mnie. Każda broń gwarantuje, że potrafi uderzyć. Sama kategoria broni jednak nie określa, jak ma mnie ona uderzyć. Wiem, że broń nie może być klasą, ani klasą abstrakcyjną.

Dlatego stwierdziłem, że jest ona interfejsem. IWeapon

 

public interface IWeapon
{
    void Hit(Person person);
}

public class BareHands : IWeapon
{
    public void Hit(Person person)
    {
        person.TakeDamage(5);
    }
}

public class Axe : IWeapon
{
    public void Hit(Person person)
    {
        person.TakeDamage(50);
    }
}

Nie mogę już dalej uciekać. Mój przeciwnik stoi naprzeciwko. Ja też posiadam broń. Moje gołe ręce jednak na niewiele się zdadzą, w takiej sytuacji.

Reguły tego świata uległy zmianie. Teraz każda osoba do swojego interfejsu będzie przyjmować interfejs “IWeapon”, określającą jakąś niewiadomą, na tym etapie broń.

Każda osoba teraz będzie mogła atakować, używając broni, gdy wykona metodę “AttackTarget”.

Każda osoba teraz będzie mogła obrywać śmiertelnie, gdyż w niej wykona się metoda “TakeDamage”.

public abstract class Person
{
    public void TakeDamage(int damage)
    {
        _lifePoints =- damage;
    }

    public void AttackTarget(Person person)
    {
        _weapon.Hit(person);
    }

    protected Person(int lifePoints,Gender gender, IWeapon weapon)
    {
        _lifePoints = lifePoints;
        _gender = gender;
        _weapon = weapon;
    }

    private IWeapon _weapon;

Konstruktor w klasie abstrakcyjnej uległ zmianie. Co zatem idzie mój konstruktor i mojego przeciwnika też.

public class Hero : Person
{
    public Hero(int lifePoints,Gender gender,IWeapon weapon)
        : base(lifePoints,gender,weapon)
    {}
}

public class Hitchhiker : Person
{
    public Hitchhiker(int lifePoints, Gender gender, IWeapon weapon)
        : base(lifePoints, gender,weapon)
    {}
}

Nie chciałem nawet atakować swojego przeciwnika. Biorąc jednak pod uwagę, że mam 100 punktów życia, to Cienista istota bez problemu zabije mnie dwoma uderzeniami siekiery. Każdy atak zabierze mi 50 punktów życia.

Axe axe = new Axe();
Person hero = new Hero(100,Gender.Men, new BareHands());
Person hitchhiker = new Hitchhiker(100, Gender.Men, new Axe());

hitchhiker.AttackTarget(hero);
hitchhiker.AttackTarget(hero);

if (!hitchhiker.CheckOthersPersonLife(hero))
{
    Console.WriteLine("Bohater umarł");
}

Powstaje też pytanie na temat determinizmu w tym świecie. Siekiera powinna  zabić mnie za jednym uderzeniami, jeśli by mnie trafiła w dobre miejsce.

Tak naprawdę wiele zmiennych mogłoby o tym zadecydować.

Nie ma jednak czasu, aby opisywać te skomplikowane zachowania z rzeczywistego świata.

public class Axe : IWeapon
{
    public void Hit(Person person)
    {
        int seed = Guid.NewGuid().GetHashCode();
        Random random = new Random(seed);
        int demage = random.Next(0, 50) + 50;
        person.TakeDamage(demage);
    }
}

Aby przestawić losowość zadanych obrażeń mogę użyć klasy Random. Umieścić jej unikalne ziarno, tak by mieć pewność, że algorytm nie zwróci dwa takiego samego wyniku losowania ze względu na zegar procesora.

Teraz siekiera może zadać od 50 do 100 obrażeń. W  zależności od wylosowanej liczby z przedziału od 0 do 50.

random.Next(0,50)

Prawda jest taka, że wewnątrz komputera wszystko można przewidzieć. Przypominają mi się czasy młodości, gdy próbowałem wygrać trudną bitwę grając z komputerem. Emulator środowiska, na którym działała gra, pozwalał mi się cofać do określonego miejsca działania programu i jego wewnętrznego zegara.

Niektóre parametry bitwy losowały się przeciwko komputerowi. Zaważyłem, że wciskając w odpowiednim momencie guziki jestem w stanie wpływać na wyniki losowania. Gdy wcisnąłem guzik dokładnie po 0.5 sekundzie od powrotu do miejsca wczytania gry zauważyłem, że wylosowane parametry są takie same.

Teraz jednak sytuacja uległa pogorszeniu. Siekiera w najgorszym przypadku może teraz zadać mi nawet 100 obrażeń i zabić. Dlaczego napisałem taki kod?

Usłyszałem okrzyk bojowy mojego przeciwnika.

Cienista osoba: Przekroczyłeś deadline! Jak mam wytłumaczyć to klientowi, że nie napiszę aplikacji szybciej niż Asseco, czy Comarch. Posmakuj marszu śmierci moją siekierą. Posmakuj gównianego kodu.

Musiałem zwiększyć swoje szanse na przeżycie.

Postanowiłem do klasy osoba przedstawić kolejną zależność. Zależność, która będzie określała sposób uniku.

Stworzyłem dwie klasy implementujące umiejętność uniku. NullDogeAbility informuje, że dana osoba nie ma żadnych zdolności uniku. Prawdopodobieństwo równa się zero.

public interface IDogeAbility
{
    double ChanceOfDoge();
}

public class NullDogeAbility : IDogeAbility
{
    public double ChanceOfDoge()
    {
        return 0;
    }
}

public class SwingDogeAbility : IDogeAbility
{
    public double ChanceOfDoge()
    {
        return 0.5;
    }
}

SwingDogeAbility da mi 50% szansę uniknięcie ataku

Unik

Każda osoba będzie mogła unikać obrażeń. Wewnątrz metody TakeDamage wykona się metoda prywatna DogedAttack. Prywatna, bo nie ma sensu jej używać poza klasą.

Używając klasy Random wylosuję liczbę, tym razem niecałkowitą z przedziału od 0 do 1. Jeśli wylosowana liczba jest mniejsza od mojej umiejętności uniku, którą zaraz określę, to przeżyję.

public abstract class Person
{
    public void TakeDamage(int damage)
    {
        if (!DogedAttack())
            _lifePoints = -damage;
    }

    private bool DogedAttack()
    {
        Random random = new Random(Guid.NewGuid().GetHashCode());
        double prob = random.NextDouble();
        if (prob < _dogeAbility.ChanceOfDoge())
            return true;
        return false;
    }

    protected Person(int lifePoints,Gender gender, 
        IWeapon weapon, IDogeAbility doge)
    {
        _lifePoints = lifePoints;
        _gender = gender;
        _weapon = weapon;
        _dogeAbility = doge;
    }

    private IWeapon _weapon;
    private IDogeAbility _dogeAbility;

Dałem sobie umiejętność 50% szans uniku. 

Axe axe = new Axe();
Person hero = new Hero(100,Gender.Men, 
    new BareHands(),new SwingDogeAbility());
Person hitchhiker = new Hitchhiker(100, Gender.Men, 
    new Axe(),new NullDogeAbility());

hitchhiker.AttackTarget(hero);
hitchhiker.AttackTarget(hero);

if (!hitchhiker.CheckOthersPersonLife(hero))
{
    Console.WriteLine("Bohater umarł");
}
else
{
    Console.WriteLine("Gra toczy sie dalej. Uciekam");
}

Kto wie, w ilu alternatywnych wszechświatach właśnie umarłem. Udało mi się jednak uniknąć jego ataku dwa razy. Pseudo losowość jest dzisiaj po mojej stronie.

Cienista osoba: Nie możesz mnie powstrzymać.

Biegnąc przed siebie zobaczyłem znajomą twarz. Czyżby to był mój znajomy z liceum, który się ze mnie nabijał, że programowanie jest dla leszczy, a ważniejsze są mięśnie.

Nie było to teraz istotne. Chociaż przez myśl mi przeszło, jak za parę lat będzie działała selekcja naturalna. Czy zdolności komputerowe zostaną uznane za seksi?

Most

Swoją drogą cały ten zbiór klas musi działać w jakimś projekcie. Czym jest ten świat? Chyba niegigantycznym plikiem Program.cs.

Nie było czasu na zastanawianie się. Zbiór klas to zbiór klas. Może on być użyty wszędzie. Zwłaszcza jeśli w Visual Studio wybierze się bibliotekę klas przenośnych.

Class Library (Portable).

Nawet gdy się wybrało projekt “Class Library” to istnieje później możliwość prze konwertowania na ten elastyczny typ. Czy to nie jest wspaniałe? Klasy napisane możemy użyć w aplikacji Konsolowej, WPF, WCF, Windows Phone, XBOX, Windows App. Instalując rozwiązanie XAMARIN, możemy później klasy napisane w C# użyć, pisząc aplikację na IPhone i Androida.

Class Library (Portable).

Utwórzmy więc projekt “Class Library”. Klas jest tak dużo, że trzeba je trzymać w oddzielnych folderach. Jak jednak klasy podzielić na grupy.

Solucja

Pobierz kodMożna to zrobić na wiele sposobów.

Można pliki klas, interfejsów i typów wyliczeniowych podzielić według ich odgrywanej roli w programie. Broń umieścić w folderze z bronią. Umiejętności uniku umieścić w folderze o nazwie tychże umiejętności. A klasy określające osoby - do folderu z osobami.

Gdy szukamy odpowiedniej broni, to wiemy, w jakim folderze mamy szukać.

Do projektu użytych klas przydałaby się konsola. Dodajmy do naszej solucji kolejny projekt. Solucja potrafi przechowywać wiele projektów, a projekt biblioteki jest tylko jeden.

Nowy projekt

Prawym przyciskiem myszki klikamy na “Solution” i wybieramy “New Project”. W oknie wyboru projektu tym razem wybieram projekt “Con soli”.

Projekt konsoli musi się odwoływać do projektu z bibliotekami. Inaczej on nie może używać jego klas.

Aby to zrobić trzeba kliknąć na ikonę “References” i dodać referencję do naszego projektu z bibliotekami.

Dodaj referencje

W następnym oknie trzeba kliknąć na zakładkę “Solution”, a potem z listy wybrać projekt naszej biblioteki. U mnie nazywa się on “NightmareWorld”.

Koszmarny świat

Mając tak poukładane klasy łatwiej było mi się zorientować się, co się dzieje.

Korzystając z rady przyjaciela schowałem się w pobliskiej chacie.

Ucieczka do chaty

On sam poczuł się bardziej pewnie. Nic dziwnego, miał przy sobie coś lepszego niż gołe dłonie. Miał pistolet.

Używanie interfejsów pozwala nam łatwo przestawić nowy mechanizm do naszej aplikacji. Każda osoba przyjmuje teraz broń. Definicja tej broni jednak nie jest zawarta nigdzie w klasie osoba. Na poziomie klasy osoba wie, że broń może tylko uderzyć.

Interfejs daje nam gwarancję, że broń może uderzyć, ale nie mówi jak. To definicja broni, którą później podam w konstruktorze.

public interface IWeapon
{
    void Hit(Person person);
}

public class Gun : IWeapon
{
    public void Hit(Person person)
    {
        person.TakeDamage(100);
    }
}

Pistolet zabija od razu. Zadaje 100 obrażeń.

Dla ułatwienia kodu aplikacji konsolowej napisałem klasę statyczną “StartAFight”. Klasa statyczna może posiadać tylko metody statyczne. Nie można utworzyć jej bytu.

public static class StartAFight
{
    public static void Fight(Person personA, Person personB)
    {
        while (personA.IsALife() || personB.IsALife())
        {
            personA.AttackTarget(personB);
            personB.AttackTarget(personA);
        }
    }

}

Służy ona jako przechowalnia metod, które do działania potrzebują tylko parametrów. Piszemy nazwę klasy, a po kropce piszemy metodę statyczną, którą chcemy wywołać.

StartAFigth

Metoda statyczna “Fight()” wymusi walkę

Przez okno widziałem jak stanął naprzeciw mrocznej istoty.

Siekiera

Postanowiłem przeanalizować kod tej walki.

Kod tej aplikacji konsolowej.

using System;
using NightmareWorld.DogeAbilities;
using NightmareWorld.People;
using NightmareWorld.Weapons;

namespace NightmareWorld.TestConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Person hitchhiker = new Hitchhiker(100, Gender.Men,
                new Axe(),
                new NullDogeAbility());

            OldFriend friend = new OldFriend(100, Gender.Men, new Gun(),
                new NullDogeAbility());

            StartAFight.Fight(hitchhiker, friend);

            if (hitchhiker.IsALife())
                Console.WriteLine("Wygrał Autostopowicz");
            else if (friend.IsALife())
                Console.WriteLine("Wygrał stary znajomy");
            else
                Console.WriteLine("Oboje zginęli");

            Console.ReadKey();
        }
    }
}

Analizując mój program. Zaatakują się nawzajem.

Broń na pewno go zabije, bo zadaje 100 obrażeń. Nie może uniknąć ataku.

Siekiera w najgorszym wypadku też go zabije.

Czyli rezultat walki zawsze powinien być taki sam, mój oprawca w końcu zginie.

Za oknem zobaczyłem jednak coś nielogicznego. Coś, co nie powinno się wydarzyć. Oprawca nie zginie, żaden strzał z broni go nie zranił.

Mój stary przyjaciel został zabity

Zabicie

Byłem uwięziony w drewnianej chacie.

Uwieziony w chacie

Poczułem się osaczony. Coś zrobiłem nie tak w moim programie i jeszcze do końca nie wiedziałem co.

Postanowiłem przeanalizować swój kod jeszcze raz. Jak jeszcze mogę go ulepszyć.

protected Person(int lifePoints, Gender gender,int age, string name, string surname,
    IWeapon weapon, IDogeAbility doge)

Konstruktor klasy abstrakcyjnej może urosnąć do niesamowitych rozmiarów, jeśli będę chciał do niej przekazywać inne informacje, jak: wiek i imię.

Obecnie w konstruktorze typy proste, określające liczbę punktów życia i płeć można wydzielić do oddzielnej klasy. Klasy abstrakcyjnej, która będzie miała dwie właściwości określające punkty życia i płeć.

Klasy, które będą dziedziczyć po niej będą określać konkretne wartości. Punkty życia się powielają i zwykle w tym programie mają one wartość 100. Dlatego konstruktor dla normalnych ludzkich  istot z góry będzie określać tę wartość.

Są więc dwie klasy dziedziczące po klasie abstrakcyjnej PersonBodyInfo. Jedna określa domyślne wartości dla ciała męskiego, a druga dla ciała żeńskiego.

public abstract class PersonBodyInfo
{

    protected PersonBodyInfo(int lifePoints, 
        Gender gender)
    {
        LifePoints = lifePoints;
        Gender = gender;
    }

    public int LifePoints { get; set; }
    public Gender Gender { get; private set; }
}

public class MenInfoBody : PersonBodyInfo
{
    public MenInfoBody() :
        base(100, Gender.Men)
    {}
}

public class WomanInfoBody : PersonBodyInfo
{
    public WomanInfoBody() : 
        base(100, Gender.Woman)
    {}
}

Teraz muszę tylko zmienić konstruktor osoby. Tak, by nie przyjmował już wieku i punktów życia, tylko klasę abstrakcyjną PersonBodyInfo.

public abstract class Person
{
    public bool IsALife()
    {
        return _personBodyInfo.LifePoints > 0;
    }

    public void TakeDamage(int damage)
    {
        if (!DogedAttack())
            _personBodyInfo.LifePoints -= -damage;
    }

    protected Person(PersonBodyInfo personBodyInfo,
        IWeapon weapon, IDogeAbility doge)
    {
        _personBodyInfo = personBodyInfo;
        _weapon = weapon;
        _dogeAbility = doge;
    }

Swoją drogą, dlaczego nie zmienić także konstruktorów dla klas określających konkretne osoby. Wszystkie postacie występujące w tym spektaklu są facetami.  Ja ciągle mam swoje dłonie i możliwość uniku.

Autostopowicz, moja zmora ma zawsze siekierę i zerową możliwość unikania.

A mój zmarły, stary przyjaciel miał pistolet.

public class Hero : Person
{
    public Hero()
        : base(new MenInfoBody(), new BareHands(), new SwingDogeAbility())
    {
    }
}

public class Hitchhiker : Person
{
    public Hitchhiker() 
        : base(new MenInfoBody(), new Axe(), new NullDogeAbility())
    {
    }
}

public class OldFriend : Person
{
    public OldFriend()
        : base(new MenInfoBody(), new Gun(), new NullDogeAbility())
    {
    }
}

Trzeba przyznać, że jest to genialny kod.

Gdy tylko wypowiedziałem te słowa,  w chacie można było odczuć trzęsienie ziemi. Chyba ktoś ma inne zdanie na temat tego kodu.

Cienista osoba z okna: Jak to jest, gdy się umiera z rąk własnej kreacji. Już nigdy nie napiszesz śmieciowego kodu.

Uwieziony w chacie 2

Myślałem, że umrę. Zrobiło się ciemno.

Poczułem, że lada moment sufit może się na mnie zawalić.

W jednej ze ścian zobaczyłem jasne światło. Usłyszałem wtedy głos, idź za światłem.

Kula światła: Idź za światłem. Głupcze!

Idz za światłem

Kula światła: Pozwól, że coś ci powiem na temat twojego kodu. Używasz techniki wstrzykiwania zależności do konstruktora.

Obecnie konstruktor klasy osoba przyjmuje obiekt, który implementuje interfejs broni, obiekt, który implementuje interfejs umiejętności unikania oraz obiekt, który dziedziczy po klasie abstrakcyjnej określającej cechy ciała.

W klasach dziedziczących po osobie masz konstruktor bezparametrowy, a konstruktor klasy osoby uzupełniasz gotowymi wartościami.

Jeśli chciałbyś zmienić definicję swoich umiejętności uniku wystarczy byś stworzył nową klasę implementującą interfejs IDogeAbilitiy. Później użył jej w swoim konstruktorze.

Jeśli chciałbyś dać sobie nową broń wystarczy, żebyś stworzył nową klasę implementującą interfejs “IWeapon”.

Jeśli chciałbyś dodać do siebie nowe cechy ciała, stworzyłbyś nową klasę dziedzicząc po “PersonalBodyInfo”.

Twój kod tworzy jednak poważne problemy.

Chce cię poinformować, że właśnie utknąłeś. Stworzyłeś problem, który może być tylko rozwiązany poprzez napisanie większości klas ponownie.

Pozwól, że zanim ciebie opuszczę nauczę cię czegoś o wstrzykiwaniu zależności i o tym, jak używać poprawnie techniki “Dependency Injection”.

Będzie ci potrzebny kontener.

Ja: Co? Jaki kontener?

Kula światła: Pozwól, że ci pokażę jeden z bardziej popularnych kontenerów wstrzykiwania zależności.

W nowej aplikacji konsolowej skorzystaj z systemu paczek NuGet. NuGet zawiera wiele przydatnych bibliotek, których możesz użyć.  Czasami nie musimy pisać kodu, który rozwiąże nasze problemy. Kto wie może go ktoś już napisał. Warto więc zajrzeć do istniejących paczek NuGet.

NuGet

Istnieje wiele kontenerów wstrzykiwania zależności. Patrząc na ilość pobrań w systemie NuGet, oto dwa najbardziej popularne:

  • Ninject
  • Castle Windsor

Castle Windsor istnieje od dłuższego czasu dlatego myślę, że rozumiesz, iż jako bardziej doświadczony programista preferuje tę bibliotekę.

W oknie dialogowym NuGet wpisz w polu wyszukiwania “Castle”. Twoim oczom powinna ukazać się lista paczek z aplikacjami Castle Windsor.

Zainstaluj paczkę “Castle Windsor”.

Castle Windsor

Co robi kontener wstrzykiwania zależności.

Określa jaki komponent…

Ja: Co to jest komponent?

Kula Światła:  Komponent określa fragment kodu w innych klasach, który zgodnie z filozofią wstrzykiwania i zasadą otwarte zamknięte, jest na tym etapie jeszcze nieokreślony. Dlaczego nie jest określony, bo jest albo interfejsem, albo klasą abstrakcyjną.

Obecnie takimi komponentami w klasie “Person” jest broń “IWeapon”, “IDogeAbliity” oraz informacje na temat ciała “PersonalBodyInfo”.

Są to rzeczywiście komponenty szkoda tylko, że uzupełniasz je w taki zły sposób. Tworzy to jeszcze inny problem.

Pozwól, że pokażę ci jak działa kontener. Stworzyłem klasę BootStrapper, która powinna na początku działania aplikacji uruchomić metodę “BootUniverse”.

Wewnątrz metody “BootUniverse” do kontenera rejestrujemy jak dany interfejs lub klasa abstrakcyjna będzie zaimplementowana. Kontener ten będzie przechowywał informacje o każdej takiej rejestracji.

public static class BootStrapper
{
    public static void BootUniverse()
    {
        IWindsorContainer container = new WindsorContainer();

        container.Register(
            Component.For<IWeapon>().ImplementedBy<Axe>(),
            Component.For<PersonBodyInfo>().ImplementedBy<MenInfoBody>(),
            Component.For<IDogeAbility>().ImplementedBy<NullDogeAbility>()
            );

        Container = container;
    }

    public static T Resolve<T>()
    {
        return Container.Resolve<T>();
    }

    public static IWindsorContainer Container;
}

Później w aplikacji, po rejestracji komponentów, na początku aplikacji mogę wyciągnąć ich konkretne definicje. Chociaż na poziomie klasy Program nie wiem, co to dokładnie jest.

public class Program
{
    static void Main(string[] args)
    {
        BootStrapper.BootUniverse();

        IWeapon weapon = 
            BootStrapper.Resolve<IWeapon>();
        IDogeAbility dogeAbility = 
            BootStrapper.Resolve<IDogeAbility>();
        PersonBodyInfo personBodyInfo = 
            BootStrapper.Resolve<PersonBodyInfo>();

    }
}

Jeśli w kontenerze istnieją takie zarejestrowane komponenty. Zostaną one uzupełnione w przeciwnym wypadku otrzymam wyjątek aplikacji.

Resolve

Ja: Co to daje.

Kula światła: Obecnie istnieje pewien problem z twoimi konstruktorami. Nie ma jak ich uzupełnić. A nawet, gdyby była taka możliwość, obecnie tworzy to kolejny problem. Nie chcemy, aby każda osoba miała taką samą broń, taką samą umiejętność uniku i ten sam zestaw cech ciała.

Ja: Możesz omówić ten problem jaśniej.

Kula światła: Pozwól, że pokażę ci mój przykład, który poprawnie używa techniki wstrzykiwania zależności do konstruktora.

Mamy interfejs świata. Obecnie gwarantuje on, że w tym świecie możemy walczyć.

public interface IWorld
{
    void Fight(Person personA, Person personB);
}

To jednak nie koniec, nasz świat też będzie miał komponenty. Pierwszym komponentem będzie pogoda określona interfejsem “IWeather”.

Pogoda da możliwość stracenia tury.

public interface IWeather
{
    bool SkipTurn();
}

public class FogWeather : IWeather
{
    public bool SkipTurn()
    {
        Random random = new Random();
        return random.NextDouble() > 5.0;
    }
}

public class NormalWeather : IWeather
{
    public bool SkipTurn()
    {
        return false;
    }
}

Przy pochmurnej pogodzie szansa stracenia tury wynosi 50% . Przy normalnej pogodzie mechanizm tracenia tury nie działa.

Drugim komponentem, który będzie potrzebował nasz świat jest algorytm określający, jak walka ma się zakończyć. Ten algorytm jest określany interfejsem “IConditionOfEndFigth”.

public interface IConditionOfEndFight
{
    bool BattleEnded(Person a, Person b);
}

public class OneDeathEndThisFight : IConditionOfEndFight
{
    public bool BattleEnded(Person a, Person b)
    {
        return !a.IsALife() || !a.IsALife();
    }
}

public class OneRoundEndThisFight : IConditionOfEndFight
{
    private int _increment = 0;

    public bool BattleEnded(Person a, Person b)
    {
        if (_increment == 1)
            return true;
        _increment++;
        return false;
    }
}

Walka może zakończyć się na dwa sposoby.

Pierwsza klasa implementująca ten interfejs określa, że walka zakończy się, dopiero gdy jedna z osób nie będzie już żyć.

Druga klasa implementująca ten interfejs określa, że walka zawsze będzie się kończyć po jednej rundzie.

A teraz jak wygląda klasa implementująca świat. Widzimy, że klasa “NightmareWorld” zawiera dwa komponenty. Pogody “IWeather” i zasady zakończenia walki “IConditionOfEndFight”.

public class NigthmareWorld : IWorld
{
    private IWeather _weather;
    private IConditionOfEndFight _conditionOfEndFight;

    public NigthmareWorld(IWeather weather, IConditionOfEndFight conditionOfEndFight)
    {
        _weather = weather;
        _conditionOfEndFight = conditionOfEndFight;
    }

    public void Fight(Person personA, Person personB)
    {
        while (_conditionOfEndFight.BattleEnded
            (personA,personB))
        {
            if (!_weather.SkipTurn())
                personA.AttackTarget(personB);

            if (!_weather.SkipTurn())
                personB.AttackTarget(personA);
                    
        }
    } 
}

Algorytm walki jest zbliżony do tego, który napisałem wcześniej w swojej klasie statycznej.

Jednakże nie powinieneś używać klasy statycznych do określania tak ważniej części kodu. W klasie statycznej nie możemy wstrzykiwać zależności.

Użyjmy więc klasy świata.

W kontenerze określamy jak pogoda i algorytm powinny być zaimplementowane.

public static class BootStrapper
{
    public static void BootUniverse()
    {
        IWindsorContainer container = new WindsorContainer();

        //container.Register(
        //    Component.For<IWeapon>().ImplementedBy<Axe>(),
        //    Component.For<PersonBodyInfo>().ImplementedBy<MenInfoBody>(),
        //    Component.For<IDogeAbility>().ImplementedBy<NullDogeAbility>()
        //    );

        container.Register(
            Component.For<IWeather>().ImplementedBy<FogWeather>(),
            Component.For<IConditionOfEndFight>().ImplementedBy<OneRoundEndThisFight>(),
            Component.For<IWorld>().ImplementedBy<NigthmareWorld>()
            );

        Container = container;
    }

    public static T Resolve<T>()
    {
        return Container.Resolve<T>();
    }

    public static IWindsorContainer Container;

}

W samym programie robię tylko “resolve “na interfejsie IWorld.

Nadużywanie lokalizowania zależności  prowadzi do innego problemu.

Castle.Windsor i jego kontener w trakcie tworzenia świata “IWorld” zaimplementuje w konstruktorze automatycznie wszystkie zależności.

public class Program
{
    static void Main(string[] args)
    {

        BootStrapper.BootUniverse();

        Person hero = new Hero();
        Person hitchhiker = new Hitchhiker();

        IWorld world = BootStrapper.Container.Resolve<IWorld>();

        world.Fight(hero,hitchhiker);

    }
}

Pogoda i algorytm kończący walkę zostały już zaimplementowane.

image_thumb59

Ja: Wooah!

Kula światła: To jednak jeszcze nie koniec. To dopiero początek. Przy okazji pobierz kod.

Pobierz Kod