Dlaczego programista chciałby oddzielić implementację od interfejsu?

18

Wzorzec projektowy mostka oddziela implementację od interfejsu programu.

Dlaczego to jest korzystne?

David Faux
źródło
Aby zapobiec nieszczelności abstrakcji
SK-logic
1
Wydaje się, że wszyscy już to rozumieją, ale dla nowych widzów Bridge Design nie dotyczy implementacji i interfejsu „programu”, ale części, być może nawet małych części programu. Cały program będzie zbiorem interfejsów i implementacji (każdy interfejs ma jedną lub więcej implementacji). Pierwszym zdaniem powinno być: „Wzorzec projektu mostu oddziela interfejsy od ich implementacji w całym kodzie źródłowym”.
RalphChapin

Odpowiedzi:

35

Pozwala zmienić implementację niezależnie od interfejsu. Pomaga to poradzić sobie ze zmieniającymi się wymaganiami.

Klasycznym przykładem jest zastąpienie implementacji pamięci w interfejsie czymś większym, lepszym, szybszym, mniejszym lub innym innym bez konieczności zmiany reszty systemu.

Daniel Pittman
źródło
23

Oprócz odpowiedzi Daniela, oddzielenie interfejsu od implementacji poprzez pojęcia takie jak polimorfizm pozwala na stworzenie kilku implementacji tego samego interfejsu, które robią podobne rzeczy na różne sposoby.

Na przykład wiele języków ma pojęcie strumienia gdzieś w standardowej bibliotece. Strumień to coś, co przechowuje dane do dostępu szeregowego. Ma dwie podstawowe operacje: Odczytywanie (ładowanie następnej liczby bajtów X ze strumienia) i Zapisywanie (dodawanie liczby X bajtów danych do strumienia), a czasami trzecią - Wyszukiwanie (resetowanie „bieżącej pozycji” strumienia do nowej lokalizacji).

To dość prosta koncepcja, ale pomyśl o wszystkich rzeczach, które możesz z tym zrobić. Najbardziej oczywistym z nich jest interakcja z plikami na dysku. Strumień plików umożliwia odczyt danych z pliku lub zapis do niego. A jeśli zamiast tego chcesz przesyłać dane przez połączenie sieciowe?

Jeśli polegałeś bezpośrednio na implementacjach, musiałbyś napisać dwie zupełnie różne procedury, aby zapisać te same dane w pliku lub wysłać je przez sieć. Ale jeśli masz interfejs strumieniowy, możesz utworzyć dwie różne jego implementacje ( FileStreami NetworkStream), które zawierają szczegółowe szczegóły wysyłania danych tam, gdzie muszą się udać, a następnie wystarczy napisać kod, który zajmuje się zapisaniem pliku tylko raz . Nagle twoje SaveToFilei SendOverNetworkprocedury stają się o wiele prostsze: po prostu konfigurują strumień odpowiedniego typu i przekazują go do SaveDataprocedury, która akceptuje interfejs strumienia - nie musi dbać o to, jakiego typu, o ile może wykonać Operacja zapisu - i zapisuje dane w strumieniu.

Oznacza to również, że jeśli zmieni się format danych, nie trzeba go zmieniać w wielu różnych miejscach. Jeśli scentralizujesz kod zapisywania danych w jednej procedurze, która pobiera strumień, to jest to jedyne miejsce, które musi zostać zaktualizowane, abyś nie mógł przypadkowo wprowadzić błędu, zmieniając tylko jedno miejsce, gdy trzeba było zmienić oba. Tak więc oddzielenie interfejsów od implementacji i użycie polimorfizmu sprawia, że ​​kod jest łatwiejszy do odczytania i zrozumienia, i rzadziej zawiera błędy.

Mason Wheeler
źródło
1
Jest to jedna z najsilniejszych cech OOP. Po prostu to uwielbiam ...
Radu Murzea
1
Czym różni się ta forma przy użyciu zwykłych interfejsów?
vainolo,
@Vainolo: Pomaga w ponownym użyciu kodu. Nawet jeśli masz wiele rodzajów strumieni, wszystkie one wykonają wiele takich samych rzeczy. Jeśli zaczynasz od IStreaminterfejsu, musisz na nowo wymyślić cały zestaw funkcji dla każdego strumienia. Ale jeśli zaczniesz od podstawowej abstrakcyjnej klasy strumienia, może ona pomieścić wszystkie wspólne stany i funkcje, a następnie pozwolić potomnym na wdrożenie różnych funkcji.
Mason Wheeler
2
@RaduMurzea nie jest to specyficzne dla OOP. Klasy typów pozwalają ci robić to samo w sposób całkowicie bez OOP.
Wes
@ Dokładnie, chciałem tylko powiedzieć to samo, a potem przeczytałem twój komentarz.
jhegedus
4

