SOLID Zasady i struktura kodu

150

Podczas ostatniej rozmowy o pracę nie mogłem odpowiedzieć na pytanie o SOLID - poza podaniem podstawowego znaczenia różnych zasad. Naprawdę mnie to wkurza. Zrobiłem kilka dni, żeby się rozejrzeć i jeszcze nie opracowałem satysfakcjonującego podsumowania.

Pytanie do wywiadu brzmiało:

Jeśli spojrzałbyś na projekt .Net, o którym mówiłem, że ściśle przestrzegasz zasad SOLID, czego byś oczekiwał pod względem projektu i struktury kodu?

Kręciłem się trochę, tak naprawdę nie odpowiedziałem na pytanie, a potem zbombardowałem.

Jak mogłem lepiej poradzić sobie z tym pytaniem?

Jednostka S.
źródło
3
Zastanawiałem się, co nie jest jasne na stronie wiki dla SOLID
BЈовић
Rozszerzalne abstrakcyjne bloki konstrukcyjne.
rwong
Przestrzegając SOLIDNYCH zasad projektowania obiektowego, twoje klasy będą naturalnie małe, dobrze przemyślane i łatwe do przetestowania. Źródło: docs.asp.net/en/latest/fundamentals/…
WhileTrueSleep

Odpowiedzi:

188

S = zasada pojedynczej odpowiedzialności

Spodziewałbym się więc dobrze zorganizowanej struktury folderów / plików i hierarchii obiektów. Każdą klasę / element funkcjonalności należy nazwać, że jego funkcjonalność jest bardzo oczywista i powinna zawierać jedynie logikę do wykonania tego zadania.

Gdybyście zobaczyli ogromne klasy menedżerskie z tysiącem linii kodu, byłby to znak, że pojedyncza odpowiedzialność nie była przestrzegana.

O = zasada otwarta / zamknięta

Jest to w zasadzie pomysł, aby nowa funkcjonalność była dodawana poprzez nowe klasy, które mają minimalny wpływ na / wymagają modyfikacji istniejącej funkcjonalności.

Spodziewałbym się wielu zastosowań dziedziczenia obiektów, podtypów, interfejsów i klas abstrakcyjnych, aby oddzielić projekt elementu funkcjonalności od faktycznej implementacji, umożliwiając innym tworzenie i wdrażanie innych wersji obok, bez wpływu na oryginał.

L = zasada podstawienia Liskowa

Ma to związek z możliwością traktowania podtypów jako typu nadrzędnego. To wychodzi z pudełka w języku C #, jeśli implementujesz odpowiednią dziedziczoną hierarchię obiektów.

Spodziewałbym się zobaczyć kod traktujący wspólne obiekty jako ich typ podstawowy i wywołujące metody w klasach base / abstract zamiast tworzenia instancji i pracy na samych podtypach.

I = zasada segregacji interfejsu

Jest to podobne do SRP. Zasadniczo definiujesz mniejsze podzbiory funkcjonalności jako interfejsy i pracujesz z nimi, aby utrzymać system w stanie oddzielonym (np. FileManagerMoże mieć pojedynczą odpowiedzialność za obsługę plików we / wy, ale może zaimplementować IFileReadera, IFileWriterktóry zawiera konkretne definicje metod do odczytu i pisanie plików).

D = zasada inwersji zależności.

Ponownie odnosi się to do utrzymania oddzielenia systemu. Być może chcesz być na poszukiwania wykorzystaniem biblioteki .NET Dependency wtrysk, używane w roztworze, takie jak Unitylub Ninjectczy system ServiceLocator takich jak AutoFacServiceLocator.

Eoin Campbell
źródło
36
Widziałem wiele naruszeń LSP w C #, za każdym razem, gdy ktoś decyduje, że jego konkretny podtyp jest wyspecjalizowany, a zatem nie musi implementować fragmentu interfejsu, a zamiast tego rzuca wyjątek na ten kawałek ... To jest powszechne podejście młodsze do rozwiązania problemu nieporozumień związanych z implementacją i projektowaniem interfejsu
Jimmy Hoffa
2
@JimmyHoffa To jeden z głównych powodów, dla których nalegam na stosowanie Kodeksów; przejrzenie przemyślanego procesu projektowania umów pomaga wielu ludziom wyciągnąć ten zły nawyk.
Andy
12
Nie podoba mi się, że „LSP wychodzi z pudełka w języku C #” i utożsamia DIP z praktyką wstrzykiwania zależności.
Euforia
3
+1, ale odwrócenie zależności <> wstrzyknięcie zależności. Grają dobrze razem, ale inwersja zależności jest czymś więcej niż tylko zastrzykiem zależności. Odniesienie: DIP na wolności
Marjan Venema
3
@Andy: pomocne są również testy jednostkowe zdefiniowane na interfejsach, na których testowane są wszystkie implementatory (dowolna klasa, która może / jest tworzona).
Marjan Venema
17

