Co autor ma na myśli, przesyłając odniesienie interfejsu do dowolnej implementacji?

17

Jestem aktualnie w procesie próbują opanować C #, więc czytam kod adaptacyjnego poprzez C # przez Gary McLean Hall .

Pisze o wzorach i anty-wzorach. W części Implementacje kontra interfejsy pisze:

Deweloperzy, którzy są nowi w koncepcji programowania interfejsów, często mają trudności z odpuszczeniem tego, co kryje się za interfejsem.

W czasie kompilacji każdy klient interfejsu nie powinien mieć pojęcia, jakiej implementacji interfejsu używa. Taka wiedza może prowadzić do błędnych założeń, które łączą klienta z konkretną implementacją interfejsu.

Wyobraź sobie powszechny przykład, w którym klasa musi zapisać rekord w trwałym magazynie. Aby to zrobić, słusznie deleguje do interfejsu, który ukrywa szczegóły stosowanego mechanizmu trwałego przechowywania. Jednak niewłaściwe byłoby przyjmowanie jakichkolwiek założeń dotyczących tego, która implementacja interfejsu jest używana w czasie wykonywania. Na przykład rzutowanie odwołania interfejsu do dowolnej implementacji jest zawsze złym pomysłem.

Może to być bariera językowa lub brak doświadczenia, ale nie do końca rozumiem, co to oznacza. Oto co rozumiem:

Mam projekt zabawy w wolnym czasie, aby ćwiczyć C #. Tam mam zajęcia:

public class SomeClass...

Ta klasa jest używana w wielu miejscach. Podczas nauki języka C # przeczytałem, że lepiej jest abstrakcji za pomocą interfejsu, więc zrobiłem następujące

public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.

public class SomeClass : ISomeClass <- Same as before. All implementation here.

Więc przejrzałem wszystkie odniesienia do klas i zastąpiłem je ISomeClass.

Z wyjątkiem konstrukcji, w której napisałem:

ISomeClass myClass = new SomeClass();

Czy dobrze rozumiem, że to źle? Jeśli tak, dlaczego tak i co powinienem zrobić zamiast tego?

Marshall
źródło
25
Nigdzie w twoim przykładzie nie rzutujesz obiektu typu interfejsu na typ implementacji. Przypisujesz coś typu implementacyjnego do zmiennej interfejsu, co jest całkowicie poprawne i poprawne.
Caleth,
1
Co masz na myśli „w konstruktorze, w którym napisałem ISomeClass myClass = new SomeClass();? Jeśli tak naprawdę masz na myśli, to jest to rekurencja w konstruktorze, prawdopodobnie nie to, czego chcesz. Mam nadzieję, że masz na myśli w„ konstrukcji ”, to znaczy alokacji, ale nie w samym konstruktorze, prawda ?
Erik Eidt,
@Erik: Tak. W budowie. Masz rację. Poprawi pytanie. Dzięki
Marshall,
Ciekawostka: F # ma pod tym względem lepszą historię niż C # - eliminuje niejawne implementacje interfejsu, więc gdy chcesz wywołać metodę interfejsu, musisz przejść na typ interfejsu. To wyjaśnia, kiedy i jak używasz interfejsów w kodzie, i sprawia, że ​​programowanie interfejsów jest znacznie bardziej zakorzenione w języku.
scrwtp
3
To trochę nie na temat, ale myślę, że autor źle zdiagnozował problem, jaki mają nowi ludzie w tej koncepcji. Moim zdaniem problem polega na tym, że ludzie nowi w koncepcji nie wiedzą, jak tworzyć dobre interfejsy. Bardzo łatwo jest tworzyć zbyt specyficzne interfejsy, które w rzeczywistości nie zapewniają żadnej ogólności (co może się zdarzyć ISomeClass), ale łatwo jest także tworzyć zbyt ogólne interfejsy, dla których nie można napisać przydatnego kodu, na którym to etapie jedyne opcje są na nowo przemyśleć interfejs i przepisać kod lub obniżyć.
Derek Elkins opuścił SE

Odpowiedzi:

37

Przekształcenie twojej klasy w interfejs jest czymś, co powinieneś rozważyć tylko wtedy, gdy masz zamiar napisać inne implementacje tego interfejsu lub istnieje duża możliwość takiego uczynienia w przyszłości.

Może SomeClassi ISomeClassjest złym przykładem, ponieważ byłoby to jak posiadanie OracleObjectSerializerklasy i IOracleObjectSerializerinterfejsu.

Bardziej dokładnym przykładem byłoby coś takiego jak OracleObjectSerializeri IObjectSerializer. Jedynym miejscem w twoim programie, w którym zależy Ci na implementacji, jest tworzenie instancji. Czasami jest to dalej oddzielane przy użyciu wzoru fabrycznego.

