W zeszłym tygodniu na dotNETomaniaku wypromowano artykuł na temat interfejsów. Zapoznałem się z nim i odnoszę wrażenie, że autor nie rozumie w pełni roli interfejsu i myli go z klasą bazową. O taką pomyłkę rzeczywiście nietrudno, wiele klas buduje bowiem swoją funkcjonalność na podstawie interfejsów, ale to nie oznacza automatycznie, że interfejs jest ich przodkiem, zaczynem.

Trzeba zacząć od tego, że choć interfejs umieszcza się w definicji klasy tak samo jak klasę dziedziczoną, to interfejsu się nie dziedziczy. Wszystkim którzy już się palą, aby zaprotestować w komentarzach, proszę o chwilę cierpliwości ;). Zdaję sobie sprawę, że w literaturze pojawiają się tego typu określenia, ale tak samo zdaję sobie sprawę, że autorzy mogą albo chcieć uprościć pewne kwestie, albo – niestety – też traktują umieszczenie interfejsu w definicji klasy jako dziedziczenie. Być może wywodzą się z C++ i dlatego łatwiej jest im taką konstrukcję przyswoić (analogia do dziedziczenia po wielu klasach), jeśli traktują ją w taki sposób.

Więc co tak naprawdę klasa robi z interfejsem? Ona go implementuje. Interfejs jest pewnego rodzaju specyfikacją umiejętności, a klasa – poprzez odwołanie do niego w swojej definicji – potwierdza: tak umiem tak robić, posiadam tę umiejętność. I nie są to bezpodstawne twierdzenia, albowiem kompilator nie pozwoli skompilować klasy, która nie będzie posiadała implementacji metod czy właściwości interfejsu – zobowiązanie w nagłówku klasy musi zostać wypełnione.

Bardzo trafną analogią realizowania (czyli implementacji) interfejsów jest dla mnie scyzoryk armii szwajcarskiej. Istnieją interfejsy: umiem ciąć jak nożyczki, umiem otwierać konserwy, umiem zdejmować kapsle, umiem wyciągać korek, umiem strugać, umiem dziurawić – wszystkie te umiejętności zawarto w jednym scyzoryku. Ta analogia dodatkowo pozwala łatwo zapamiętać, że klasa może implementować nieograniczoną ilość interfejsów, choć dziedziczyć może jedynie po jednej klasie (jest scyzorykiem, czyli nożem rozkładanym).

Wracając do rzeczonego artykułu, to podjęte tam rozważania de facto dotyczą … różnic pomiędzy metodami wirtualnymi i zwykłymi. Choć przewija się w nich interfejs, równie dobrze mogłoby go tam nie być. Należy jednak oddać, że używa się w nich pojęcia implementowania, a nie dziedziczenia interfejsu.

W artykule mamy do czynienia z klasą bazową Animal, która implementuje interfejs IAnimal. Następnie mamy dziedziczącą po niej klasę Cat, która pośrednio także implementuje interfejs, ale niestety – bezskutecznie, albowiem klasa bazowa nie zdefiniowała metod interfejsu jako wirtualnych. Zatem o ile podstawienie tak jednej jak i drugiej klasy pod zmienną typu IAnimal będzie poprawne, o tyle wywołana zostanie zawsze metoda klasy bezpośrednio implementującej interfejs.

Zauważmy, że tak samo stałoby się, gdyby pod zmienną typu Animal podstawić instancję klasy Cat (czyli obiekt). Tu także odwołanie się do bytu traktowanego jako Animal, spowoduje, że wszystkie metody niewirtualne zostaną pobrane z klasy Animal, a nie Cat – no taka już właściwość zwykłych metod.

Autor wskazuje jak poradzić sobie z problem użycia metod Animal w interfejsie IAnimal zamiast metod klasy Cat. Niestety jest to jedynie obejście, bo zadeklarowanie, że Cat też implementuje interfejs (choć robi to już jego klasa bazowa) zamiast zmiany typu metody w klasie bazowej na wirtualną, nie można traktować inaczej. Oczywiście rozwiązanie to rozpatrywane jest w kontekście braku możliwości zmiany typu metody (z jakichś powodów klasa bazowa nie jest dostępna do edycji). Tylko, że wówczas należy zastanowić się nad sensem samego dziedziczenia. Bo jeśli klasa bazowa nie posiada żadnych metod wirtualnych, niezwiązanych z interfejsem – dziedziczenie po niej nie ma sensu, bo i tak nie uda się wykorzystać polimorfizmu, po prostu klasa bazowa jest nierozwijalna. Dziedziczenie po Animal ma sens jedynie, jeśli rozbudowuje się lub zmienia jej funkcjonalności z wykorzystaniem metod wirtualnych. W przeciwnym wypadku lepiej jest po prostu użyć wzorca Adaptera, zagregować klasę Animal, w klasie o takiej samej funkcjonalności (FlexibleAnimal) implementującej interfejs IAnimal, ale wirtualizującej stosowne metody i delegującej ich wywołanie do zagregowanej klasy Animal. Teraz klasa Cat dziedzicząca po klasie FlexibleAnimal, nie musi ponownie deklarować implementowania interfejsu, tylko zaimplementować swoją postać metod wirtualnych. Dowolną z tych klas (FlexibleAnimal, Cat) można teraz przypisać do zmiennej typu IAnimal i cieszyć się z uroków polimorfizmu.

// interfejs jak w oryginale
public interface IAnimal
{
	void Voice();
}

// oryginalna, nierozwijalna klasa bazowa
public class Animal : IAnimal
{
	public void Voice()
	{
		Console.WriteLine("Animal");
	}
}

// klasa rozwiązująca problem klasy bazowej i będąca nową klasą bazową
public class FlexibleAnimal : IAnimal
{
	// pole z instancją nieelastycznej klasy pierwotnej
	private IAnimal PureAnimal = new Animal();
	// nowa metoda interfejsu
	public virtual void Voice()
	{
		PureAnimal.Voice();
	}
}

// klasa potomna, bez sztuczek z ponowną implementacją interfejsu, a działająca zgodnie z oczekiwaniami
public class Cat : FlexibleAnimal 
{
	// metoda nie jest sealed, bo dlaczegóż by nie pozwolić na dziedziczenie kotów?
	public override void Voice()
	{
		Console.WriteLine("Cat");
	}
}

// test
class Program
{
	static void Main(string[] args)
	{
		IAnimal SomeAnimal;
		SomeAnimal = new FlexibleAnimal();
		SomeAnimal.Voice();
		SomeAnimal = new Cat();
		SomeAnimal.Voice();
	}
}

Wynik będzie zgodny z oczekiwaniem:

Animal
Cat