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:
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ą?
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.
źródło
Odpowiedzi:
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.
źródło
List myList = new ArrayList()
zamiastArrayList myList = new ArrayList()
. (Pytanie znajduje się w następnym komentarzu)List
/ wArrayList
ogóle nie o tym mówię. Bardziej przypomina dostarczenieEmployee
obiektu 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.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ć,
źródło
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
ILogger
interfejs, który jest obecnie implementowany jako konkretnaLogToEmailLogger
klasa.LogToEmailLogger
Klasa eksponuje wszystkieILogger
metody i właściwości, ale również zdarza się mieć własności implementacji specyficznychsysAdminEmail
.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
I2DRenderable
s. Nie ma znaczenia, czy obiekty w jego kolekcji również są,I3DRenderable
czy teżIUpdateble
inne moduły będą odpowiedzialne za traktowanie tych aspektów obiektów.Przechowywanie obiektów do renderowania jako listy
I2DRenderable
utrzymuje 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 .
źródło
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.
źródło
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.
źródło
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
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:
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
źródło