Wiele małych klas i interfejsów z zastrzykiem zależności w każdym miejscu. Prawdopodobnie w dużym projekcie użyłbyś również frameworka IoC, aby pomóc Ci w konstruowaniu i zarządzaniu żywotnością wszystkich tych małych obiektów. Zobacz https://stackoverflow.com/questions/21288/which-net-dependency-injection-frameworks-are-worth-looking-into

Zauważ, że duży projekt .NET, który ŚCISŁO przestrzega zasad SOLID, niekoniecznie oznacza dobrą bazę kodu do współpracy z każdym. W zależności od tego, kim był ankieter, mógł / ona chcieć, abyś wykazał, że rozumiesz, co oznacza SOLID i / lub sprawdź, jak dogmatycznie przestrzegasz zasad projektowania.

Widzisz, aby być SOLIDNYM, musisz przestrzegać:

S Ingle zasada odpowiedzialności, więc trzeba będzie wiele małych grupach każdy z nich robi tylko jedno

O zasada zamkniętego pióra, która w .NET jest zwykle implementowana z iniekcją zależności, która również wymaga I i D poniżej ...

Zasada podstawienia L iskova jest prawdopodobnie niemożliwa do wyjaśnienia w c # za pomocą jednowierszowej. Na szczęście istnieją inne pytania dotyczące tego problemu, np. Https://stackoverflow.com/questions/4428725/can-you-explain-liskov-substitution-principle-with-a-good-c-sharp-example

I nterface Zasada segregacji działa w tandemie z zasadą otwartego i zamkniętego. Gdyby postępować dosłownie, oznaczałoby to preferowanie dużej liczby bardzo małych interfejsów zamiast kilku „dużych” interfejsów

D ependency zasada inwersji zajęcia na wysokim poziomie nie powinno być uzależnione od klasy niskopoziomowych, zarówno powinna zależeć od abstrakcji.

Paolo Falabella
źródło
SRP nie oznacza „zrób tylko jedną rzecz”.
Robert Harvey
13

