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.
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 🙂
@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:)