Wszędzie w twoim programie nie powinno się IObjectSerializertroszczyć o to, jak to działa. Załóżmy przez chwilę, że oprócz tego masz także SQLServerObjectSerializerimplementację OracleObjectSerializer. Załóżmy teraz, że musisz ustawić specjalną właściwość do ustawienia, a ta metoda jest obecna tylko w OracleObjectSerializer, a nie SQLServerObjectSerializer.

Można to zrobić na dwa sposoby: niewłaściwy sposób i podejście oparte na zasadzie substytucji Liskowa .

Niepoprawny sposób

Niepoprawnym sposobem i samą instancją, o której mowa w książce, byłoby pobranie instancji IObjectSerializeri przesłanie jej, OracleObjectSerializera następnie wywołanie metody setPropertydostępnej tylko na OracleObjectSerializer. Jest to złe, ponieważ nawet jeśli znasz instancję OracleObjectSerializer, wprowadzasz jeszcze jeden punkt w swoim programie, w którym chcesz wiedzieć, co to za implementacja. Kiedy ta implementacja ulegnie zmianie i przypuszczalnie stanie się to wcześniej czy później, jeśli masz wiele implementacji, najlepszym scenariuszem, będziesz musiał znaleźć wszystkie te miejsca i dokonać odpowiednich poprawek. W najgorszym scenariuszu rzutujesz IObjectSerializerinstancję na a OracleObjectSerializeri pojawia się awaria środowiska wykonawczego podczas produkcji.

Liskov Podstawowa zasada podejścia

Liskov powiedział, że nigdy nie powinieneś potrzebować metod takich jak setPropertyw klasie implementacji, jak w przypadku mojej, OracleObjectSerializerjeśli wykonano to poprawnie. Jeśli abstrakcyjne klasy OracleObjectSerializerdo IObjectSerializer, należy objąć wszystkie metody niezbędne do korzystania z tej klasy, a jeśli nie, to coś jest nie tak z abstrakcją (próbuje zrobić Dogpracę klasy jako IPersonrealizacji na przykład).

Prawidłowym podejściem byłoby zapewnienie setPropertymetody IObjectSerializer. Podobne metody SQLServerObjectSerializernajlepiej działałyby dzięki tej setPropertymetodzie. Co więcej, standaryzujesz nazwy właściwości poprzez miejsce, w Enumktórym każda implementacja tłumaczy ten wyliczenie na ekwiwalent dla własnej terminologii bazy danych.

Mówiąc prosto, użycie a ISomeClassto tylko połowa tego. Nigdy nie powinieneś rzucać go poza metodę odpowiedzialną za jego tworzenie. Jest to prawie na pewno poważny błąd projektowy.

Neil
źródło
1
Wydaje mi się, że jeśli masz zamiar rzucić IObjectSerializersię OracleObjectSerializerdlatego, że „wie”, że jest to co to jest, to powinieneś być uczciwym wobec siebie (i co ważniejsze, z innymi, którzy mogą utrzymać ten kod, który może obejmować swoją przyszłość siebie) i używaj OracleObjectSerializergo od miejsca, w którym został utworzony, do miejsca, w którym jest używany. To sprawia, że ​​jest to bardzo publiczne i jasne, że wprowadzasz zależność od konkretnej implementacji - a praca i brzydota związana z robieniem tego staje się sama w sobie silną wskazówką, że coś jest nie tak.
KRyan,
(A jeśli z jakiegoś powodu naprawdę nie muszą polegać na konkretnej implementacji, to staje się o wiele bardziej oczywiste, że to co robisz i że robisz to w sposób zamierzony i cel. To „powinno” nigdy zdarzyć oczywiście i 99% razy może się wydawać, że tak się dzieje, że tak naprawdę nie jest i powinieneś to naprawić, ale nic nie jest nigdy w 100% pewne, czy tak powinno być.)
KRyan 11.09.17
@KRyan Absolutnie. Abstrakcji należy używać tylko wtedy, gdy jest to potrzebne. Używanie abstrakcji, gdy nie jest konieczne, służy jedynie do nieco trudniejszego zrozumienia kodu.
Neil
29

Przyjęta odpowiedź jest poprawna i bardzo przydatna, ale chciałbym krótko omówić konkretnie wiersz kodu, o który pytałeś:

ISomeClass myClass = new SomeClass();

Ogólnie rzecz biorąc, to nie jest okropne. W miarę możliwości należy unikać:

void someMethod(ISomeClass interface){
    SomeClass cast = (SomeClass)interface;
}