Kilka podstawowych rzeczy, których spodziewałbym się zobaczyć w bazie kodu sklepu, który promował SOLID w swojej codziennej pracy:

  • Wiele małych plików kodu - z jedną klasą na plik jako najlepszą praktyką w .NET i Zasadą Pojedynczej Odpowiedzialności zachęcającą do małych modułowych struktur klas, spodziewałbym się zobaczyć wiele plików zawierających jedną małą, skoncentrowaną klasę.
  • Wiele wzorców adapterów i kompozytów - spodziewałbym się użycia wielu wzorców adapterów (klasa implementująca jeden interfejs poprzez „przejście” do funkcjonalności innego interfejsu) w celu usprawnienia podłączania zależności opracowanej dla jednego celu w nieco różne miejsca, w których jego funkcjonalność jest również potrzebna. Aktualizacje tak proste, jak zastąpienie rejestratora konsoli rejestratorem plików, będą naruszać LSP / ISP / DIP, jeśli interfejs zostanie zaktualizowany, aby udostępnić sposób określania nazwy pliku do użycia; zamiast tego klasa rejestratora plików ujawni dodatkowe elementy, a następnie adapter sprawi, że rejestrator plików będzie wyglądał jak rejestrator konsoli, ukrywając nowe elementy, więc tylko obiekt przyciągający to wszystko musi znać różnicę.

    Podobnie, gdy klasa musi dodać zależność interfejsu podobnego do istniejącego, aby uniknąć zmiany obiektu (OCP), zwykle odpowiedzią jest implementacja wzorca Composite / Strategy (klasa implementująca interfejs zależności i zużywająca wiele innych implementacje tego interfejsu, z różną logiką pozwalającą klasie na przekazanie wywołania do jednej, niektórych lub wszystkich implementacji).

  • Wiele interfejsów i ABC - DIP koniecznie wymaga istnienia abstrakcji, a ISP zachęca do ich wąskiego zakresu. Dlatego interfejsy i abstrakcyjne klasy podstawowe są regułą i będziesz potrzebował ich wiele, aby pokryć funkcje współzależności twojej bazy kodu. Chociaż ścisłe SOLID wymaga wstrzyknięcia wszystkiego , oczywiste jest, że trzeba gdzieś utworzyć, więc jeśli formularz GUI jest kiedykolwiek tworzony tylko jako dziecko jednego formularza nadrzędnego poprzez wykonanie jakiejś akcji na tym rodzicu, nie mam żadnych wątpliwości, że formularz podrzędny jest nowy z kodu bezpośrednio w rodzicu. Po prostu zwykle tworzę ten kod jako własną metodę, więc jeśli dwie akcje tego samego formularza kiedykolwiek otworzyły okno, po prostu wywołuję tę metodę.
  • Wiele projektów - celem tego wszystkiego jest ograniczenie zakresu zmian. Zmiana obejmuje konieczność ponownej kompilacji (ćwiczenie jest już stosunkowo trywialne, ale nadal ważne w wielu operacjach krytycznych dla procesora i przepustowości, takich jak wdrażanie aktualizacji w środowisku mobilnym). Jeśli jeden plik w projekcie musi zostać odbudowany, wszystkie pliki tak robią. Oznacza to, że jeśli umieścisz interfejsy w tych samych bibliotekach, co ich implementacje, stracisz sens; będziesz musiał ponownie skompilować wszystkie użycia, jeśli zmienisz implementację interfejsu, ponieważ będziesz również ponownie skompilować samą definicję interfejsu, wymagając, aby wskazania wskazywały nową lokalizację w wynikowym pliku binarnym. Dlatego oddzielanie interfejsów od zastosowań i wdrożenia, przy czym dodatkowo segregują je według ogólnego obszaru zastosowania, są typową najlepszą praktyką.
  • Wiele uwagi poświęcono terminologii „Gang of Four” - wzorce projektowe zidentyfikowane w książce Design Patterns z 1994 roku podkreślają modułowy projekt kodu wielkości kęsa, który SOLID chce stworzyć. Na przykład zasada inwersji zależności i zasada otwarta / zamknięta stanowią sedno większości zidentyfikowanych wzorców w tej książce. Jako taki, spodziewałbym się, że sklep, który ściśle przestrzega zasad SOLID, będzie również obejmował terminologię w książce Gang of Four i nazwał klasy zgodnie z ich funkcją według tych linii, takich jak „AbcFactory”, „XyzRepository”, „DefToXyzAdapter ”,„ A1Command ”itp.
  • Ogólne repozytorium - Zgodnie z powszechnie rozumianym ISP, DIP i SRP, Repozytorium jest prawie wszechobecne w projektowaniu SOLID, ponieważ pozwala na pobieranie kodu w celu abstrakcyjnego sposobu zapytania o klasy danych bez potrzeby szczegółowej wiedzy o mechanizmie pobierania / trwałości oraz umieszcza kod, który robi to w jednym miejscu, w przeciwieństwie do wzorca DAO (w którym, na przykład, gdybyś miał klasę danych Faktury, miałbyś również Fakturę DAO, która produkowała uwodnione obiekty tego typu i tak dalej dla wszystkie obiekty / tabele danych w bazie kodu / schemacie).
  • Kontener IoC - waham się, aby go dodać, ponieważ tak naprawdę nie używam frameworka IoC do wykonania większości wstrzyknięć zależności. Szybko staje się anty-wzorem Boga Przedmiotu, wrzucając wszystko do pojemnika, wstrząsając nim i wylewając nawodnioną zależność, której potrzebujesz, za pomocą fabrycznej metody wstrzykiwania. Brzmi świetnie, dopóki nie zdasz sobie sprawy, że struktura staje się dość monolityczna, a projekt z informacjami rejestracyjnymi, jeśli „płynny”, musi teraz wiedzieć wszystko o wszystkim w twoim rozwiązaniu. To wiele powodów do zmiany. Jeśli nie jest płynny (późna rejestracja przy użyciu plików konfiguracyjnych), to kluczowy element twojego programu opiera się na „magicznych ciągach”, zupełnie innych robakach.
KeithS
źródło
1
dlaczego opinie negatywne?
KeithS
Myślę, że to dobra odpowiedź. Zamiast być podobnym do wielu postów na blogu na temat tych warunków, wymieniono przykłady i objaśnienia, które pokazują ich wykorzystanie i wartość
Crowie
10

Odciągnij ich uwagę od dyskusji Jona Skeeta o tym, jak „O” w SOLID jest „nieprzydatny i źle zrozumiany”, i poproś ich, aby mówili o „chronionej wariacji” Alistaira Cockburna i „projekcie odziedziczonym przez Josha Blocha” lub zabraniają go.

