sobota, 22 października 2011

Prism czyli DI

DI czyli Dependency Injection (wstrzykiwanie zależności) to technika usuwająca zależności pomiędzy komponentami. Definicję można znaleźć np. w wikipedi czy też w wielu innych źródłach, jednak dla osób wolących zobaczyć w przykładzie o co chodzi pokażę na przykładzie w jaki sposób można skorzystać z dobrodziejstwa DI.

W celu zaprezentowania działania DI stworzymy prostą aplikację konsolową. Będzie to prosta klasa realizująca wzorzec Repository:

Code:
    public class Person 
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int BirthYear { get; set; }

        public override string ToString()
        {
            return string.Format("{0} {1} {2}", FirstName, LastName, BirthYear);
        }
    }


Code:
    public interface IRepository<T>
    {
        void Add(T entity);
        void Remove(T entity);
        IEnumerable<T> GetElements();
    }


Code:
    public interface IPersonRepository : IRepository<Person>
    {

    }


Code:
    public class PersonRepository : IPersonRepository
    {
        private List<Person> lPersonList = new List<Person>(); 

        public void Add(Person entity)
        {
            lPersonList.Add(entity);
        }

        public void Remove(Person entity)
        {
            lPersonList.Remove(entity);
        }

        public IEnumerable<Person> GetElements()
        {
            return lPersonList;
        }
    }

Funkcja main będzie zawierać następujący kod:

Code:
        static void Main(string[] args)
        {
            var unityContainer = new UnityContainer();
            unityContainer.RegisterType<IPersonRepository, PersonRepository>();

            var personRepository = unityContainer.Resolve<IPersonRepository>();
            personRepository.Add(new Person {FirstName = "Jacek", BirthYear = 2000, LastName = "Kowalski"});
            foreach (var person in personRepository.GetElements())
            {
                Console.WriteLine(person);
            }
        }

Na początku tworzymy nasz kontener. Następnie  następuje rejestracja interfejsu wraz z implementacją. Zabieg ten mówi: jeżeli użytkownik zażąda zwrócenia obiektu implementującego IPersonRepository - użytkownik otrzyma konkretną implementację w postaci obiektu klasy PersonRepository.
Następnie następuje pobranie obiektu na podstawie interfejsu - Resolve. Użytkownikowi zostaje zwrócony obiekt klasy PersonRepository na którym operuje jak na zwykłym obiekcie.

Przykład przedstawiony powyżej nie jest wymyślnym przykładem i nie obrazuje możliwości jakie daje DI w bardziej złożonym projekcie. Dlatego spróbujemy trochę bardziej skomplikować przykład:
Do naszego repozytorium dodamy interfejs odpowiedzialny za obliczanie całkowitego wynagrodzenia na podstawie wymyślonego wzoru:


Code:
    public interface IEducationalYears
    {
        int EducationYears(Person persons);
    }


Code:
    public class EducationalYears : IEducationalYears
    {
        public int EducationYears(Person person)
        {
            int sum = 0;
            int age = DateTime.Now.Year - person.BirthYear;
            sum += age > 18 ? 11 : age - 7;

            return sum;
        }
    }


Code:
    public interface IPersonRepository : IRepository<Person>
    {
        decimal CalculateTotalWage();
    }


Code:
public class PersonRepository : IPersonRepository
    {
        private List<Person> lPersonList = new List<Person>();
        private IEducationalYears _educationalYears;

        public PersonRepository(IEducationalYears educationalYears)
        {
            _educationalYears = educationalYears;
        }

        public void Add(Person entity)
        {
            lPersonList.Add(entity);
        }

        public void Remove(Person entity)
        {
            lPersonList.Remove(entity);
        }

        public IEnumerable<Person> GetElements()
        {
            return lPersonList;
        }

        public decimal CalculateTotalWage()
        {
            decimal sum = decimal.Zero;
            foreach (var person in lPersonList)
            {
                sum += _educationalYears.EducationYears(person) * 125;
            }

            return sum;
        }
    }


Code:
        static void Main(string[] args)
        {
            var unityContainer = new UnityContainer();
            unityContainer.RegisterType<IPersonRepository, PersonRepository>();
            unityContainer.RegisterType<IEducationalYears, EducationalYears>();

            var personRepository = unityContainer.Resolve<IPersonRepository>();
            personRepository.Add(new Person {FirstName = "Jacek", BirthYear = 2000, LastName = "Kowalski"});
            foreach (var person in personRepository.GetElements())
            {
                Console.WriteLine(person);
            }
            Console.WriteLine("Totale wage: {0}", personRepository.CalculateTotalWage());
        }

