Co to znaczy, kiedy mówi się „obuduj to, co się zmienia”?

25

Jedną z zasad OOP, na które natrafiłem, jest: -Ekapsuluj, co się różni.

Rozumiem, czym jest dosłowne znaczenie tego wyrażenia, tzn. Ukryj to, co się różni. Nie wiem jednak, jak dokładnie przyczyniłoby się to do lepszego projektu. Czy ktoś może to wyjaśnić na dobrym przykładzie?

Haris Ghauri
źródło
Zobacz en.wikipedia.org/wiki/Encapsulation_(computer_programming), który dobrze to wyjaśnia. Myślę, że to, co się zmienia, nie jest poprawne, ponieważ czasem powinieneś także enkapsulować stałe.
qwerty_so
I don't know how exactly would it contribute to a better designHermetyzowanie szczegółów dotyczy luźnego sprzężenia między „modelem” a szczegółami implementacji. Im mniej „model” jest powiązany ze szczegółami implementacji, tym bardziej elastyczne jest rozwiązanie. I ułatwia to ewolucję. „Abstrahuj od szczegółów”.
Laiv
@Laiv Więc „zmienia się” odnosi się do tego, co ewoluuje w trakcie cyklu życia oprogramowania lub jakie zmiany zachodzą podczas wykonywania programu lub w obu przypadkach?
Haris Ghauri,
2
@HarisGhauri oba. Grupuj razem, co się różni. Wyizoluj to, co zmienia się niezależnie. Bądź podejrzliwy wobec tego, co zakładasz, że nigdy się nie zmieni.
candied_orange
1
@laiv uważa, że ​​„streszczenie” jest dobrym punktem. Może to być przytłaczające. W każdym obiekcie masz ponosić jedną odpowiedzialność. Zaletą tego jest to, że musisz tylko dokładnie przemyśleć tę jedną rzecz tutaj. Kiedy szczegóły reszty problemu są czyimś problemem, ułatwia to wszystko.
candied_orange

Odpowiedzi:

30

Możesz napisać kod, który wygląda następująco:

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

lub możesz napisać kod, który wygląda następująco:

pet.speak();

Jeśli to, co zmienia się, jest hermetyzowane, nie musisz się tym martwić. Po prostu martwisz się o to, czego potrzebujesz i cokolwiek używasz, zastanawiasz się, jak zrobić to, czego naprawdę potrzebujesz, w zależności od tego, co jest różne.

Hermetyzuj to, co się różni, i nie musisz rozprowadzać kodu, który dba o to, co się zmienia. Po prostu ustawisz zwierzaka na określony typ, który umie mówić jako ten typ, a następnie możesz zapomnieć, który typ i traktować go jak zwierzaka. Nie musisz pytać, jaki typ.

Możesz pomyśleć, że typ jest enkapsulowany, ponieważ do uzyskania dostępu jest wymagany getter. Ja nie. Getter nie jest tak naprawdę enkapsulowany. Po prostu drżą, gdy ktoś złamie twoje kapsułkowanie. Są ładnym dekoratorem przypominającym aspekt haczyka, który jest najczęściej używany jako kod debugowania. Bez względu na to, jak go pokroisz, nadal ujawniasz typ.

Możesz spojrzeć na ten przykład i pomyśleć, że łączę polimorfizm i enkapsulację. Nie jestem. Łączę „co się zmienia” i „szczegóły”.

Fakt, że twoje zwierzę jest psem, jest szczegółem. Taki, który może się dla ciebie różnić. Taki, który może nie. Ale na pewno taki, który może różnić się w zależności od osoby. O ile nie wierzymy, że to oprogramowanie będzie kiedykolwiek używane tylko przez miłośników psów, mądrze jest traktować psa jako szczegół i obudować go. W ten sposób niektóre części systemu są błogo nieświadome psa i nie zostaną naruszone, gdy połączymy się z „papugami jesteśmy my”.

Oddziel, oddziel i ukryj szczegóły od reszty kodu. Nie pozwól, aby wiedza o szczegółach rozprzestrzeniła się w twoim systemie, a będziesz dobrze przestrzegać „enkapsulacji tego, co się zmienia”.