Krótkie streszczenie artykułu Skeeta (choć nie poleciłbym porzucenia jego nazwiska bez przeczytania oryginalnego posta na blogu!):

  • Większość ludzi nie wie, co oznaczają „otwarte” i „zamknięte” w „zasadzie otwartego zamknięcia”, nawet jeśli myślą, że tak.
  • Typowe interpretacje obejmują:
    • moduły należy zawsze rozszerzać poprzez dziedziczenie implementacji, lub
    • że kodu źródłowego oryginalnego modułu nigdy nie można zmienić.
  • Podstawowa intencja OCP i jej oryginalne sformułowanie przez Bertranda Meyera jest w porządku:
    • moduły powinny mieć dobrze zdefiniowane interfejsy (niekoniecznie w sensie technicznym „interfejsu”), od których mogą polegać ich klienci, ale
    • powinno być możliwe rozszerzenie tego, co mogą zrobić, bez zerwania tych interfejsów.
  • Ale słowa „otwarty” i „zamknięty” po prostu mylą sprawę, nawet jeśli tworzą ładny wymowny akronim.

OP zapytał: „Jak mógłbym lepiej poradzić sobie z tym pytaniem?” Jako starszy inżynier prowadzący rozmowę byłbym niezmiernie bardziej zainteresowany kandydatem, który potrafi inteligentnie mówić o zaletach i wadach różnych stylów projektowania kodu, niż kimś, kto może wyrzucić listę punktów.

Inną dobrą odpowiedzią byłoby: „Cóż, to zależy od tego, jak dobrze to zrozumieli. Jeśli wszystko, co wiedzą, to modne słowa SOLID, spodziewałbym się nadużycia dziedziczenia, nadużywania struktur wstrzykiwania zależności, miliona małych interfejsów, z których żaden nie odzwierciedlają słownictwo domenowe używane do komunikowania się z zarządzaniem produktem… ”

David Moles
źródło
6

Prawdopodobnie istnieje wiele sposobów, na które można na nie odpowiedzieć w różnym czasie. Myślę jednak, że jest to bardziej podobne do „Czy wiesz, co oznacza SOLID?” Zatem odpowiedź na to pytanie prawdopodobnie sprowadza się do trafienia w punkty i wyjaśnienia go w kontekście projektu.

Oczekujesz więc, że:

  • Klasy mają jedną odpowiedzialność (np. Klasa dostępu do danych dla klientów będzie pobierać dane klientów tylko z bazy danych klientów).
  • Zajęcia można łatwo przedłużyć bez wpływu na istniejące zachowanie. Nie muszę modyfikować właściwości ani innych metod w celu dodania dodatkowej funkcjonalności.
  • Klasy pochodne można zastąpić klasami podstawowymi, a funkcje korzystające z tych klas bazowych nie muszą rozpakowywać klasy bazowej do bardziej specyficznego typu w celu obsługi ich.
  • Interfejsy są małe i łatwe do zrozumienia. Jeśli klasa korzysta z interfejsu, nie musi polegać na kilku metodach do wykonania zadania.
  • Kod jest na tyle abstrakcyjny, że implementacja wysokiego poziomu nie zależy konkretnie od konkretnej implementacji niskiego poziomu. Powinienem być w stanie przełączyć implementację niskiego poziomu bez wpływu na kod wysokiego poziomu. Na przykład mogę zmienić warstwę dostępu do danych SQL na warstwę opartą na usłudze sieci Web bez wpływu na resztę mojej aplikacji.
edytor kodów kreskowych
źródło
4

To doskonałe pytanie, choć myślę, że jest to trudne pytanie podczas rozmowy kwalifikacyjnej.

Zasady SOLID naprawdę rządzą klasami i interfejsami oraz tym, w jaki sposób są ze sobą powiązane.

To pytanie naprawdę dotyczy bardziej plików, a niekoniecznie klas.

Krótka spostrzeżenie lub odpowiedź, którą chciałbym udzielić, to to, że generalnie zobaczysz pliki, które zawierają tylko interfejs, a często konwencja polega na tym, że zaczynają się od dużej litery. Poza tym wspomnę, że pliki nie miałyby zduplikowanego kodu (szczególnie w module, aplikacji lub bibliotece) i że kod byłby ostrożnie dzielony między pewnymi granicami między modułami, aplikacjami lub bibliotekami.

Robert Martin omawia ten temat w dziedzinie C ++ w projektowaniu aplikacji C ++ zorientowanych zorientowanych za pomocą metody Booch (patrz rozdziały na temat spójności, zamykania i ponownego użycia) oraz w czystym kodzie .

J. Polfer
źródło
Kodery .NET IME zwykle przestrzegają reguły „1 klasa na plik”, a także odzwierciedlają struktury folderów / przestrzeni nazw; Visual Studio IDE zachęca do obu praktyk, a różne wtyczki, takie jak ReSharper, mogą je egzekwować. Oczekiwałbym więc, że struktura projektu / pliku będzie odzwierciedlać strukturę klasy / interfejsu.
KeithS,