Zrozumienie „programowania do interfejsu”

30

Często spotykałem się z terminem „programowanie interfejsu zamiast implementacji” i myślę, że rozumiem, co to znaczy. Ale chcę się upewnić, że rozumiem, jakie są korzyści i możliwe wdrożenia.

„Programowanie interfejsu” oznacza, że ​​tam, gdzie to możliwe, należy odwoływać się do bardziej abstrakcyjnego poziomu klasy (interfejs, klasa abstrakcyjna, a czasem do jakiejś nadklasy), zamiast do konkretnej implementacji.

Typowym przykładem w Javie jest użycie:

List myList = new ArrayList();zamiast ArrayList myList = new ArrayList();.

Mam dwa pytania dotyczące tego:

  1. Chcę się upewnić, że rozumiem główne zalety tego podejścia. Myślę, że korzyści to przede wszystkim elastyczność. Zadeklarowanie obiektu jako odniesienia wyższego poziomu, a nie konkretnej implementacji, pozwala na większą elastyczność i łatwość konserwacji w całym cyklu programowania i w całym kodzie. Czy to jest poprawne? Czy elastyczność jest główną korzyścią?

  2. Czy istnieje więcej sposobów „programowania do interfejsu”? Czy też „deklarowanie zmiennej jako interfejsu, a nie konkretnej implementacji” jest jedyną implementacją tej koncepcji?

Nie mówię o interfejsie konstruktora Java . Mówię o zasadzie OO „programowanie do interfejsu, a nie implementacja”. Zgodnie z tą zasadą światowy „interfejs” odnosi się do dowolnego „nadtypu” klasy - interfejsu, klasy abstrakcyjnej lub prostej nadklasy, która jest bardziej abstrakcyjna i mniej konkretna niż bardziej konkretne podklasy.

Aviv Cohn
źródło
możliwy duplikat Dlaczego interfejsy są przydatne?
komar
1
Ta odpowiedź daje łatwy do zrozumienia przykład programmers.stackexchange.com/a/314084/61852
Tulains Córdova

Odpowiedzi:

45

„Programowanie interfejsu” oznacza, że ​​tam, gdzie to możliwe, należy odwoływać się do bardziej abstrakcyjnego poziomu klasy (interfejs, klasa abstrakcyjna, a czasem do jakiejś nadklasy), zamiast do konkretnej implementacji.

To nie jest poprawna . A przynajmniej nie jest to całkowicie poprawne.

Ważniejszy punkt pochodzi z perspektywy projektowania programu. Tutaj „programowanie do interfejsu” oznacza skupienie projektu na tym, co robi kod, a nie na tym , jak to robi. Jest to istotne rozróżnienie, które popycha twój projekt w kierunku poprawności i elastyczności.

Główną ideą jest to, że domeny zmieniają się znacznie wolniej niż oprogramowanie. Załóżmy, że masz oprogramowanie do śledzenia listy zakupów. W latach 80. to oprogramowanie działało przeciwko wierszowi poleceń i niektórym płaskim plikom na dyskietce. Masz interfejs użytkownika. Następnie możesz umieścić listę w bazie danych. Później może zostać przeniesiony do chmury, telefonów komórkowych lub integracji z Facebookiem.

Jeśli zaprojektowałeś swój kod specjalnie wokół implementacji (dyskietki i wiersze poleceń), byłbyś źle przygotowany na zmiany. Jeśli kod został zaprojektowany wokół interfejsu (manipulowanie listą artykułów spożywczych), implementacja może ulec zmianie.