candied_orange
źródło
3
To naprawdę dziwne. „Hermetyzuj, co się różni” dla mnie oznacza ukrywanie zmian stanu, np. Nigdy nie posiadaj zmiennych globalnych. Ale odpowiedź też ma sens, nawet jeśli wydaje się bardziej odpowiedzią na polimorfizm niż enkapsulację :)
David Arno
2
@DavidArno Polimorfizm jest jednym ze sposobów, aby to zadziałało. Mógłbym po prostu wcisnąć strukturę if w zwierzaka, a wszystko wyglądałoby tu ładnie dzięki hermetyzacji zwierzaka. Ale to po prostu przesuwałoby bałagan zamiast go sprzątać.
candied_orange
1
„Hermetyzuj, co się różni” dla mnie oznacza ukrywanie zmian stanu . Nie, nie Podoba mi się komentarz CO. Odpowiedź Dericka Elkina idzie głębiej, przeczytaj ją więcej niż raz. Jak powiedział @JacquesB „Ta zasada jest naprawdę dość głęboka”
radarbob,
16

„Różni się” oznacza tutaj „może się zmieniać z czasem ze względu na zmieniające się wymagania”. Jest to podstawowa zasada projektowania: Oddzielanie i izolowanie fragmentów kodu lub danych, które mogą wymagać osobnej zmiany w przyszłości. Jeśli zmieni się pojedynczy wymóg, najlepiej powinien wymagać od nas zmiany powiązanego kodu w jednym miejscu. Ale jeśli baza kodu jest źle zaprojektowana, tj. Silnie połączona i logika dla wymagań rozłożonych w wielu miejscach, zmiana będzie trudna i będzie wiązać się z wysokim ryzykiem wywołania nieoczekiwanych efektów.

Załóżmy, że masz aplikację, która korzysta z kalkulacji podatku od sprzedaży w wielu miejscach. Jeśli zmieni się stawka podatku od sprzedaży, co wolisz:

  • stawka podatku jest dosłownie zakodowana wszędzie w aplikacji, w której naliczany jest podatek od sprzedaży.

  • stawka podatku od sprzedaży jest globalną stałą, która jest stosowana wszędzie w aplikacji, w której naliczany jest podatek od sprzedaży.

  • istnieje jedna metoda o nazwie, calculateSalesTax(product)która jest jedynym miejscem, w którym stosowana jest stawka podatku od sprzedaży.

  • stawka podatku od sprzedaży jest określona w pliku konfiguracyjnym lub polu bazy danych.

Ponieważ stawka podatku od sprzedaży może ulec zmianie w wyniku decyzji politycznej niezależnej od innych wymagań, wolimy, aby była ona izolowana w konfiguracji, dzięki czemu można ją zmienić bez wpływu na kod. Ale możliwe jest również, że logika obliczania podatku od sprzedaży może ulec zmianie, np. Różne stawki dla różnych produktów, dlatego też chcielibyśmy, aby logika obliczeń była zamknięta. Stała globalna może wydawać się dobrym pomysłem, ale w rzeczywistości jest zła, ponieważ może zachęcać do korzystania z podatku od sprzedaży w różnych miejscach programu, a nie w jednym miejscu.

Rozważmy teraz inną stałą, Pi, która jest również używana w wielu miejscach kodu. Czy obowiązuje ta sama zasada projektowania? Nie, ponieważ Pi się nie zmieni. Wyodrębnienie go do pliku konfiguracyjnego lub pola bazy danych po prostu wprowadza niepotrzebną złożoność (a wszystko inne jest równe, preferujemy najprostszy kod). Sensowne jest nadanie jej globalnej stałej zamiast stałego kodowania w wielu miejscach, aby uniknąć niespójności i poprawić czytelność.

Chodzi o to, że jeśli spojrzymy tylko na to, jak program działa teraz , stawka podatku od sprzedaży i Pi są równoważne, oba są stałymi. Dopiero gdy zastanowimy się, co może się różnić w przyszłości , zdamy sobie sprawę, że musimy traktować je inaczej w projekcie.

