Stosowanie zasad SOLID

13

Jestem całkiem nowy w zasadach projektowania SOLID . Rozumiem ich przyczynę i korzyści, ale jednak nie stosuję ich do mniejszego projektu, który chciałbym zreformować jako praktyczne ćwiczenie z wykorzystaniem zasad SOLID. Wiem, że nie ma potrzeby zmiany aplikacji, która działa idealnie, ale i tak chcę ją zrefaktoryzować, aby zyskać doświadczenie projektowe dla przyszłych projektów.

Aplikacja ma następujące zadanie (właściwie o wiele więcej, ale bądźmy prostsze): musi odczytać plik XML zawierający definicje tabeli bazy danych / kolumny / widoku itp. I utworzyć plik SQL, którego można użyć do utworzenia schemat bazy danych ORACLE.

(Uwaga: powstrzymaj się od dyskusji, dlaczego go potrzebuję lub dlaczego nie używam XSLT i tak dalej, istnieją powody, ale są one nie na temat).

Na początek postanowiłem spojrzeć tylko na tabele i ograniczenia. Jeśli zignorujesz kolumny, możesz podać je w następujący sposób:

Ograniczenie jest częścią tabeli (a ściślej częścią instrukcji CREATE TABLE), a ograniczenie może również odnosić się do innej tabeli.

Najpierw wyjaśnię, jak teraz wygląda aplikacja (nie stosując SOLID):

W tej chwili aplikacja ma klasę „Tabela”, która zawiera listę wskaźników do ograniczeń należących do tabeli oraz listę wskaźników do ograniczeń odnoszących się do tej tabeli. Za każdym razem, gdy zostanie ustanowione połączenie, zostanie również ustanowione połączenie wsteczne. Tabela ma metodę createStatement (), która z kolei wywołuje funkcję createStatement () każdego ograniczenia. Wspomniana metoda sama użyje połączeń z tabelą właściciela i tabelą odniesienia, aby pobrać ich nazwy.

Oczywiście nie dotyczy to w ogóle SOLID. Na przykład istnieją zależności cykliczne, które rozprężają kod pod względem wymaganych metod „dodawania” / „usuwania” i niektórych destrukterów dużych obiektów.

Jest więc kilka pytań:

  1. Czy powinienem rozwiązać zależności cykliczne za pomocą wstrzykiwania zależności? Jeśli tak, to przypuszczam, że Ograniczenie powinno otrzymać tabelę właściciela (i opcjonalnie odnośnik) w swoim konstruktorze. Ale jak w takim razie mógłbym przeglądać listę ograniczeń dla pojedynczej tabeli?
  2. Jeśli zarówno klasa Table przechowuje stan samego siebie (np. Nazwę tabeli, komentarz do tabeli itp.), Jak i linki do Ograniczeń, to czy te jedno lub dwa „obowiązki” mają na myśli zasadę pojedynczej odpowiedzialności?
  3. W przypadku, gdy 2. ma rację, czy powinienem po prostu utworzyć nową klasę w logicznej warstwie biznesowej, która zarządza łączami? Jeśli tak, 1. oczywiście nie byłoby już istotne.
  4. Czy metody „createStatement” powinny być częścią klas Table / Constraint, czy też mam je przenieść? Jeśli tak to gdzie? Jedna klasa menedżera na każdą klasę przechowywania danych (tj. Tabela, ograniczenie, ...)? Czy raczej utworzyć klasę menedżera dla linku (podobną do 3.)?

Ilekroć próbuję odpowiedzieć na jedno z tych pytań, znajduję się gdzieś w kółko.

Problem oczywiście staje się o wiele bardziej złożony, jeśli uwzględnisz kolumny, indeksy i tak dalej, ale jeśli pomożecie mi z prostą kwestią związaną z tabelą / ograniczeniami, może resztę samodzielnie opracuję.

Tim Meyer
źródło
3
Jakiego języka używasz? Czy możesz opublikować przynajmniej trochę kodu szkieletu? Bardzo trudno jest omówić jakość kodu i możliwe refaktoryzacje, nie widząc rzeczywistego kodu.
Péter Török,
Używam C ++, ale starałem się trzymać go z dyskusji, jak można mieć tego problemu w dowolnym języku
Tim Meyer
Tak, ale stosowanie wzorców i refaktoryzacji zależy od języka. Np. @ Back2dos zasugerował AOP w swojej odpowiedzi poniżej, co oczywiście nie dotyczy C ++.
Péter Török
Więcej informacji o zasadach SOLID można znaleźć na stronie programmers.stackexchange.com/questions/155852/ ...
LCJ

