Interfejsy: implementacja jawna vs. niejawna

W C# można implementować interfejsy na dwa sposoby: jawny oraz niejawny. Rozważmy poniższy interfejs:

interface ISerializable
{
    void Serialize(string path);
}

Implementacja jawna:

class ExplicitImplementation:ISerializable
{

    #region ISerializable Members

    void ISerializable.Serialize(string path)
    {
        throw new NotImplementedException();
    }

    #endregion
}

Implementacja niejawna:

class ImplicitImplementation:ISerializable
{
    #region ISerializable Members

    public void Serialize(string path)
    {
        throw new NotImplementedException();
    }

    #endregion
}

Spotkałem się z dwoma przeciwstawnymi opiniami. Według jednej należy unikać implementacji jawnej a według drugiej powinna ona być traktowana jako domyślna i tylko w specyficznych sytuacjach powinno korzystać się  z niejawnej.

W poście chciałbym przedstawić za i przeciw dwóch podejść. W końcu skoro c# dostarcza dwa sposoby muszą istnieć jakieś zalety i wady obu rozwiązań.  Podejście jawne przede wszystkim jest niezbędne gdy klasa musi zaimplementować dwa interfejsy, które posiadają metody o takich samych sygnaturach a implementacje wyglądają inaczej. Przykład:

interface IXmlSerializable
{
    void Serialize(string path);
}
interface IBinarySerializable
{
    void Serialize(string path);
}

class ExplicitImplementation:IXmlSerializable,IBinarySerializable
{
    void IXmlSerializable.Serialize(string path)
    {        
    }

    void IBinarySerializable.Serialize(string path)
    {        
    }
}

Nie ma możliwości dostarczenia dwóch różnych implementacji jeśli wybierze się podejście niejawne. Pod tym względem, implementacja jawna niewątpliwie wygrywa z niejawną.

Kolejnym argumentem za jawną implementacją jest fakt, że nie można takich metod wywoływać inaczej niż z poziomu interfejsu. Przykład:

var explicitImplementation=new ExplicitImplementation();
explicitImplementation.Serialize();// blad
IXmlSerializable serializable = explicitImplementation;
serializable.Serialize(); // OK

Aby wywołać metodę zaimplementowaną w sposób jawny, zawsze trzeba najpierw ją zrzutować na interfejs. Ma to jednak jedną zaletę. Załóżmy, że pierwotna wersja interfejsu ma dwie metody:

interface IBinarySerializable
{
    void Serialize(string path);
    void Deserialize(string path);
}
class ImplicitImplementation:IBinarySerializable
{
    public void Serialize(string path)
    {
        throw new NotImplementedException();
    }
    public void Deserialize(string path)
    {
        throw new NotImplementedException();
    }
}

Po jakimś czasie jedna z metod interfejsu została usunięta (w tym przypadku akurat to nie ma sensu…) ponieważ nigdy nie była używana i interfejs wygląda następująco:

interface IBinarySerializable
{
    void Serialize(string path);
}

Co się dzieje? Kod nadal kompiluje się  mimo, że wciąż wszystkie klasy zawierają implementacje Deserialize. Gdyby wszystkie klasy implementowały Deserialize w sposób jawny, wtedy usunięcie metody z interfejsu wiązałoby się z koniecznością usunięcia wszystkich implementacji – nie pozostałby nigdzie martwy kod. Nie można po prostu skompilować kodu, gdzie metoda jest implementowana w sposób jawny a nie istnieje ona w interfejsie. Usuwanie metod jest oczywiście brzydkie (niezgodne z open\closed principle) ale czasami nie ma innego wyjścia.

Kolejną zaletą wymogu wywoływania przez interfejs jest promowanie izolacji interfejsu od implementacji. Na przykład gdyby metody były zaimplementowane w sposób niejawny wtedy często popełnianym błędem jest:

var serializer = new ImplicitImplementation();

W tym przypadku var jest typu ImplicitImplementation. Zgodnie z dobrymi praktykami powinno używać się czegoś bardziej abstrakcyjnego np. interfejsu. Z tego względu dużo lepiej jest:

IXmlSerializable serializator = ...

Należy operować na interfejsie a specyficzna implementacja powinna zostać wstrzyknięta np. przez konstruktor. Jawna implementacja interfejsu promuje takie podejście ponieważ nie da się tych metod wywołać bezpośrednio z klasy.

