Podziel duże interfejsy

9

Korzystam z dużego interfejsu z około 50 metodami dostępu do bazy danych. Interfejs został napisany przez mojego kolegę. Omówiliśmy to:

Ja: 50 metod to za dużo. To zapach kodu.
Kolega: Co mam z tym zrobić? Chcesz dostępu do DB - masz go.
Ja: Tak, ale jest niejasne i trudno je utrzymać w przyszłości.
Kolega: OK, masz rację, to nie jest miłe. Jak powinien wyglądać interfejs?
Ja: A może 5 metod zwracających obiekty, które mają po 10 metod każda?

Mmm, ale czy to nie będzie to samo? Czy to naprawdę prowadzi do większej przejrzystości? Czy warto?

Od czasu do czasu jestem w sytuacji, w której chcę interfejs, a pierwszą rzeczą, która przychodzi mi na myśl, jest jeden, duży interfejs. Czy istnieje dla tego ogólny wzór?


Aktualizacja (w odpowiedzi na komentarz SJuan):

„Rodzaj metod”: Jest to interfejs do pobierania danych z bazy danych. Wszystkie metody mają postać (pseudokod)

List<Typename> createTablenameList()

Metody i tabele nie są dokładnie w relacji 1-1, większy nacisk kładziony jest na fakt, że zawsze otrzymujesz jakąś listę pochodzącą z bazy danych.

TobiMcNamobi
źródło
12
Brakuje odpowiednich informacji (jakie masz metody). W każdym razie, jak sądzę: jeśli dzielisz tylko przez liczbę, wtedy twój kolega ma rację, nic nie poprawiasz. Jedno możliwe kryterium podziału zwróciłoby „podmiot” (prawie odpowiednik tabeli) (więc a UserDaoi a CustomerDaooraz a ProductDao)
SJuan76
Rzeczywiście niektóre tabele są semantycznie zbliżone do innych tabel tworzących „kliki”. Podobnie jak metody.
TobiMcNamobi
Czy można zobaczyć kod? Wiem, że ma 4 lata i pewnie już go naprawiłeś: D Chciałbym jednak pomyśleć o tym problemie. Rozwiązałem już coś takiego.
clankill3r
@ clankill3r Rzeczywiście nie mam już dostępu do konkretnego interfejsu, który skłonił mnie do opublikowania powyższego pytania. Problem jest jednak bardziej ogólny. Wyobraź sobie DB z około 50 tabelami i dla każdej tabeli metodę podobnąList<WeatherDataRecord> createWeatherDataTable() {db.open(); return db.select("*", "tbl_weatherData");}
TobiMcNamobi

Odpowiedzi:

16

Tak, 50 metod to zapach kodu, ale zapach kodu oznacza ponowne przyjrzenie się temu, nie automatycznie. Jeśli każdy klient korzystający z tej klasy potencjalnie potrzebuje wszystkich 50 metod, może nie być przypadku, aby ją podzielić. Jest to jednak mało prawdopodobne. Chodzi mi o to, że arbitralny podział interfejsu może być gorszy niż całkowity jego brak.

Nie ma jednego wzorca, aby to naprawić, ale zasadą opisującą pożądany stan jest zasada segregacji interfejsu („I” w SOLID), która stwierdza, że ​​żaden klient nie powinien być zmuszany do polegania na metodach, których nie używa .

Opis dostawcy daje podpowiedź, jak to naprawić: spójrz na klienta . Często po prostu patrząc na klasę wydaje się, że wszystko należy do siebie, ale kiedy patrzy się na klientów korzystających z tej klasy, powstają wyraźne podziały . Zawsze projektuj interfejs w pierwszej kolejności.

Innym sposobem ustalenia, czy i gdzie interfejs powinien zostać podzielony, jest wykonanie drugiej implementacji. Często zdarza się, że twoja druga implementacja nie wymaga wielu metod, więc te powinny wyraźnie zostać podzielone na własny interfejs.

Karl Bielefeldt
źródło
To i odpowiedź utnapistim są naprawdę świetne.
TobiMcNamobi
13

Kolega: OK, masz rację, to nie jest miłe. Jak powinien wyglądać interfejs?

Ja: A może 5 metod zwracających obiekty, które mają po 10 metod każda?

To nie są dobre kryteria (w rzeczywistości nie ma żadnych kryteriów). Możesz je pogrupować według (na przykład, jeśli twoja aplikacja jest aplikacją do transakcji finansowych):

  • funkcjonalność (wstawia, aktualizuje, wybiera, transakcje, metadane, schemat itp.)
  • podmiot (użytkownik DAO , depozyt DAO itp.)
  • obszar zastosowania (transakcje finansowe, zarządzanie użytkownikami, sumy itp.)
  • poziom abstrakcji (cały kod dostępu do tabeli jest osobnym modułem; wszystkie wybrane interfejsy API znajdują się we własnej hierarchii, obsługa transakcji jest osobna, cały kod konwersji w module, cały kod sprawdzania poprawności w module itp.)

Mmm, ale czy to nie będzie to samo? Czy to naprawdę prowadzi do większej przejrzystości? Czy warto?

Jeśli wybierzesz odpowiednie kryteria, zdecydowanie. Jeśli nie, to na pewno nie :).