Odpowiedzi:

8

Możesz zacząć od innego punktu widzenia, aby zastosować tutaj „zasadę pojedynczej odpowiedzialności”. To, co nam pokazałeś, to (mniej więcej) tylko model danych Twojej aplikacji. SRP oznacza tutaj: upewnij się, że twój model danych jest odpowiedzialny tylko za przechowywanie danych - nie mniej, nie więcej.

Więc jeśli masz zamiar czytać plik XML, stworzyć model z niego dane i pisać SQL, co powinno nie wystarczy wdrożyć coś do swojej Tableklasy, która jest specyficzna XML lub SQL. Chcesz, aby przepływ danych wyglądał tak:

[XML] -> ("Read XML") -> [Data model of DB definition] -> ("Write SQL") -> [SQL]

Zatem jedynym miejscem, w którym należy umieścić kod specyficzny dla XML, jest klasa o nazwie na przykład Read_XML. Jedynym miejscem dla kodu specyficznego dla SQL powinna być klasa Write_SQL. Oczywiście, być może zamierzasz podzielić te 2 zadania na więcej pod-zadań (i podzielić swoje klasy na wiele klas menedżerów), ale twój „model danych” nie powinien ponosić żadnej odpowiedzialności z tej warstwy. Więc nie dodawaj a createStatementdo żadnej z klas modelu danych, ponieważ daje to twojemu modelowi odpowiedzialność za SQL.

Nie widzę żadnego problemu, kiedy opisujesz, że Tabela jest odpowiedzialna za przechowywanie wszystkich jej części (nazwa, kolumny, komentarze, ograniczenia ...), taka jest idea modelu danych. Ale opisałeś, że „Tabela” jest również odpowiedzialna za zarządzanie pamięcią niektórych jej części. Jest to problem specyficzny dla C ++, z którym nie spotkałbyś się tak łatwo w językach takich jak Java lub C #. Sposobem C ++ na pozbycie się tej odpowiedzialności jest użycie inteligentnych wskaźników, przekazanie własności na inną warstwę (na przykład bibliotekę boost lub na własną „inteligentną” warstwę wskaźnika). Ale uwaga, twoje cykliczne zależności mogą „drażnić” niektóre inteligentne implementacje wskaźników.

Coś więcej o SOLID: oto fajny artykuł

http://cre8ivethought.com/blog/2011/08/23/software-development-is-not-a-jenga-game

wyjaśniając SOLID na małym przykładzie. Spróbujmy zastosować to w twoim przypadku:

  • trzeba nie tylko klasy Read_XMLi Write_SQL, ale także trzecią klasę, która zarządza oddziaływanie tych 2 klas. Nazwijmy to ConversionManager.

  • Stosując zasadę DI może oznaczać tutaj: ConversionManager nie powinny tworzyć instancje Read_XMLi Write_SQLprzez siebie. Zamiast tego obiekty te można wstrzykiwać przez konstruktor. Konstruktor powinien mieć taki podpis

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

gdzie IDataModelReaderjest interfejsem, z którego Read_XMLdziedziczy, i IDataModelWriterto samo dla Write_SQL. To sprawia, że ​​są ConversionManagerotwarte na rozszerzenia (bardzo łatwo udostępniasz różnym czytelnikom lub pisarzom) bez konieczności ich zmiany - więc mamy przykład zasady otwartej / zamkniętej. Pomyśl o tym, co będziesz musiał zmienić, jeśli chcesz wesprzeć innego dostawcę bazy danych - w zasadzie nie musisz niczego zmieniać w swoim modelu danych, po prostu podaj innego SQL-Writera.

Doktor Brown
źródło
Chociaż jest to bardzo rozsądne ćwiczenie SOLID, (w górę) zauważam, że narusza ono „old school Kay / Holub OOP”, ponieważ wymaga pobierania i ustawiania dla dość anemicznego modelu danych. Przypomina mi również niesławny rant Steve'a Yegge .
user949300,
2

W takim przypadku powinieneś zastosować S SOLID.

Tabela zawiera wszystkie zdefiniowane ograniczenia. Ograniczenie zawiera wszystkie tabele, do których się odwołuje. Prosty i prosty model.

W tym tkwi zdolność do wykonywania odwrotnych wyszukiwań, tj. Do ustalenia, z jakimi ograniczeniami odnosi się do niektórych tabel.
Więc tak naprawdę chcesz usługi indeksowania. To jest zupełnie inne zadanie i dlatego powinno być realizowane przez inny przedmiot.

Aby rozbić go na bardzo uproszczoną wersję:

class Table {
      void addConstraint(Constraint constraint) { ... }
      bool removeConstraint(Constraint constraint) { ... }
      Iterator<Constraint> getConstraints() { ... }
}
class Constraint {
      //actually I am not so sure these two should be exposed directly at all
      void addReference(Table to) { ... }
      bool removeReference(Table to) { ... }
      Iterator<Table> getReferencedTables() { ... }
}
class Database {
      void addTable(Table table) { ... }
      bool removeTable(Table table) { ... }
      Iterator<Table> getTables() { ... }
}
class Index {
      Iterator<Constraint> getConstraintsReferencing(Table target) { ... }
}

Jeśli chodzi o implementację indeksu, istnieją 3 sposoby:

  • getContraintsReferencingmetoda może naprawdę tylko pełzać cała Databasedla Tableinstancji i indeksowanie ich Constraints, aby uzyskać wynik. W zależności od tego, jak to jest kosztowne i jak często go potrzebujesz, może to być opcja.
  • może również użyć pamięci podręcznej. Jeśli model bazy danych może ulec zmianie po zdefiniowaniu, można zachować pamięć podręczną, uruchamiając sygnały z odpowiednich instancji Tablei Constraintinstancji, gdy ulegną zmianie. Nieco prostszym rozwiązaniem byłoby Indexzbudowanie „indeksu migawek” całości Databasedo pracy, który następnie należy odrzucić. Jest to oczywiście możliwe tylko wtedy, gdy aplikacja robi duże rozróżnienie między „czasem modelowania” a „czasem zapytania”. Jeśli raczej możliwe jest wykonanie tych dwóch jednocześnie, nie jest to wykonalne.
  • Inną opcją byłoby użycie AOP do przechwycenia wszystkich wywołań tworzenia i odpowiedniego utrzymania indeksu.
back2dos
źródło
Bardzo szczegółowa odpowiedź, jak na razie podoba mi się twoje rozwiązanie! Co byś pomyślał, gdybym wykonał DI dla klasy Table, podając jej listę ograniczeń podczas budowy? W każdym razie mam klasę TableParser, która może działać jako fabryka lub współpracować z fabryką w tym przypadku.
Tim Meyer,
@Tim Meyer: DI niekoniecznie jest wstrzykiwaniem konstruktora. DI można również wykonać za pomocą funkcji składowych. To, czy Tabela powinna uzyskać wszystkie swoje części za pośrednictwem konstruktora, zależy od tego, czy chcesz dodawać te części tylko w czasie budowy i nigdy nie zmieniać ich później, czy też chcesz utworzyć tabelę krok po kroku. To powinno być podstawą twojej decyzji projektowej.
Doc Brown,
1

Lekarstwem na okrągłe zależności jest ślubowanie, że nigdy, nigdy ich nie stworzysz. Uważam, że testowanie kodu jako pierwszego jest silnym czynnikiem odstraszającym.

W każdym razie zależności okrągłe można zawsze zerwać, wprowadzając abstrakcyjną klasę podstawową. Jest to typowe dla reprezentacji graficznych. Tutaj tabele są węzłami, a ograniczenia klucza obcego są krawędziami. Utwórz więc abstrakcyjną klasę tabeli i abstrakcyjną klasę ograniczeń, a może abstrakcyjną klasę kolumny. Wtedy wszystkie implementacje mogą zależeć od klas abstrakcyjnych. To może nie być najlepsza możliwa reprezentacja, ale jest to poprawa w stosunku do klas wzajemnie powiązanych.

Ale, jak podejrzewasz, najlepsze rozwiązanie tego problemu może nie wymagać śledzenia związków między obiektami. Jeśli chcesz tylko przetłumaczyć XML na SQL, nie potrzebujesz reprezentacji wykresu ograniczeń w pamięci. Wykres ograniczeń byłby miły, gdybyś chciał uruchomić algorytmy grafowe, ale nie wspomniałeś o tym, więc założę, że nie jest to wymagane. Potrzebujesz tylko listy tabel i listy ograniczeń oraz gościa dla każdego dialektu SQL, który chcesz obsługiwać. Wygeneruj tabele, a następnie wygeneruj ograniczenia zewnętrzne względem tabel. Dopóki wymagania się nie zmienią, nie będę miał problemu z podłączeniem generatora SQL do DOM XML. Zaoszczędź jutro na jutro.

Kevin Cline
źródło
Tutaj zaczyna się gra „(właściwie o wiele więcej, ale bądźmy prościej)”. Na przykład są przypadki, w których muszę usunąć tabelę, więc muszę sprawdzić, czy jakieś ograniczenia odnoszą się do tej tabeli.
Tim Meyer,