Jak widać w funkcji main dodaliśmy tylko linijki odpowiedzialne za wyświetlenie wartości wynagrodzenia oraz zarejestrowaliśmy nowy typ w kontenerze.
Dzięki temu nie musimy w konstruktorze podawać konkretnego obiektu. Zaleta ta daje nam niezależność w przypadku rozszerzania funkcjonalności klas. Wystarczy że w konstruktorze dodamy odpowiednie obiekty, a w kontenerze zarejestrujemy nowe typy.

http://pl.wikipedia.org/wiki/Wstrzykiwanie_zale%C5%BCno%C5%9Bci

MEF czy Unity w Prism

Jak wiadomo Microsoft posiada dwa kontenery umożliwiające wykorzystanie od "ręki" możliwości jakie daje Prism. Dla innych bibliotek jak np. Ninject należy odpowiednio zaimplementować własny Bootstrapper co oczywiście nie jest rzeczą trudną i jeżeli będę posiadał trochę wolnego czasu postaram się pokazać.

Tak więc pytanie dla nie lubiących zbytnio kodowania: co użyć MEF czy Unity? Oczywiście odpowiedź nie jest prosta i należy się zastanowić co dają oba kontenery a w czym są różnice. Przeglądając dokumentację Prism można znaleźć informacje o różnicach w nich występujących. Oprócz tych, które znajdują się w podręczniku podam kilka które znalazłem podczas testowania obu tych kontenerów.

Zacznijmy więc od rzeczy które umożliwiają wspólnie i MEF i Unity:
- rejestrują typy w kontenerze
- rejestrują konkretne instancje w kontenerze
- umożliwiają stworzenie instancji stworzonych typów
- umożliwiają wstrzykiwanie instancji poprzez konstruktor
- umożliwiają wstrzykiwanie instancji do właściwości klas
- posiadają atrybuty do oznaczania typów i związków którymi zarządzają obie biblioteki
- obsługują zależności w grafach obiektów (graf obiektu można sobie wyobrazić jako strukturę zależności pomiędzy kolejnymi klasami: np. mamy klasę która w konstruktorze przyjmuje interfejs, a interfejs ten implementuje kolejna klasa posiadająca w swoim konstruktorze inny interfejs itd.)

Rzeczy które oferuje unity a nie MEF:
- potrafi stworzyć instancję niezarejestrowanego typu
- tworzy typy generyczne
- potrafi przechwycić odwołania do obiektu i dodać dodatkowe funkcjonalności do obiektu docelowego
- domyślnie tworzy za każdym razem nową instancję obiektu
- interfejsy muszą być rejestrowane

Co ma MEF a czego nie ma Unity:
- wyszukuje biblioteki w katalogu (automatycznie)
- pobiera pliki XAP i pobierania bibliotek
- reorganizuje właściwości i kolekcje w trakcie odnajdowania bilbiotek
- automatycznie eksportuje odnalezione typy
- standardowo dostarczany wraz z platformą .NET
- domyślnie tworzy obiekty według wzorca Singleton
- nie trzeba niczego rejestrować, wystarczy atrybut Export
- łatwe zarządzanie i tworzenie aplikacji w stylu pluginowym

Tak więc obie biblioteki mają swoje zalety. Można także tworzyć aplikacje wykorzystujące obie biblioteki na raz, np. Unity do operacji DI a MEF do implementowania pluginów.

czwartek, 20 października 2011

Atrybut Conditional

Czasami potrzebujemy aby jakaś metoda została wykonana w przypadku kiedy zajdzie odpowiednia zmiana w systemie użytkownika. Co prawa istnieje możliwość sprawdzania warunkiem if czy możliwe jest wywołanie metody, jednak to powoduje zwiększenie ilości kodu w metodzie.
Możemy tego uniknąć przez zastosowanie atrybutu Conditional.

Dodanie atrybutu Conditional do metody informuje kompilator, że odwołanie do metody nie powinno zostać skompilowane do języka MSIL dopóki symbol powiązany z atrybutem warunku nie zostanie zdefiniowany.
Dodając atrybut Conditional do atrybutu spowoduje że nie zostanie on dodany do metadanych, dopóki odpowiedni symbol powiązany z warunkiem nie zostanie zdefiniowany.
W jaki sposób możemy zdefiniować warunki?
  •  Opcje linii poleceń kompilatora (np. /define:DEBUG) 
  •  Zmienne środowiskowe (set DEBUG=1)
  •  W kodzie (#define DEBUG, #undef DEBUG)

Zobaczmy na prosty przykład:


Code:
using System;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            MyMethod();
            MyMethod2();
        }

        [Conditional("C1")]
        public static void MyMethod()
        {
            Console.WriteLine("Hello world");
        }

        [Conditional("C2")]
        public static void MyMethod2()
        {
            Console.WriteLine("Hello world");
        }
    }
}

