poniedziałek, 19 września 2016

Interfejsy

Interfejsy deklarują właściwości, metody oraz zdarzenia, ale właściwa definicja zawartości zostaje przeniesiona na klasę. Mechanizm ten wymusza zachowanie zgodności pomiędzy klasą a interfejsem bez potrzeby implementacji kodu po stronie interfejsu. Dzięki temu interfejs może działać z dowolnym typem referencyjnym, bez ograniczenia do konkretnego typu danych. Słowem kluczowym deklarującym interfejs jest interface. Poniżej znajduje się przykład interfejsu odpowiedzialnego za uruchomienie oraz zatrzymanie, wyposażonego w stan oraz zdarzenie informujące o zmianie stanu.


interface IStartStop
{
    event EventHandler StateChanged;
	
    bool IsOn { get; }

    void Start();

    void Stop();
}

Interfejsy nie mogą określać modyfikatorów dostępności; wszystkie składowe są publiczne. Kolejne ograniczenie to brak możliwości deklaracji konstruktora. Nazwy interfejsów zgodnie z konwencją powinny zaczynać się od dużej litery "I", a sama nazwa powinna być zapisana zgodnie z PascalCasing. Klasa może definiować dowolną liczbę interfejsów, a ich wyliczenie następuje za nazwą klasy po znaku ":".

Przykład poniżej prezentuje użycie wcześniej przygotowanego interfejsu. Klasa WashingMachine definiuje metody Start oraz Stop, które podczas działania wywołują metodę OnStateChanged, odpowiedzialną za propagację zdarzenia StateChanged. Warto zwrócić uwagę na właściwość IsOn, której get'er został zadeklarowany przez interfejs, natomiast set'er został dodany przez klasę wraz z ograniczeniem dostępności.


public class WashingMachine : IStartStop
{
    public event EventHandler StateChanged;

    public string SerialNumber { get; private set; }

    public bool IsOn { get; private set; }

    public WashingMachine(string serialNumber)
    {
        SerialNumber = serialNumber;
    }

    public void Start()
    {
        IsOn = true;
        OnStateChanged(EventArgs.Empty);

        Console.WriteLine("{0} is on = {1}", SerialNumber, IsOn);
    }

    public void Stop()
    {
        IsOn = false;
        OnStateChanged(EventArgs.Empty);

        Console.WriteLine("{0} is on = {1}", SerialNumber, IsOn);
    }

    protected virtual void OnStateChanged(EventArgs e)
    {
        if (StateChanged != null)
        {
            StateChanged(this, e);
        }
    }
}

Zadeklarowaliśmy interfejs, zdefiniowaliśmy go również wewnątrz klasy, teraz sprawdźmy, jak to działa. Wewnątrz metody Main utworzony został obiekt typu WashingMachine. Następnie obsłużone zostało zdarzenie StateChanged, a wywołana metoda Start. Operator is umożliwia zweryfikowanie, czy klasa implementuje wskazany interfejs. Wyrażenie będzie prawdziwe, przez co wykonane zostanie rzutowanie obiektu na interfejs, a następnie uruchomiona metoda Stop.


class Program
{
    static void Main(string[] args)
    {
        var wm1 = new WashingMachine("WM1");
        wm1.StateChanged += (o, e) => {
            Console.WriteLine("State changed");
        };

        wm1.Start();
        
        if(wm1 is IStartStop)
            ((IStartStop)wm1).Stop();

        Console.ReadLine();
    }
}

Interfejsy oferują możliwość użycia składowych zdefiniowanych przez interfejs bez konieczności dostępu do typu. Wyobraźmy sobie sytuację, w której mamy kilka projektów. Przyjmijmy, że główny projekt o nazwie Core udostępnia klasę bazową o nazwie Presenter, która wykorzystywana jest przez klasę UserPresenter w projekcie Administration. Jeżeli pojawi się konieczność dostępu w projekcie Core do składowych klasy UserPresenter, to dzięki interfejsom jesteśmy w stanie udostępnić potrzebne informacje poprzez implementację interfejsów.

Zgodnie z tym, co napisałem wcześniej, implementacja interfejsu wymusza upublicznienie wszystkich składowych, a co, jeżeli nie chcemy, aby wszystko było publiczne? W teorii, interfejsy powinniśmy projektować tak, aby klasa wykorzystywała wszystkie składowe. Jeżeli potrzebna jest dodatkowa weryfikacja lub – co gorsza – wykonanie zaślepki w postaci rzucenia wyjątku typu NotImplementedException, to jest to złamanie zasady Liskov ze zbioru zasad określanych jako SOLID. Tyle teorii. W praktyce bywa różnie, więc jeżeli musimy ukryć składowe interfejsu, możemy skorzystać z techniki implementacji jawnej. Mechanizm pozwala na wskazanie konkretnych składowych interfejsu, które zostaną ukryte. Również w tym przypadku nie określamy modyfikatorów dostępności. W celu implementacji jawnej należy poprzedzić nazwę składowej nazwą interfejsu. W przypadku zdarzeń oprócz podania nazwy interfejsu musimy wstawić zaślepkę.


public class WashingMachine : IStartStop
{
    event EventHandler IStartStop.StateChanged  
    { 
        add { ; } 
        remove { ; }
    }

    public string SerialNumber { get; private set; }

    public bool IsOn { get; private set; }

    public WashingMachine(string serialNumber)
    {
        SerialNumber = serialNumber;
    }

    public void Start()
    {
        IsOn = true;
        Console.WriteLine("{0} is on = {1}", SerialNumber, IsOn);
    }

    void IStartStop.Stop()
    {
        IsOn = false;
        Console.WriteLine("{0} is on = {1}", SerialNumber, IsOn);
    }
}

Po modyfikacji klasy WashingMachine, metoda Stop oraz zdarzenie StateChanged nie są podpowiadane przez IntelliSense. Na potwierdzenie zamieszczam poniżej zrzut ekranu.

Oczywiście istnieją różne sposoby umożliwiające dostęp do przysłoniętych składowych, np. rzutowanie obiektu na interfejs, co zostało zaprezentowane w metodzie MyStop. Zastosowanie metody do obsługi daje możliwość przygotowania wspólnej metody obsługującej różne typy referencyjne implementujące wskazany interfejs.


class Program
{
    static void Main(string[] args)
    {
        var wm2 = new WashingMachine("WM2");

        wm2.Start();
        //wm2.Stop() is not public

        MyStop(wm2);

        Console.ReadLine();
    }

    private static void MyStop(object obj)
    {
        if(obj is IStartStop)
            ((IStartStop)obj).Stop();
    }
}

Podsumowując, zachęcam do tworzenia wyspecjalizowanych interfejsów z niewielką liczbą składowych, zgodnie z zasadą Interface segregation.

Troska Robert