Niestety jawna implementacja ma również wady. Najważniejsza z nich to boxing, który ma miejsce dla value type. W poprzednim poście pisałem, że struktury również mogą implementować interfejsy. Pokazałem wtedy jedynie niejawne podejście jednak “nic” nie stoi na przeszkodzie aby wykonać to w sposób jawny:

struct SampleStruct:IBinarySerializable
{
    #region IBinarySerializable Members

    void IBinarySerializable.Serialize(string path)
    {
        throw new NotImplementedException();
    }

    #endregion
}

W takiej sytuacji jedyny sposób na wykonanie metody Serialzie to zrzutowanie struktury na interfejs, co powoduje oczywiście boxing:

IBinarySerializable binarySerializable = new SampleStruct();
binarySerializable.Serialize(path);

Inny argument przeciwko to fakt, że nie jest to zbyt intuicyjne. W końcu skoro klasa implementuje interfejs to naturalne jest, że oczekuje się od niej, że jest w stanie wykonać logikę opisaną przez interfejs. Bez dobrej dokumentacji użytkownikom ciężko będzie korzystać z takiej funkcjonalności. Niektóre typy  w .NET Framework potwierdzają tą regułę. Na przykład, jak skonwertować integer na char? Czy poniższy kod zadziała?

int integer = 5;
char character = integer.ToChar(null);

Niestety nie, ponieważ Int32 implementuje ToChar w sposób jawny i należy najpierw zrzutować obiekt na IConvertible:

int integer = 5;
char character = ((IConvertible) integer).ToChar(null);

Trzeba przyznać, że jest to naprawdę mało intuicyjne i na dodatek powoduje niechciany i niepotrzebny boxing. Kolejnym argumentem przeciw są utrudnienia w wykorzystywaniu interfejsu w klasach pochodnych tj.:

class ExplicitImplementation:IBinarySerializable
{
    #region IBinarySerializable Members

    void IBinarySerializable.Serialize(string path)
    {
        throw new NotImplementedException();
    }

    #endregion
}
class DerivedClass:ExplicitImplementation
{
    public void WrapSerialize()
    {
        IBinarySerializable baseClass = this;
        baseClass.Serialize(null);
    }
}

Prawda, że  tworzenie nowego wskaźnika aby wykonać metodę bazową, to trochę za wiele? Osobiście używam jawnych interfejsów w sytuacjach gdy dane metody są wykorzystywane tylko w jednym kontekście. Mam na myśli sytuacje, w której interfejs jest zaimplementowany tylko po to aby dostosować klasę do jakiegoś innego API– innymi słowy nie dodaje on nowej logiki a tylko eksponuje aktualną w inny sposób. W sytuacjach gdy interfejs to część opisywanej logiki wtedy preferuje niejawne podejście.

2 thoughts on “Interfejsy: implementacja jawna vs. niejawna”

  1. Implementacja jawna i niejawna się nie różnią 🙂 Chyba w niejawnej nie miało być ISerializable.Serialize() tylko ISerializable.Serialize(). Ogólnie proponuję jeszcze raz przejrzeć artykuł. Przeczytałem bardzo pobieżnie, ale np. “Aby wywołać metodę zaimplementowaną w sposób jawny, zawsze trzeba najpierw ją zrzutować na interfejs. Ma to jednak jedną zaletę. Załóżmy, że pierwotna wersja interfejsu ma dwie metody:” Potem jest opis, który wydaje mi się, że omawia wadę jawnej implementacji.

    Tak poza tym to bardzo ciekawy artykuł i zgadzam się, że generalnie lepiej używać niejawnych implementacji intefejsu 🙂

    Na marginesie to można jeszcze dodać informację związaną z polimorfizmem… Otóż w przypadku implemnetacji jawnej nie ma w ogóle sensu oznaczania metod jako wirtualnych, ponieważ i tak musimy rzutować obiekt do intefejsu. Z kolei w przypadku standardowych musimy pamiętać, że wykorzystanie samego interfejsu nie sprawia, że nasze metody są wirtualne. Jeśli się mylę to proszę o poprawkę.

    Pozdrawiam,
    Robert

    PS. Dziękuję za prowadzenie bloga 🙂

  2. @Pellared:
    Hey, faktycznie zly kod wkleiłem. Dzieki za info. A co do drugiej uwagi to omawiawiam zalete jaka jest unikanie martwego kodu wiec jest OK:)

Leave a Reply

Your email address will not be published.