Telastyn
źródło
Dziękuję za odpowiedź. Sądząc po tym, co napisałeś, myślę, że rozumiem, co oznacza „programowanie do interfejsu” i jakie są jego zalety. Ale mam jedno pytanie - najczęstszym konkretnym przykładem tej koncepcji jest to: Tworząc odwołanie do obiektu, ustaw typ odwołania na typ interfejsu, który ten obiekt implementuje (lub nadklasę, którą ten obiekt dziedziczy), zamiast tworzenia typu odwołania typ obiektu. (Aka, List myList = new ArrayList()zamiast ArrayList myList = new ArrayList(). (Pytanie znajduje się w następnym komentarzu)
Aviv Cohn
Moje pytanie brzmi: czy możesz podać mi więcej przykładów miejsc w kodzie świata rzeczywistego, w których ma miejsce zasada „programowania do interfejsu”? Innym niż typowy przykład, który opisałem w ostatnim komentarzu?
Aviv Cohn
6
NO . List/ w ArrayListogóle nie o tym mówię. Bardziej przypomina dostarczenie Employeeobiektu niż zestawu połączonych tabel używanych do przechowywania rekordu pracownika. Lub zapewnienie interfejsu do iteracji po utworach i nie dbanie o to, czy utwory te są tasowane, na płycie CD lub przesyłane strumieniowo z Internetu. To tylko sekwencja piosenek.
Telastyn
1
Istnienie „przycisku turbo”: en.wikipedia.org/wiki/Turbo_button jest faktycznym przykładem analogii dyskietek w świecie rzeczywistym.
JimmyJames
1
@AvivCohn Sugeruję SpringFramework jako dobry przykład. Wszystko na wiosnę można ulepszyć lub dostosować, wprowadzając własne interfejsy i główne zachowanie Springs, a funkcje będą działały zgodnie z oczekiwaniami ... Lub w przypadku dostosowywania, zgodnie z oczekiwaniami. Programowanie interfejsu jest również najlepszą strategią do projektowania ram integracji . Ponownie wiosna robi to dzięki integracji wiosennej. W czasie projektowania programujemy interfejs do tego, co robimy z UML. Albo to bym zrobił. Oddziel się od tego, jak to działa i skup się na tym, co robić.
Laiv
19

Moje rozumienie „programowania do interfejsu” różni się od tego, co sugerują pytania lub inne odpowiedzi. Co nie znaczy, że moje rozumowanie jest poprawne lub że inne odpowiedzi nie są dobrymi pomysłami, tylko że nie są to, o czym myślę, kiedy słyszę ten termin.

Programowanie interfejsu oznacza, że ​​gdy zostanie ci zaprezentowany jakiś interfejs programowania (biblioteka klas, zestaw funkcji, protokół sieciowy lub cokolwiek innego), będziesz używać tylko rzeczy gwarantowanych przez interfejs. Możesz mieć wiedzę na temat podstawowej implementacji (być może napisałeś ją), ale nigdy nie powinieneś z niej korzystać.

Powiedzmy na przykład, że API przedstawia ci nieprzejrzystą wartość, która jest „uchwytem” czegoś wewnętrznego. Twoja wiedza może ci powiedzieć, że ten uchwyt jest tak naprawdę wskaźnikiem, i możesz wyłuskać go i uzyskać dostęp do pewnej wartości, która może pozwolić ci łatwo wykonać pewne zadanie, które chcesz wykonać. Ale interfejs nie zapewnia tej opcji; to twoja wiedza na temat konkretnej implementacji.

Problem polega na tym, że tworzy silne połączenie między twoim kodem a implementacją, czego dokładnie miał zapobiec interfejs. W zależności od polityki może to oznaczać, że implementacji nie można już zmienić, ponieważ spowodowałoby to uszkodzenie twojego kodu lub że kod jest bardzo delikatny i ciągle się psuje przy każdej aktualizacji lub zmianie podstawowej implementacji.

Dużym tego przykładem są programy napisane dla systemu Windows. WinAPI to interfejs, ale wiele osób używa sztuczek, które działały ze względu na konkretną implementację, powiedzmy Windows 95. Te sztuczki mogą przyspieszyć działanie programów lub pozwolić im na wykonywanie zadań przy użyciu mniejszej ilości kodu, niż byłoby to konieczne. Ale te sztuczki oznaczały również, że program ulegnie awarii w systemie Windows 2000, ponieważ interfejs API został tam zaimplementowany inaczej. Gdyby program był wystarczająco ważny, Microsoft mógłby faktycznie dodać trochę hakerów do ich implementacji, aby program kontynuował pracę, ale kosztem tego jest zwiększona złożoność (wraz ze wszystkimi wynikającymi z tego problemami) kodu Windows. To sprawia, że ​​życie ludzi Wine jest wyjątkowo trudne, ponieważ próbują również zaimplementować WinAPI, ale mogą tylko odwołać się do dokumentacji, jak to zrobić,

Sebastian Redl
źródło
To dobra uwaga i często to słyszę w niektórych kontekstach.
Telastyn
Rozumiem co masz na myśli. Zobaczmy, czy mogę dostosować to, co mówisz do programowania ogólnego: Powiedzmy, że mam klasę (klasę A), która wykorzystuje funkcjonalność abstrakcyjnej klasy B. Klasy C i D dziedziczą klasę B - zapewniają one konkretną implementację tego, co to klasa ma robić. Jeśli klasa A używa bezpośrednio klasy C lub D, nazywa się to „programowaniem do implementacji”, co nie jest zbyt elastycznym rozwiązaniem. Ale jeśli klasa A korzysta z odwołania do klasy B, którą można później ustawić na implementację C lub D, sprawia to, że rzeczy są bardziej elastyczne i łatwe w utrzymaniu. Czy to jest poprawne?
Aviv Cohn
Jeśli jest to poprawne, to moje pytanie brzmi - czy są bardziej konkretne przykłady „programowania interfejsu”, inne niż „stosowanie odwołania do interfejsu zamiast zwykłego przykładu odniesienia do konkretnej klasy”?
Aviv Cohn
2
@AvivCohn Trochę późno w tej odpowiedzi, ale jednym konkretnym przykładem jest sieć WWW. Podczas wojen przeglądarkowych (era IE 4) strony internetowe były pisane nie w kierunku tego, co mówi każda specyfikacja, ale w dziwactwach niektórych przeglądarek (Netscape lub IE). To było w zasadzie programowanie do implementacji zamiast interfejsu.
Sebastian Redl,
9

Mogę tylko mówić o moim osobistym doświadczeniu, ponieważ nigdy nie zostało mi to formalnie nauczone.

Twój pierwszy punkt jest poprawny. Uzyskana elastyczność wynika z niemożności przypadkowego wywołania szczegółów implementacji konkretnej klasy, w której nie należy ich wywoływać.

Rozważmy na przykład ILoggerinterfejs, który jest obecnie implementowany jako konkretna LogToEmailLoggerklasa. LogToEmailLoggerKlasa eksponuje wszystkie ILoggermetody i właściwości, ale również zdarza się mieć własności implementacji specyficznych sysAdminEmail.

Gdy twój program rejestrujący jest używany w twojej aplikacji, ustawianie go nie powinno zajmować konsumpcyjnego kodu sysAdminEmail. Ta właściwość powinna zostać ustawiona podczas konfiguracji rejestratora i powinna być ukryta przed światem.

Jeśli kodujesz w oparciu o konkretną implementację, możesz przypadkowo ustawić właściwość implementacji podczas korzystania z programu rejestrującego. Teraz twój kod aplikacji jest ściśle powiązany z twoim loggerem, a przejście do innego loggera będzie wymagać najpierw oddzielenia kodu od oryginalnego.

W tym sensie kodowanie do interfejsu rozluźnia sprzężenie .

Odnośnie twojego drugiego punktu: Innym powodem, dla którego widziałem kodowanie interfejsu, jest zmniejszenie złożoności kodu.

Na przykład, wyobraź sobie, mam grę z następujących interfejsów I2DRenderable, I3DRenderable, IUpdateable. Często zdarza się, że pojedyncze elementy gry zawierają treści do renderowania w 2D i 3D. Inne elementy mogą być tylko 2D, a inne tylko 3D.

Jeśli rendering 2D jest wykonywany przez jeden moduł, sensowne jest utrzymanie kolekcji I2DRenderables. Nie ma znaczenia, czy obiekty w jego kolekcji również są, I3DRenderableczy też IUpdatebleinne moduły będą odpowiedzialne za traktowanie tych aspektów obiektów.

Przechowywanie obiektów do renderowania jako listy I2DRenderableutrzymuje złożoność klasy renderowania na niskim poziomie. Renderowanie 3D i logika aktualizacji nie stanowią jego problemu, dlatego te aspekty obiektów potomnych można i należy zignorować.

W tym sensie kodowanie interfejsu utrzymuje złożoność na niskim poziomie, izolując obawy .

MetaFight
źródło
4

Są tu prawdopodobnie dwa zastosowania interfejsu słowa. Interfejs, o którym głównie mowa w pytaniu, to interfejs Java . Jest to w szczególności koncepcja Java, bardziej ogólnie jest to interfejs języka programowania.

Powiedziałbym, że programowanie interfejsu jest szerszą koncepcją. Popularne obecnie interfejsy API REST dostępne dla wielu stron internetowych są kolejnym przykładem szerszej koncepcji programowania interfejsu na wyższym poziomie. Tworząc warstwę między wewnętrznym działaniem kodu a światem zewnętrznym (ludzie w Internecie, inne programy, a nawet inne części tego samego programu) możesz zmienić wszystko w kodzie, o ile nie zmienisz świata zewnętrznego oczekuje, jeśli jest to określone przez interfejs lub umowę, którą zamierzasz dotrzymać.

Zapewnia to elastyczność refaktoryzacji kodu wewnętrznego, bez konieczności mówienia o wszystkich innych sprawach zależnych od jego interfejsu.

Oznacza to również, że Twój kod powinien być bardziej stabilny. Trzymając się interfejsu, nie powinieneś łamać kodu innych osób. Gdy naprawdę musisz zmienić interfejs, możesz wydać nową główną wersję interfejsu API (1.abc do 2.xyz), która sygnalizuje, że nastąpiły przełomowe zmiany w interfejsie nowej wersji.

Jak wskazuje @Doval w komentarzach do tej odpowiedzi, istnieje także lokalizacja błędów. Myślę, że wszystkie sprowadzają się do enkapsulacji. Podobnie jak w przypadku obiektów w projekcie zorientowanym obiektowo, ta koncepcja jest również przydatna na wyższym poziomie.

Encaitar
źródło
1
Zaletą zwykle pomijaną jest lokalizacja błędów. Powiedz, że potrzebujesz mapy, a implementujesz ją za pomocą drzewa binarnego. Aby to zadziałało, klucze muszą mieć pewną kolejność i należy zachować niezmienność, że klucze, które są „mniejsze niż” klucz bieżącego węzła, znajdują się w lewym poddrzewie, podczas gdy te, które są „większe niż”, są włączone właściwe poddrzewo. Jeśli ukrywasz implementację mapy za interfejsem, jeśli wyszukiwanie mapy nie powiedzie się, wiesz, że błąd musi znajdować się w module mapy. Jeśli zostanie ujawniony, błąd może znajdować się w dowolnym miejscu programu. Dla mnie jest to główna korzyść.
Doval
4

Analogia ze świata rzeczywistego może pomóc:

A Wtyczka zasilania sieciowego w interfejsie.
Tak; trzy przypięte rzeczy na końcu przewodu zasilającego z telewizora, radia, odkurzacza, pralki itp.

Każde urządzenie, które ma główną wtyczkę (tzn. Implementuje interfejs „ma wtyczkę sieciową”), może być traktowane dokładnie w ten sam sposób; wszystkie mogą być podłączone do gniazdka ściennego i mogą pobierać energię z tego gniazdka.

To, co robi każde urządzenie , jest zupełnie inne. Nie zajdziesz daleko do czyszczenia dywanów za pomocą telewizora, a większość ludzi nie ogląda pralki dla rozrywki. Ale wszystkie te urządzenia mają wspólne zachowanie, ponieważ można je podłączyć do gniazdka ściennego.

Właśnie to zapewniają interfejsy.
Ujednolicone zachowania, które mogą być realizowane przez wiele różnych klas obiektów, bez potrzeby komplikacji związanych z dziedziczeniem.

Phill W.
źródło
1

Termin „programowanie do interfejsu” jest otwarty na wiele interpretacji. Interfejs w tworzeniu oprogramowania to bardzo popularne słowo. Oto jak wyjaśniam tę koncepcję młodszym programistom, których trenowałem przez lata.

W architekturze oprogramowania istnieje szeroki zakres naturalnych granic. Typowe przykłady to

  • Granica sieci między procesami klienta i serwera
  • Granica interfejsu API między aplikacją a biblioteką strony trzeciej
  • Wewnętrzna granica kodu między różnymi domenami biznesowymi w programie

Liczy się to, że kiedy te naturalne granice istnieją, są one identyfikowane i określa się sposób zachowania się tej granicy. Oprogramowanie testuje się nie na podstawie tego, czy zachowuje się „druga strona”, ale czy interakcje są zgodne ze specyfikacją.

Konsekwencje tego są następujące:

  • Elementy zewnętrzne można wymieniać, o ile implementują one specyfikację
  • Naturalny punkt dla testów jednostkowych w celu sprawdzenia poprawności zachowania
  • Testy integracyjne stały się ważne - czy specyfikacja była niejednoznaczna?
  • Jako programista masz mniejszy problem podczas pracy nad określonym zadaniem

Chociaż wiele z nich może dotyczyć klas i interfejsów, równie ważne jest, aby zdawać sobie sprawę, że dotyczy to również modeli danych, protokołów sieciowych i, bardziej ogólnie, współpracy z wieloma programistami

Michael Shaw
źródło