Ta zasada jest w rzeczywistości dość głęboka, ponieważ oznacza, że ​​musisz spojrzeć poza to, co ma dziś zrobić baza kodu , a także wziąć pod uwagę siły zewnętrzne, które mogą spowodować jej zmianę, a nawet zrozumieć różnych interesariuszy stojących za wymaganiami.

JacquesB
źródło
2
Podatki są dobrym przykładem. Przepisy i podatki cielęta mogą zmieniać się z dnia na dzień. Jeśli wdrażasz system deklaracji podatkowych, jesteś mocno przywiązany do tego rodzaju zmian. Zmienia się także z jednego regionu do drugiego (kraje, prowincje, ...)
Laiv
„Pi się nie zmieni” rozśmieszyło mnie. To prawda, że ​​Pi najprawdopodobniej się nie zmieni, ale przypuśćmy, że nie wolno ci już tego używać? Jeśli niektórzy mają na to sposób, Pi będzie przestarzałe. Załóżmy, że stanie się to wymogiem? Mam nadzieję, że masz szczęśliwy dzień Tau . Dobra odpowiedź BTW. Rzeczywiście głęboko.
candied_orange
14

Obie obecne odpowiedzi wydają się tylko częściowo trafiać w sedno i koncentrują się na przykładach, które przesłaniają podstawową ideę. Nie jest to również (wyłącznie) zasada OOP, ale ogólnie zasada projektowania oprogramowania.

Tym, co „różni się” w tym wyrażeniu, jest kod. Christophe ma rację mówiąc, że zwykle jest to coś, co może się różnić, to znaczy, że często tego oczekujesz . Celem jest ochrona się przed przyszłymi zmianami w kodzie. Jest to ściśle związane z programowaniem w interfejsie . Jednak Christophe błędnie ogranicza to do „szczegółów implementacji”. W rzeczywistości wartość tej porady często wynika ze zmian wymagań .

Jest to tylko pośrednio związane z stanem enkapsulacji, o czym, jak sądzę, myśli David Arno. Ta rada nie zawsze (ale często) sugeruje stan kapsułkowania, a ta rada dotyczy również obiektów niezmiennych. W rzeczywistości zwykłe nazywanie stałych jest (bardzo podstawową) formą enkapsulacji tego, co różni.

CandiedOrange wyraźnie łączy „to, co się zmienia” z „szczegółami”. Jest to tylko częściowo poprawne. Zgadzam się, że każdy kod, który się zmienia, jest w pewnym sensie „szczegółami”, ale „szczegół” może się nie różnić (chyba że zdefiniujesz „szczegóły”, aby uczynić to tautologicznym). Mogą istnieć powody, by ujmować nie zmieniające się szczegóły, ale to powiedzenie nie jest jedno. Z grubsza mówiąc, jeśli jesteś bardzo pewny, że „pies”, „kot” i „kaczka” będą jedynymi typami, z którymi kiedykolwiek będziesz musiał sobie poradzić, to to powiedzenie nie sugeruje refaktoryzacji, jaką wykonuje CandiedOrange.

Rzucając przykład CandiedOrange w innym kontekście, załóżmy, że mamy język proceduralny, taki jak C. Jeśli mam jakiś kod, który zawiera:

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

Mogę się spodziewać, że ten fragment kodu zmieni się w przyszłości. Mogę to „zamknąć” po prostu, definiując nową procedurę:

void speak(pet) {
  if (pet.type() == dog) {
    pet.bark();
  } else if (pet.type() == cat) {
    pet.meow();
  } else if (pet.type() == duck) {
    pet.quack()
  }
}

i stosując tę ​​nową procedurę zamiast bloku kodu (tj. refaktoryzację metodą „wyodrębniania”). W tym momencie dodanie typu „krowa” lub cokolwiek innego wymaga jedynie zaktualizowania speakprocedury. Oczywiście w języku OO możesz zamiast tego skorzystać z dynamicznej wysyłki, o czym wspomina odpowiedź CandiedOrange. Stanie się to naturalnie, jeśli uzyskasz dostęp petprzez interfejs. Eliminacja logiki warunkowej za pomocą dynamicznej wysyłki jest kwestią ortogonalną, która była częścią tego, dlaczego dokonałem tego proceduralnego wykonania. Chcę również podkreślić, że nie wymaga to cech charakterystycznych dla OOP. Nawet w języku OO hermetyzacja tego, co się różni, niekoniecznie oznacza, że ​​należy utworzyć nową klasę lub interfejs.