Tam, gdzie twój kod ma interfejs zewnętrzny, ale wewnętrznie przekazuje go do konkretnej implementacji, „Ponieważ wiem, że będzie to tylko ta implementacja”. Nawet jeśli okazało się to prawdą, używając interfejsu i rzutując go na implementację, dobrowolnie rezygnujesz z bezpieczeństwa typu rzeczywistego tylko po to, abyś mógł udawać, że używasz abstrakcji. Jeśli ktoś miałby później popracować nad kodem i zobaczył metodę, która akceptuje parametr interfejsu, to założą, że jakakolwiek implementacja tego interfejsu jest prawidłową opcją do przekazania. wiersz po zapomnieniu, że określona metoda polega na tym, jakich parametrów potrzebuje. Jeśli kiedykolwiek poczujesz potrzebę przesyłania z interfejsu do konkretnej implementacji, wówczas interfejs, implementacja, lub kod, który się do nich odwołuje, jest niepoprawnie zaprojektowany i powinien ulec zmianie. Na przykład, jeśli metoda działa tylko wtedy, gdy przekazany argument jest określoną klasą, parametr powinien akceptować tylko tę klasę.

Teraz, patrząc wstecz na twoje wywołanie konstruktora

ISomeClass myClass = new SomeClass();

problemy z rzutowaniem tak naprawdę nie mają zastosowania. Nic z tego nie wydaje się być narażone na zewnątrz, więc nie wiąże się z tym żadne szczególne ryzyko. Zasadniczo ten wiersz kodu sam w sobie jest szczegółem implementacji, którego interfejsy zostały zaprojektowane tak, aby na początku były abstrakcyjne, więc zewnętrzny obserwator widziałby, że działa tak samo, niezależnie od tego, co robią. Jednak to również NIE ZYSKUJE z istnienia interfejsu. Twój myClassma typ ISomeClass, ale nie ma żadnego powodu, ponieważ zawsze ma przypisaną określoną implementację,SomeClass. Istnieją pewne drobne potencjalne zalety, takie jak możliwość zamiany implementacji w kodzie przez zmianę tylko wywołania konstruktora lub ponowne przypisanie tej zmiennej później do innej implementacji, ale chyba że jest coś innego, co wymaga wpisania zmiennej do interfejsu zamiast implementacja tego wzorca sprawia, że ​​Twój kod wygląda, jakby interfejsy były używane tylko przez rote, a nie z faktycznego zrozumienia korzyści płynących z interfejsów.

Kamil Drakari
źródło
1
Apache Math robi to z Cartesian3D Source, linia 286 , co może być naprawdę denerwujące.
J_F_B_M,
1
To jest właściwie poprawna odpowiedź, która odnosi się do pierwotnego pytania.
Benjamin Gruenbaum,
2

Myślę, że łatwiej jest pokazać kod na złym przykładzie:

public interface ISomeClass
{
    void DoThing();
}

public class SomeClass : ISomeClass
{
    public void DoThing()
    {
       // Mine for BitCoin
    }

}

public class AnotherClass : ISomeClass
{
    public void DoThing()
    {
        // Mine for oil
    }
    public Decimal Depth;
 }

 void main()
 {
     ISomeClass task = new SomeClass();

     task.DoThing(); //  This is good

     Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
 }

Problem polega na tym, że kiedy początkowo piszesz kod, prawdopodobnie istnieje tylko jedna implementacja tego interfejsu, więc rzutowanie będzie nadal działać, po prostu w przyszłości możesz zaimplementować inną klasę, a następnie (jak pokazuje mój przykład) spróbuj uzyskać dostęp do danych, które nie istnieją w używanym obiekcie.

Neil
źródło
Cześć, proszę pana. Ktoś powiedział ci kiedyś, jaki jesteś przystojny?
Neil
2

Dla jasności zdefiniujmy casting.

Przesyłanie jest siłą przekształcające coś z jednego typu do drugiego. Typowym przykładem jest rzutowanie liczby zmiennoprzecinkowej na liczbę całkowitą. Podczas rzutowania można określić konkretną konwersję, ale domyślnie jest to po prostu reinterpretacja bitów.

Oto przykład przesyłania z tej strony dokumentacji Microsoft .

// Create a new derived type.  
Giraffe g = new Giraffe();  

// Implicit conversion to base type is safe.  
Animal a = g;  

// Explicit conversion is required to cast back  
// to derived type. Note: This will compile but will  
// throw an exception at run time if the right-side  
// object is not in fact a Giraffe.  
Giraffe g2 = (Giraffe) a;  

Ty mógłby zrobić to samo i oddać coś, implementuje interfejs do konkretnej implementacji tego interfejsu, ale nie powinno , bo to doprowadzi do błędu lub nieoczekiwanego zachowania, jeśli używana jest inna realizacja niż można się spodziewać.