Naprawdę masz tutaj dwa bardzo różne pytania, choć są one powiązane.

Bardziej ogólne pytanie brzmi: w tytule, dlaczego ogólnie oddzielasz interfejs od implementacji. Drugie pytanie dotyczy tego, dlaczego wzór mostu jest użyteczny. Są ze sobą powiązane, ponieważ wzorzec mostka jest specyficznym sposobem oddzielenia interfejsu od implementacji, co ma również pewne inne konsekwencje.

Ogólne pytanie jest czymś niezbędnym do zrozumienia przez każdego programistę. To właśnie zapobiega rozprzestrzenianiu się zmian w programie wszędzie. Nie wyobrażam sobie ludzi, którzy mogliby programować bez tego.

Kiedy piszesz prostą instrukcję dodawania w języku programowania, która jest już abstrakcją (nawet jeśli nie używa przeciążania operatora do dodawania macierzy lub czegoś podobnego), która przechodzi całkiem sporo innego kodu, zanim w końcu zostanie wykonany w obwodzie w komputerze. Jeśli nie było oddzielenia interfejsu (powiedzmy „3 + 5”) od implementacji (wiązka kodu maszynowego), musiałbyś zmieniać kod za każdym razem, gdy implementacja się zmieniała (tak jakbyś chciał uruchomić na nowy procesor).

Nawet w prostej aplikacji CRUD każda sygnatura metody jest, w szerokim znaczeniu, interfejsem do jej implementacji.

Wszystkie te rodzaje abstrakcji mają ten sam podstawowy cel - niech kod wywołujący wyraża swoją intencję w możliwie najbardziej abstrakcyjny sposób, który zapewnia implementatorowi tyle informacji, ile potrzeba. Zapewnia to minimalne możliwe sprzężenie między nimi i ogranicza efekt tętnienia, gdy kod musi zostać zmieniony w jak największym stopniu.

Brzmi prosto, ale w praktyce komplikuje się.

Wzorzec mostu jest szczególnym sposobem rozdzielenia niektórych bitów implementacji na interfejsy. Schemat klasowy wzorca jest bardziej pouczający niż opis. To raczej sposób na posiadanie modułów wtykowych niż mostek, ale nazwali go mostem, ponieważ jest często używany tam, gdzie moduły zostały utworzone przed interfejsem. Tak więc utworzenie wspólnego interfejsu dla podobnych istniejących implementacji stanowi „pomost” różnicy i umożliwia kodowanie pracy z dowolnymi implementacjami.

Powiedzmy, że chcesz napisać dodatek do edytora tekstu, ale chcesz, aby działał na wielu edytorach tekstu. Możesz utworzyć interfejs, który wyodrębnia funkcje oparte na edytorze tekstów, których potrzebujesz (i które muszą być możliwe do wdrożenia przez każdy edytor tekstu, ponieważ nie możesz ich zmienić) oraz jeden implementator tego interfejsu dla każdego edytora tekstu, który chcesz obsługiwać. Następnie aplikacja może wywołać ten interfejs i nie martwić się o szczegóły każdego edytora tekstu.

W rzeczywistości jest nieco bardziej szczegółowy, ponieważ każda klasa może naprawdę być hierarchią klas (więc może istnieć nie tylko abstrakcyjny edytor tekstu, ale abstrakcyjny dokument, abstrakcyjny wybór tekstu itp., Z konkretnymi implementacjami dla każdej), ale jest ten sam pomysł.

To trochę jak fasada, z tym wyjątkiem, że w tym przypadku warstwa abstrakcji koncentruje się na zapewnieniu tego samego interfejsu wielu bazowym systemom.

Jest to związane z Inversion Of Control, ponieważ konkretny implementator zostanie przekazany do metod lub konstruktorów i określi rzeczywistą wywoływaną implementację.

psr
źródło