Czy „kompozycja zamiast dziedziczenia” narusza „suchą zasadę”?

36

Rozważmy na przykład, że mam klasę dla innych klas do rozszerzenia:

public class LoginPage {
    public String userId;
    public String session;
    public boolean checkSessionValid() {
    }
}

i niektóre podklasy:

public class HomePage extends LoginPage {

}

public class EditInfoPage extends LoginPage {

}

W rzeczywistości podklasa nie ma żadnych metod przesłonięcia, również nie uzyskałbym dostępu do strony głównej w sposób ogólny, tj .: nie zrobiłbym czegoś takiego:

for (int i = 0; i < loginPages.length; i++) {
    loginPages[i].doSomething();
}

Chcę tylko ponownie użyć strony logowania. Ale zgodnie z https://stackoverflow.com/a/53354 powinienem tutaj preferować kompozycję, ponieważ nie potrzebuję interfejsu LoginPage, więc nie używam tutaj dziedziczenia:

public class HomePage {
    public LoginPage loginPage;
}

public class EditInfoPage {
    public LoginPage loginPage;
}

ale problem pojawia się tutaj, w nowej wersji kod:

public LoginPage loginPage;

duplikuje się po dodaniu nowej klasy. A jeśli LoginPage wymaga narzędzia ustawiającego i pobierającego, należy skopiować więcej kodów:

public LoginPage loginPage;

private LoginPage getLoginPage() {
    return this.loginPage;
}
private void setLoginPage(LoginPage loginPage) {
    this.loginPage = loginPage;
}

Więc moje pytanie brzmi: czy „kompozycja nad spadkiem” narusza „suchą zasadę”?

ocomfd
źródło
13
Czasami nie potrzebujesz ani dziedziczenia, ani kompozycji, co oznacza, że ​​czasami jedna klasa z kilkoma przedmiotami spełnia swoje zadanie.
Erik Eidt
23
Dlaczego chcesz, aby wszystkie Twoje strony były stroną logowania? Może po prostu mam stronę logowania.
Pan Cochese,
29
Ale z dziedziczeniem masz duplikat w extends LoginPagekażdym miejscu. SZACH MAT!
el.pescado
2
Jeśli często masz problem z ostatnim fragmentem kodu, być może nadużywasz metody pobierające i ustawiające. Mają tendencję do naruszania enkapsulacji.
Brian McCutchon
4
Więc spraw, aby twoja strona była w stanie ozdobić i stać się LoginPagedekoratorem. Nigdy więcej duplikacji, po prostu proste page = new LoginPage(new EditInfoPage())i gotowe. Lub wykorzystujesz zasadę otwartego zamknięcia, aby moduł uwierzytelniania można było dynamicznie dodawać do dowolnej strony. Istnieje wiele sposobów radzenia sobie z powielaniem kodu, głównie polegających na znalezieniu nowej abstrakcji. LoginPagejest prawdopodobnie złą nazwą, co chcesz upewnić się, że użytkownik jest uwierzytelniany podczas przeglądania tej strony, podczas gdy jest przekierowywany do LoginPagelub wyświetla odpowiedni komunikat o błędzie, jeśli nie jest.
Polygnome

Odpowiedzi:

46

Czekaj, obawiasz się, że to się powtórzy

public LoginPage loginPage;

w dwóch miejscach narusza SUSZENIE? Zgodnie z tą logiką

int x;

może teraz istnieć tylko w jednym obiekcie w całej bazie kodu. Bleh

SUSZENIE to dobra rzecz, o której należy pamiętać, ale daj spokój. Oprócz

... extends LoginPage

jest duplikowany w alternatywie, więc nawet bycie analnym na temat DRY nie ma sensu.

Prawidłowe obawy związane z DRY koncentrują się na identycznym zachowaniu potrzebnym w wielu miejscach, które są definiowane w wielu miejscach, tak że potrzeba zmiany tego zachowania kończy się koniecznością zmiany w wielu miejscach. Podejmuj decyzje w jednym miejscu, a będziesz musiał je zmienić tylko w jednym miejscu. Nie oznacza to, że tylko jeden obiekt może kiedykolwiek zawierać odniesienie do Twojej strony logowania.

Nie należy ślepo śledzić SUSZENIA. Jeśli duplikujesz, ponieważ kopiowanie i wklejanie jest łatwiejsze niż wymyślenie dobrej metody lub nazwy klasy, prawdopodobnie jesteś w błędzie.