Ryan1729
źródło
1
„Przesyłanie przekształca coś z jednego typu do drugiego”. - Nie. Casting wyraźnie konwertuje coś z jednego typu na inny. (W szczególności „rzutowanie” to nazwa składni używanej do określania tej konwersji.) Konwersje niejawne nie są rzutowane. „Podczas rzutowania można określić konkretną konwersję, ale domyślnie jest to po prostu reinterpretacja bitów”. -- Zdecydowanie nie. Istnieje wiele konwersji, zarówno domyślnych, jak i jawnych, które wymagają znacznych zmian w wzorcach bitów.
hvd,
@hvd Dokonałem teraz poprawki dotyczącej jawności przesyłania. Kiedy powiedziałem, że domyślnie jest po prostu reinterpretacja bitów, próbowałem wyrazić, że jeśli chcesz stworzyć swój własny typ, to w przypadkach, w których rzutowanie jest automatycznie definiowane, gdy rzutujesz go na inny typ, bity zostaną ponownie zinterpretowane . W powyższym przykładzie Animal/ Giraffe, jeśli miałbyś to zrobić, Animal a = (Animal)g;bity zostałyby ponownie zinterpretowane (wszelkie dane specyficzne dla Giraffe byłyby interpretowane jako „nie będące częścią tego obiektu”).
Ryan1729,
Pomimo tego, co mówi hvd, ludzie bardzo często używają terminu „obsada” w odniesieniu do niejawnych konwersji; patrz np . https://www.google.com/search?q="implicit+cast"&tbm=bks . Technicznie uważam, że bardziej poprawne jest rezerwowanie terminu „obsada” dla jawnych konwersji, pod warunkiem, że nie pomylisz się, gdy inni używają go inaczej.
ruakh
0

Moje 5 centów:

Wszystkie te przykłady są OK, ale nie są przykładami ze świata rzeczywistego i nie pokazały prawdziwych zamiarów świata.

Nie znam C #, więc dam abstrakcyjny przykład (mieszanka Java i C ++). Mam nadzieję, że to w porządku.

Załóżmy, że masz interfejs iList:

interface iList<Key,Value>{
   bool add(Key k, Value v);
   bool remove(Element e);
   Value get(Key k);
}

Załóżmy teraz, że istnieje wiele implementacji:

  • DynamicArrayList - używa płaskiej tablicy, szybko wstawia i usuwa na końcu.
  • LinkedList - wykorzystuje podwójnie połączoną listę, szybko wstawiając z przodu i na końcu.
  • AVLTreeList - używa drzewa AVL, szybko robi wszystko, ale zużywa dużo pamięci
  • SkipList - używa SkipList, szybko robi wszystko, wolniej niż Drzewo AVL, ale zużywa mniej pamięci.
  • HashList - używa HashTable

Można pomyśleć o wielu różnych implementacjach.

Załóżmy teraz, że mamy następujący kod:

uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);

Wyraźnie pokazuje naszą intencję, z której chcemy skorzystać iList. Jasne, że nie jesteśmy już w stanie wykonywać DynamicArrayListokreślonych operacji, ale potrzebujemy iList.

Rozważ następujący kod:

iList list = factory.getList();

Teraz nawet nie wiemy, czym jest wdrożenie. Ten ostatni przykład jest często używany w przetwarzaniu obrazu, gdy ładujesz jakiś plik z dysku i nie potrzebujesz jego typu (gif, jpeg, png, bmp ...), ale wszystko, czego potrzebujesz, to manipulacja obrazem (przerzucanie, skalę, zapisz jako png na końcu).

Nacięcie
źródło
0

Masz interfejs ISomeClass i obiekt myObject, o którym nic nie wiesz ze swojego kodu poza tym, że zadeklarowano go do implementacji ISomeClass.

Masz klasę SomeClass, która, jak wiesz, implementuje interfejs ISomeClass. Wiesz o tym, ponieważ zadeklarowano wdrożenie ISomeClass lub sam zaimplementowałeś go, aby zaimplementować ISomeClass.

Co jest złego w przesyłaniu myClass do SomeClass? Dwie rzeczy są złe. Po pierwsze, naprawdę nie wiesz, że myClass to coś, co można przekonwertować na SomeClass (instancja SomeClass lub podklasa SomeClass), więc rzutowanie może się nie powieść. Po drugie, nie powinieneś tego robić. Powinieneś pracować z myClass zadeklarowaną jako iSomeClass i używać metod ISomeClass.

Punktem, w którym dostajesz obiekt SomeClass, jest wywołanie metody interfejsu. W pewnym momencie wywołujesz myClass.myMethod (), która jest zadeklarowana w interfejsie, ale ma implementację w SomeClass i oczywiście w wielu innych klasach implementujących ISomeClass. Jeśli wywołanie kończy się w kodzie SomeClass.myMethod, to wiesz, że self jest instancją SomeClass, i w tym momencie jest absolutnie w porządku i właściwie jest poprawne użycie go jako obiektu SomeClass. Oczywiście, jeśli faktycznie jest to instancja OtherClass, a nie SomeClass, to nie dojdziesz do kodu SomeClass.

gnasher729
źródło