Przykład dla linii poleceń kompilatora
We właściwościach projektu definiujemy:


Po wywołaniu otrzymamy na ekranie:


Dzieje się tak dlatego, iż tylko jeden symbol został zdefiniowany

Atrybuty

Atrybuty czyli metadane umożliwiają umożliwiają udostępnienie dodatkowych informacji klasach, konstruktorach, delegatach, typach wyliczeniowych, zdarzeniach, polach, interfejsach, metodach, parametrach, właściwościach, zwracanych wartościach, strukturach lub innych atrybutach. Jak więc widać atrybuty można przypisać właściwie do każdego elementu który znajdziemy w platformie .NET.

Najczęstsze przykłady użycia atrybutów:
  • użycie atrybutów w WebSerwisach do oznaczenia metod które mogą być wywoływane przy użyciu protokołu Soap (np. WebMethod)
  • Oznaczenie w jaki sposób mają być reprezentowane typy dla kodu niezarządzanego
  • Opis assembly: dodanie informacji o autorze, tytule wersji, opisie
  • Oznaczenie klasy do serializacji (za pomocą atrybutu Serializable)
  • Dodawanie informacji o wymaganym poziomie uprawnień przy dostępie do metod
  • Oznaczenie metod które nie powinny być już używane - atrybut Obsolete 
Pisanie własnego atrybutu.
Napisanie własnego atrybutu jest rzeczą bardzo prostą. Przedstawię prosty sposób w jaki można taki atrybut sobie napisać a następnie jak można pobrać informacje o nim w czasie działania programu.

Klasa atrybutu:


Code:
using System;

namespace ConsoleApplication2
{
    public class FriendlyNameAttribute : Attribute
    {
        protected string _friendlyName;
        public string FriendlyName
        {
            get { return _friendlyName; }
            set { _friendlyName = value; }
        }

        protected bool _isUsed;
        public bool IsUsed
        {
            get { return _isUsed; }
            set { _isUsed = value; }
        }

        public FriendlyNameAttribute(string friendlyName)
        {
            FriendlyName = friendlyName;
        }
    }
}

Klasa wykorzystująca atrybut - Person


Code:
namespace ConsoleApplication2
{
    class Person
    {
        [FriendlyName("Imię", IsUsed = true)]
        public string FirstName { get; set; }

        [FriendlyName("Nazwisko")]
        public string LastName { get; set; }

        [FriendlyName("Rok urodzenia")]
        public int BirthYear { get; set; }
    }
}

Funkcja Main:


Code:
using System;
using System.Linq;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            //Get type
            var personType = typeof (Person);
            //Get propeteries
            var propertiesInfo = personType.GetProperties();
            foreach (var propertyInfo in propertiesInfo)
            {
                //Get all attributes for property
                var attributtes = propertyInfo.GetCustomAttributes(true);
                //Selecting FriendlyNameAttribute attributte
                foreach (var friendlyNameAttribute in attributtes.OfType<FriendlyNameAttribute>())
                {
                    Console.WriteLine("{0} = {1}", propertyInfo.Name, friendlyNameAttribute.FriendlyName);
                }
            }
        }
    }
}

Główne kroki programu:
  1. Stworzenie instancji klasy Type dla klasy Person
  2. Pobranie właściwości zadeklarowanych w klasie Person
  3. Przejście po kolekcji właściwości i pobranie dla każdej z nich atrybutów
  4. Instrukcją LINQ pobieramy atrybuty typu FriendlyNameAttribute 
  5. Wyświetlenie nazwy właściwości i wartości pola Name atrybutu FriendlyNameAttribute 
Efekt uruchomienia programu:


Tworzenie atrybutów jest bardzo proste. Wykorzystanie ich jest bardzo szerokie zwłaszcza w takich frameworkach jak:
  • Entity Framework
  • LINQ2SQL
  • ASP.MVC
i wielu innych w których bez skorzystania z atrybutów zmuszałoby nas do tworzenia dużo bardziej skomplikowanego kodu.