Jako bardziej archetypowy przykład (który jest bliższy, ale nie do końca OO), powiedzmy, że chcemy usunąć duplikaty z listy. Załóżmy, że wdrażamy to, iterując listę, śledząc przedmioty, które widzieliśmy do tej pory na innej liście, i usuwając wszystkie, które widzieliśmy. Rozsądnie jest założyć, że możemy chcieć zmienić sposób śledzenia obserwowanych przedmiotów, przynajmniej z powodów związanych z wydajnością. Wymóg zawarcia tego, co się różni, sugeruje, że powinniśmy zbudować abstrakcyjny typ danych, który reprezentowałby zestaw widocznych elementów. Nasz algorytm jest teraz zdefiniowany w oparciu o ten abstrakcyjny zestaw danych, a jeśli zdecydujemy się przejść na drzewo wyszukiwania binarnego, nasz algorytm nie musi się zmieniać ani się tym przejmować. W języku OO możemy użyć klasy lub interfejsu do przechwycenia tego abstrakcyjnego typu danych. W języku takim jak SML / O ”

W przykładzie opartym na wymaganiach powiedz, że musisz zweryfikować niektóre pola w odniesieniu do logiki biznesowej. Chociaż możesz mieć teraz określone wymagania, mocno podejrzewasz, że będą ewoluować. Możesz zawrzeć aktualną logikę we własnej procedurze / funkcji / regule / klasie.

Chociaż jest to problem ortogonalny, który nie jest częścią „enkapsulacji tego, co się różni”, często naturalne jest wyodrębnienie, które jest sparametryzowane przez teraz enkapsulowaną logikę. Zwykle prowadzi to do bardziej elastycznego kodu i pozwala na zmianę logiki poprzez zastąpienie jej alternatywną implementacją zamiast modyfikowania logiki zamkniętej.

Derek Elkins
źródło
Och, gorzka słodka ironio. Tak, nie jest to wyłącznie kwestia OOP. Przyłapałeś mnie na podawaniu szczegółów paradygmatu języka na moją odpowiedź i słusznie ukarałeś mnie za to poprzez „zmianę” paradygmatu.
candied_orange
„Nawet w języku OO kapsułkowanie tego, co się różni, niekoniecznie oznacza konieczność utworzenia nowej klasy lub interfejsu” - trudno sobie wyobrazić sytuację, w której brak utworzenia nowej klasy lub interfejsu nie naruszy SRP
taurelas
11

„Hermetyzuj to, co się zmienia” odnosi się do ukrywania szczegółów implementacji, które mogą się zmieniać i ewoluować.

Przykład:

Załóżmy na przykład, że klasa Courseśledzi, Studentsczy można zarejestrować (). Możesz go zaimplementować za pomocą a LinkedListi odsłonić kontener, aby umożliwić iterację na nim:

class Course { 
    public LinkedList<Student> Atendees; 
    public bool register (Student s);  
    ...
}

Ale to nie jest dobry pomysł:

  • Po pierwsze, ludzie mogliby nie mieć dobrego zachowania i używać go jako samoobsługi, bezpośrednio dodając uczniów do listy, bez przechodzenia przez metodę register ().
  • Ale jeszcze bardziej irytujące: stwarza to zależność „używania kodu” od wewnętrznych szczegółów implementacji używanej klasy. Może to zapobiec przyszłej ewolucji klasy, na przykład, jeśli wolisz użyć tablicy, wektora, mapy z numerem miejsca lub własnej trwałej struktury danych.

Jeśli hermetyzujesz to, co się różni (a raczej powiedział, co może się różnić), zachowujesz swobodę zarówno przy użyciu kodu, jak i enkapsulowanej klasy, aby samodzielnie ewoluowały. Dlatego jest to ważna zasada w OOP.

Dodatkowe czytanie:

Christophe
źródło