Ale jeśli chcesz umieścić ten sam kod w innym miejscu, ponieważ to inne miejsce podlega innej odpowiedzialności i prawdopodobnie będzie wymagać niezależnej zmiany, prawdopodobnie rozsądnie jest złagodzić wymuszanie DRY i pozwolić temu identycznemu zachowaniu mieć inną tożsamość . To ten sam sposób myślenia, który dotyczy zakazania magicznych liczb.

DRY to nie tylko wygląd kodu. Chodzi o to, aby nie rozpowszechniać szczegółów pomysłu za pomocą bezmyślnego powtarzania, zmuszając opiekunów do naprawy rzeczy przy użyciu bezmyślnego powtarzania. To kiedy próbujesz sobie powiedzieć, że bezmyślne powtarzanie jest tylko twoją konwencją, sprawy idą naprawdę źle.

To, co myślę, że tak naprawdę narzekasz, nazywa się kodem typu „kocioł”. Tak, użycie kompozycji zamiast dziedziczenia wymaga kodu typu „kocioł”. Nic nie zostaje ujawnione za darmo, musisz napisać kod, który go ujawnia. Z tą płytą główną jest elastyczność stanu, możliwość zawężenia odsłoniętego interfejsu, nadania rzeczom różnych nazw, które są odpowiednie dla ich poziomu abstrakcji, dobrej pośredniej oli, a używasz tego, z czego jesteś złożony z zewnątrz, a nie wewnątrz, więc masz do czynienia z normalnym interfejsem.

Ale tak, to dużo dodatkowego pisania na klawiaturze. Tak długo, jak mogę zapobiec problemowi jo-jo podskakiwania w górę i w dół stosu dziedziczenia, gdy czytam kod, warto.

Teraz nie chodzi o to, że odmawiam kiedykolwiek dziedziczenia. Jednym z moich ulubionych zastosowań jest nadawanie wyjątkom nowych nazw:

public class MyLoginPageWasNull extends NullPointerException{}
candied_orange
źródło
int x;może istnieć nie więcej niż zero razy w bazie kodu
K. Alan Bates
@ K.AlanBates przepraszam ?
candied_orange
... wiedziałem, że zamierzasz się odegrać z lolem klasy Point. Nikt nie dba o klasę Point. Jeśli wejdę do twojej bazy kodów i zobaczę int a; int b; int x;, będę naciskać, aby usunąć Ciebie.
K. Alan Bates
@ K.AlanBates Wow. to smutne, że myślisz, że takie zachowanie uczyniłoby cię dobrym koderem. Najlepszy programista nie jest tym, który potrafi znaleźć więcej rzeczy na temat innych, o których można się wkurzyć. To ten sprawia, że ​​reszta jest lepsza.
candied_orange
Używanie bezsensownych nazw nie jest początkowe i prawdopodobnie czyni dewelopera tego kodu raczej zobowiązaniem niż aktywem.
K. Alan Bates
127

Powszechnym nieporozumieniem z zasadą DRY jest to, że wiąże się to z powtarzaniem wierszy kodu. Zasada DRY to: „Każda wiedza musi mieć jedną, jednoznaczną, autorytatywną reprezentację w systemie” . Chodzi o wiedzę, a nie kod.

LoginPagewie, jak narysować stronę do logowania. Gdyby EditInfoPagewiedział, jak to zrobić, byłoby to naruszeniem. W tym LoginPagepoprzez kompozycji jest nie w jakikolwiek sposób naruszenie zasady DRY.

Zasada DRY jest prawdopodobnie najbardziej nadużywaną zasadą w inżynierii oprogramowania i zawsze należy ją traktować nie jako zasadę nie powielania kodu, ale jako zasadę nie powielania wiedzy w dziedzinie abstrakcyjnej. W rzeczywistości, w wielu przypadkach, jeśli zastosujesz poprawnie OSUSZANIE, będziesz kopiować kod , i to niekoniecznie jest złą rzeczą.