Kilka przykładów:

  • spójrz na obiekty ADODB, aby uzyskać uproszczony przykład operacji podstawowych OO (prawdopodobnie Twój interfejs API DB już to oferuje)

  • spójrz na model danych Django ( https://docs.djangoproject.com/en/dev/topics/db/models/ ), aby znaleźć pomysł na model danych o wysokim poziomie abstrakcji (w C ++ prawdopodobnie będziesz potrzebować nieco więcej płyty kotłowej kod, ale to fajny pomysł). Ta implementacja została zaprojektowana z myślą o roli „modelowej” w ramach wzorca projektowego MVC.

  • spójrz na API sqlite na płaski pomysł API ( http://www.sqlite.org/c3ref/funclist.html ), składający się tylko z funkcjonalnych prymitywów (C API).

utnapistim
źródło
3

Od czasu do czasu jestem w sytuacji, w której chcę interfejs, a pierwszą rzeczą, która przychodzi mi na myśl, jest jeden duży interfejs. Czy istnieje dla tego ogólny wzór?

Jest to anty-wzór konstrukcyjny zwany klasą monolityczną . Posiadanie 50 metod w klasie lub interfejsie jest prawdopodobnym naruszeniem SRP . Klasa monolityczna powstaje, ponieważ stara się być wszystkim dla wszystkich.

Metoda DCI adresuje wzdęcie. Zasadniczo wiele obowiązków klasy można przypisać do ról (przeniesionych do innych klas), które są istotne tylko w niektórych kontekstach. Zastosowanie ról można osiągnąć na wiele sposobów, w tym mixinów i dekoratorów . Takie podejście pozwala utrzymać skupienie i szczupłość klas.

A może 5 metod zwracających obiekty, które mają po 10 metod każda?

Sugeruje to utworzenie instancji wszystkich ról, gdy sam obiekt jest utworzony. Ale po co tworzyć role, których możesz nie potrzebować? Zamiast tego utwórz rolę w kontekście, w którym jej potrzebujesz.

Jeśli okaże się, że refaktoryzacja w kierunku DCI nie jest oczywista, możesz zastosować prostszy wzór dla odwiedzających . Zapewnia podobną korzyść bez podkreślania tworzenia kontekstów przypadków użycia.

EDYCJA: Moje myślenie o tym trochę się zmieniło. Podałem alternatywną odpowiedź.

Mario T. Lanza
źródło
1

Wygląda mi na to, że każda inna odpowiedź nie ma sensu. Chodzi o to, że interfejs powinien idealnie definiować atomowy fragment zachowania. To jest I w SOLID.

Klasa powinna ponosić jedną odpowiedzialność, ale nadal może obejmować wiele zachowań. Aby pozostać przy typowym obiekcie klienta bazy danych, może oferować pełną funkcjonalność CRUD. To byłyby cztery zachowania: tworzenie, czytanie, aktualizowanie i usuwanie. W czystym świecie SOLID klient bazy danych nie implementowałby IDatabaseClient, lecz niestabilny ICreator, IReader, IUpdater i IDeleter.

Miałoby to wiele zalet. Po pierwsze, po prostu czytając deklarację klasy, można natychmiast dowiedzieć się wiele o klasie, interfejsy, które implementuje, opowiadają całą historię. Po drugie, jeśli obiekt klienta miałby zostać przekazany jako argument, jeden ma teraz różne przydatne opcje. Można go uznać za IReadera i można mieć pewność, że odbiorca będzie w stanie tylko czytać. Różne zachowania można badać osobno.

Jeśli chodzi o testowanie, powszechną praktyką jest po prostu uderzenie interfejsu w klasę, która jest repliką 1-do-1 interfejsu pełnej klasy. Jeśli zależy ci na testowaniu, może to być prawidłowe podejście. Pozwala dość szybko robić manekiny. Ale prawie nigdy nie jest SOLIDNY ​​i naprawdę stanowi nadużycie interfejsów do specjalnego celu.

Tak więc, 50 metod to zapach, ale zależy od intencji i celu, czy jest zły, czy nie. Z pewnością nie jest idealny.

Martin Maat
źródło
0

Warstwy dostępu do danych mają zwykle wiele metod dołączonych do jednej klasy. Jeśli kiedykolwiek pracowałeś z Entity Framework lub innymi narzędziami ORM, zobaczysz, że generują one setki metod. Zakładam, że ty i twój kolega wdrażacie go ręcznie. Nie jest konieczny zapach kodu, ale nie jest ładnie na to patrzeć. Trudno powiedzieć bez znajomości domeny.

Roman Mik
źródło
Metody lub właściwości?
JeffO
0

Używam protokołów (nazywaj je interfejsami, jeśli chcesz) prawie uniwersalnie dla wszystkich apis zarówno z FP, jak i OOP. (Pamiętasz Matrycę? Nie ma żadnych konkrecji!) Istnieją oczywiście konkretne typy, ale w ramach programu każdy typ jest uważany za coś, co odgrywa rolę w pewnym kontekście.

Oznacza to, że obiekty przekazywane przez programy, do funkcji itp. Można traktować jako abstrakcyjne byty o nazwanych zestawach zachowań. Obiekt można uznać za odgrywający rolę, jaką jest pewien zestaw protokołów. Osoba (konkretny typ) może być mężczyzną, ojcem, mężem, przyjacielem i pracownikiem, ale nie wyobrażam sobie wielu funkcji, które uznają byt za sumę więcej niż 2 z nich.

Wydaje mi się, że możliwe jest, że złożony obiekt będzie działał zgodnie z wieloma różnymi protokołami, ale nadal trudno byłoby uzyskać dostęp do interfejsu API 50 metod. Większość protokołów ma 1 lub 2 metody, może 3, ale nigdy 50! Każdy podmiot, który ma 50 metod, powinien być agregatem kilku mniejszych komponentów, z których każdy ma własne obowiązki. Istota w ogóle przedstawiałaby prostszy interfejs, który wyodrębnia sumę apis w jej obrębie.

Zamiast myśleć o obiektach i metodach, zacznij myśleć o abstrakcjach i kontraktach oraz o tym, jaką rolę odgrywa podmiot w pewnym kontekście.

Mario T. Lanza
źródło