wasatz
źródło
27
„Zasada pojedynczej odpowiedzialności” jest znacznie częściej wykorzystywana. „Dont repeat yourself” może łatwo znaleźć się na drugim miejscu :-)
gnasher729
28
„DRY” jest przydatnym akronimem słowa „Don't Repeat Yourself”, które jest frazą catch, ale nie nazwą zasady, faktyczna nazwa zasady to „Once And Only Once”, która z kolei jest chwytliwą nazwą dla zasady, której opis wymaga kilku akapitów. Ważne jest zrozumienie kilku akapitów, a nie zapamiętywanie trzech liter D, R i Y.
Jörg W Mittag
4
@ gnasher729 Wiesz, zgadzam się z tobą, że SRP jest prawdopodobnie niewłaściwie wykorzystywane znacznie częściej. Chociaż, mówiąc uczciwie, w wielu przypadkach uważam, że często są razem niewłaściwie wykorzystywane. Moja teoria jest taka, że ​​ogólnie programiści nie powinni ufać, że używają akronimów z „łatwymi nazwami”.
wasatz
17
IMHO to dobra odpowiedź. Jednak, szczerze mówiąc: według mojego doświadczenia, najczęstszą formą naruszeń SUCHEGO jest programowanie z kopiowaniem i wklejaniem oraz po prostu powielanie kodu. Ktoś mi kiedyś powiedział: „za każdym razem, gdy zamierzasz skopiować i wkleić kod, pomyśl dwa razy, czy zduplikowanej części nie można wyodrębnić do wspólnej funkcji” - co było bardzo dobrą radą IMHO.
Doc Brown
9
Nadużywanie kopiowania i wklejania jest z pewnością siłą napędową naruszania OSUSZANIA, ale po prostu zabranianie kopiowania i wklejania jest kiepskimi wskazówkami do postępowania w przypadku OSUSZANIA. Nie miałbym żadnych wyrzutów sumienia z powodu posiadania dwóch metod z identycznym kodem, gdy te metody reprezentują różne elementy wiedzy. Służą one dwóm różnym obowiązkom. Po prostu dziś mają identyczny kod. Powinni mieć swobodę niezależnej zmiany. Tak, wiedza, nie kod. Dobrze wyłożone. Napisałem jedną z pozostałych odpowiedzi, ale pokłonię się tej. +1.
candied_orange
12

Krótka odpowiedź: tak, robi - w pewnym stopniu akceptowalnym stopniu.

Na pierwszy rzut oka dziedziczenie może czasami zaoszczędzić ci niektóre wiersze kodu, ponieważ powoduje, że „moja klasa ponownego wykorzystywania będzie zawierać wszystkie publiczne metody i atrybuty w sposób 1: 1”. Jeśli więc w składniku znajduje się lista 10 metod, nie trzeba powtarzać ich w kodzie w odziedziczonej klasie. Gdy w scenariuszu kompozycyjnym 9 z tych 10 metod powinno zostać publicznie ujawnionych za pomocą komponentu służącego do ponownego użycia, wówczas należy spisać 9 wywołań delegowania, a pozostawiając jedno z pozostałych, nie można tego obejść.

Dlaczego to jest tolerowane? Spójrz na metody replikowane w scenariuszu kompozycji - metody te delegują wyłącznie wywołania do interfejsu komponentu, więc nie zawierają żadnej logiki.

Istotą zasady DRY jest unikanie dwóch miejsc w kodzie, w których kodowane są te same reguły logiczne - ponieważ gdy te reguły logiczne ulegną zmianie, w kodzie innym niż DRY łatwo jest dostosować jedno z tych miejsc i zapomnieć o drugim, co wprowadza błąd.

Ponieważ jednak delegowanie połączeń nie zawiera logiki, zwykle nie podlegają one takiej zmianie, więc nie powodują prawdziwego problemu, gdy „wolą kompozycję niż dziedziczenie”. I nawet jeśli interfejs komponentu ulegnie zmianie, co może spowodować zmiany formalne we wszystkich klasach korzystających z komponentu, w skompilowanym języku kompilator powie nam, kiedy zapomnimy zmienić jednego z wywołujących.

Uwaga do twojego przykładu: nie wiem jak wyglądają twoje HomePagei twoje EditInfoPage, ale jeśli mają one funkcję logowania, a HomePage(lub an EditInfoPage) jest LoginPage , to dziedziczenie może być tutaj właściwym narzędziem. Mniej dyskusyjny przykład, w którym kompozycja będzie lepszym narzędziem w bardziej oczywisty sposób, prawdopodobnie by to wyjaśniło.

Zakładając, że nie ma ani HomePagenie EditInfoPagejest LoginPage, a ktoś chce ponownie użyć tego drugiego, jak pisałeś, jest całkiem prawdopodobne, że wymaga się tylko niektórych części LoginPage, a nie wszystkiego. W takim przypadku może być lepsze podejście niż użycie kompozycji w pokazany sposób

  • wyodrębnij część wielokrotnego użytku LoginPagedo własnego komponentu

  • ponowne wykorzystanie tego elementu w HomePagei EditInfoPagetak samo jest teraz używany wewnątrzLoginPage

W ten sposób zazwyczaj stanie się o wiele bardziej jasne, dlaczego i kiedy kompozycja nad dziedziczeniem jest właściwym podejściem.

Doktor Brown
źródło