Zapisujesz obiekt własną metodą lub inną klasą?

38

Jeśli chcę zapisać i pobrać obiekt, czy powinienem utworzyć inną klasę do obsługi go, czy lepiej byłoby to zrobić w samej klasie? A może mieszanie obu?

Które jest zalecane zgodnie z paradygmatem OOD?

Na przykład

Class Student
{
    public string Name {set; get;}
    ....
    public bool Save()
    {
        SqlConnection con = ...
        // Save the class in the db
    }
    public bool Retrieve()
    {
         // search the db for the student and fill the attributes
    }
    public List<Student> RetrieveAllStudents()
    {
         // this is such a method I have most problem with it 
         // that an object returns an array of objects of its own class!
    }
}

Przeciw. (Wiem, że zalecane są następujące, jednak wydaje mi się to trochę sprzeczne ze spójnością Studentklasy)

Class Student { /* */ }
Class DB {
  public bool AddStudent(Student s)
  {

  }
  public Student RetrieveStudent(Criteria)
  {
  } 
  public List<Student> RetrieveAllStudents()
  {
  }
}

Co powiesz na ich mieszanie?

   Class Student
    {
        public string Name {set; get;}
        ....
        public bool Save()
        {
            /// do some business logic!
            db.AddStudent(this);
        }
        public bool Retrieve()
        {
             // build the criteria 
             db.RetrieveStudent(criteria);
             // fill the attributes
        }
    }
Ahmad
źródło
3
Powinieneś zbadać wzór Active Record, taki jak to pytanie lub to
Eric King,
@gnat, pomyślałem, że pytanie, które z nich jest lepsze, opiera się na opiniach, a następnie dodałem „plusy i minusy” i nie mam problemu z jego usunięciem, ale w rzeczywistości mam na myśli paradygmat OOD, który jest zalecany
Ahmad
3
Nie ma poprawnej odpowiedzi. To zależy od twoich potrzeb, twoich preferencji, tego, jak ma być używana klasa i gdzie widzisz swój projekt. Jedyną prawidłową rzeczą jest to, że możesz zapisywać i pobierać jako obiekty.
Dunk

Odpowiedzi:

34

Zasada jednolitej odpowiedzialności , rozdzielenie obaw i spójność funkcjonalna . Jeśli przeczytasz te pojęcia, otrzymasz odpowiedź: Rozdziel je .

Prostym powodem, dla którego należy oddzielić Studentklasę „DB” (lub StudentRepository, aby przestrzegać bardziej popularnych konwencji), jest umożliwienie zmiany „reguł biznesowych” obecnych w Studentklasie, bez wpływu na kod odpowiedzialny za trwałość i vice- versa.

Ten rodzaj separacji jest bardzo ważny, nie tylko między regułami biznesowymi a trwałością, ale także między wieloma problemami w systemie, aby umożliwić wprowadzanie zmian przy minimalnym wpływie na niezwiązane moduły (minimalne, ponieważ czasami jest to nieuniknione). Pomaga budować bardziej niezawodne systemy, które są łatwiejsze w utrzymaniu i bardziej niezawodne w przypadku ciągłych zmian.

Łącząc reguły biznesowe i uporczywość razem, albo jedną klasę jak w twoim pierwszym przykładzie, lub DBzależnie od tego Student, łączysz dwie bardzo różne kwestie. Może się wydawać, że należą do siebie; wydają się być spójne, ponieważ używają tych samych danych. Ale o to chodzi: spójność nie może być mierzona wyłącznie danymi dzielonymi między procedurami, należy również wziąć pod uwagę poziom abstrakcji, na którym one istnieją. W rzeczywistości idealny rodzaj spójności jest opisany jako:

Spójność funkcjonalna ma miejsce, gdy części modułu są pogrupowane, ponieważ wszystkie one przyczyniają się do jednego dobrze zdefiniowanego zadania modułu.

I najwyraźniej, przeprowadzanie walidacji przez pewien Studentczas, a także utrwalanie go, nie tworzy „pojedynczego, dobrze zdefiniowanego zadania”. I znowu zasady biznesowe i mechanizmy trwałości to dwa bardzo różne aspekty systemu, które według wielu zasad dobrego projektowania obiektowego powinny być oddzielone.

Polecam przeczytać o czystej architekturze , obejrzeć ten wykład o zasadzie pojedynczej odpowiedzialności (w przypadku bardzo podobnego przykładu), a także obejrzeć ten wykład o czystej architekturze . Koncepcje te nakreślają przyczyny takich podziałów.

MichelHenrich
źródło
11
Ach, ale Studentklasa powinna być odpowiednio zamknięta, tak? Jak zatem klasa zewnętrzna może zarządzać trwałością stanu prywatnego? Hermetyzacja musi być zrównoważona w stosunku do SoC i SRP, ale po prostu wybór jednego z nich bez dokładnego rozważenia kompromisów jest prawdopodobnie niewłaściwy. Możliwym rozwiązaniem tej zagadki jest użycie prywatnych pakietów w celu użycia kodu trwałości.
amon
1
@amon Zgadzam się, że to nie jest takie proste, ale uważam, że jest to kwestia połączenia, a nie hermetyzacji. Na przykład można uczynić klasę Student abstrakcyjną, stosując abstrakcyjne metody dla dowolnych danych potrzebnych firmie, które są następnie wdrażane przez repozytorium. W ten sposób struktura danych DB jest całkowicie ukryta za implementacją repozytorium, więc jest nawet kapsułkowana z klasy Student. Ale jednocześnie repozytorium jest głęboko powiązane z klasą Student - jeśli zostaną zmienione jakiekolwiek metody abstrakcyjne, implementacja również musi ulec zmianie. Chodzi o kompromisy. :)
MichelHenrich,
4
Twierdzenie, że SRP ułatwia utrzymanie programów, jest co najmniej wątpliwe. Problem, jaki stwarza SRP, polega na tym, że zamiast posiadania zbioru dobrze nazwanych klas, w których nazwa mówi wszystko, co klasa może, a czego nie może zrobić (innymi słowy łatwo zrozumieć, co prowadzi do łatwego utrzymania), otrzymujesz setki / tysiące klas, które ludzie potrzebowali użyć tezaurusa, aby wybrać unikalną nazwę, wiele nazw jest podobnych, ale nie do końca, a sortowanie wszystkiego, co plewy jest uciążliwe. IOW, w ogóle nie do utrzymania, IMO.
Dunk
3
@Dunk mówisz tak, jakby klasy były jedyną dostępną jednostką modularyzacji. Widziałem i napisałem wiele projektów po SRP i zgadzam się, że twój przykład się zdarza, ale tylko wtedy, gdy nie używasz poprawnie pakietów i bibliotek. Jeśli podzielisz obowiązki na pakiety, ponieważ implikuje to kontekst, możesz ponownie swobodnie nazywać klasy. Następnie, aby utrzymać taki system, wystarczy wytropić się w odpowiednim pakiecie przed wyszukaniem klasy, którą należy zmienić. :)
MichelHenrich,
5
@Dunk OOD zasady można określić następująco: „Zasadniczo robienie tego jest lepsze ze względu na X, Y i Z”. Nie oznacza to, że należy go zawsze stosować; ludzie muszą być pragmatyczni i wyważać kompromisy. W dużych projektach korzyści są takie, że zwykle przewyższają krzywą uczenia się, o której mówisz - ale oczywiście nie jest to rzeczywistością dla większości ludzi, więc nie należy jej stosować tak mocno. Jednak nawet w przypadku lżejszych aplikacji SRP myślę, że oboje możemy się zgodzić, że oddzielenie uporczywości od reguł biznesowych zawsze przynosi korzyści przewyższające koszty (z wyjątkiem być może kodu wyrzucającego).
MichelHenrich,
10

Oba podejścia naruszają zasadę jednolitej odpowiedzialności. Twoja pierwsza wersja daje Studentklasie wiele obowiązków i łączy ją z konkretną technologią dostępu do bazy danych. Drugi prowadzi do ogromnej DBklasy, która będzie odpowiedzialna nie tylko za studentów, ale za wszelkie inne obiekty danych w twoim programie. EDYCJA: twoje trzecie podejście jest najgorsze, ponieważ tworzy cykliczną zależność między klasą DB a Studentklasą.

Więc jeśli nie zamierzasz pisać programu zabawkowego, nie używaj żadnego z nich. Zamiast tego użyj innej klasy, np. StudentRepositoryDo zapewnienia interfejsu API do ładowania i zapisywania, pod warunkiem, że sam zaimplementujesz kod CRUD. Możesz również rozważyć użycie struktury ORM , która może wykonać dla ciebie ciężką pracę (a struktura zazwyczaj będzie egzekwować niektóre decyzje, w których należy umieścić operacje ładowania i zapisywania).

Doktor Brown
źródło
Dziękuję, zmodyfikowałem odpowiedź, a co z trzecim podejściem? w każdym razie myślę, że tutaj chodzi o tworzenie wyraźnych warstw
Ahmad
Coś również sugeruje mi użycie StudentRepository zamiast DB (nie wiem dlaczego! Może znowu jedna odpowiedzialność), ale nadal nie mogę zrozumieć, dlaczego różne repozytorium dla każdej klasy? jeśli jest to tylko warstwa dostępu do danych, to jest więcej statycznych funkcji narzędziowych i mogą być razem, prawda? może wtedy stanie się tak brudna i zatłoczona klasa (przepraszam za mój angielski)
Ahmad
1
@Ahmad: istnieje kilka powodów oraz kilka zalet i wad do rozważenia. To może wypełnić całą książkę. Argumentacja sprowadza się do testowalności, łatwości konserwacji i długoterminowej ewolucji programów, szczególnie gdy mają one długi cykl życia.
Dok. Brown
Czy ktoś pracował nad projektem, w którym nastąpiła znacząca zmiana w technologii DB? (bez masywnego przepisywania?) Nie mam. Jeśli tak, to czy przestrzeganie SRP, jak sugerowano tutaj, aby uniknąć powiązania z tym DB, pomogło znacznie?
user949300
@ user949300: Myślę, że nie wyraziłem się jasno. Klasa ucznia nie jest powiązana z określoną technologią DB, lecz zostaje powiązana z konkretną technologią dostępu do DB, więc oczekuje czegoś takiego jak SQL DB dla trwałości. Utrzymywanie klasy Student całkowicie nieświadomy mechanizmu trwałości umożliwia znacznie łatwiejsze wykorzystanie klasy w różnych kontekstach. Na przykład w kontekście testowym lub w kontekście, w którym obiekty są przechowywane w pliku. I rzeczywiście tak często robiłem w przeszłości. Nie trzeba zmieniać całego stosu DB systemu, aby czerpać z tego korzyści.
Doc Brown
3

Istnieje wiele wzorców, które można wykorzystać do utrwalania danych. Istnieje wzorzec Jednostki Pracy, wzorzec Repozytorium , jest kilka dodatkowych wzorców, których można używać, takich jak Fasada zdalna i tak dalej.

Większość z nich ma swoich fanów i krytyków. Często chodzi o wybranie tego, co wydaje się najlepiej pasować do aplikacji i trzymanie się go (ze wszystkimi jego zaletami i wadami, tj. Nie używanie obu wzorów jednocześnie ... chyba że naprawdę jesteś tego pewien).

Na marginesie: w twoim przykładzie RetrieveStudent, AddStudent powinny być metodami statycznymi (ponieważ nie są zależne od instancji).

Innym sposobem posiadania klasowych metod zapisu / ładowania jest:

class Animal{
    public string Name {get; set;}
    public void Save(){...}
    public static Animal Load(string name){....}
    public static string[] GetAllNames(){...} // if you just want to list them
    public static Animal[] GetAllAnimals(){...} // if you actually need to retrieve them all
}

Osobiście zastosowałbym takie podejście tylko w dość małych aplikacjach, być może w narzędziach do użytku osobistego lub gdzie mogę rzetelnie przewidzieć, że nie będę miał bardziej skomplikowanych przypadków użycia niż tylko zapisywanie lub ładowanie obiektów.

Również osobiście zobacz wzór jednostki pracy. Kiedy się go pozna, jest naprawdę dobry zarówno w małych, jak i dużych przypadkach. Jest obsługiwany przez wiele frameworków / apis, na przykład EntityFramework lub RavenDB.

Gerino
źródło
2
Informacje dodatkowe: Chociaż koncepcyjnie istnieje tylko jedna baza danych podczas działania aplikacji, nie oznacza to, że należy ustawić statyczne metody RetriveStudent i AddStudent. Repozytoria są zwykle tworzone za pomocą interfejsów, aby umożliwić programistom przełączanie implementacji bez wpływu na resztę aplikacji. Może to nawet pozwolić na rozpowszechnianie aplikacji z obsługą na przykład różnych baz danych. Ta sama logika dotyczy oczywiście wielu innych obszarów oprócz trwałości, takich jak na przykład interfejs użytkownika.
MichelHenrich,
Masz zdecydowanie rację, poczyniłem wiele założeń na podstawie pierwotnego pytania.
Gerino,
dziękuję, myślę, że najlepszym podejściem jest użycie warstw, jak wspomniano we wzorze repozytorium
Ahmad
1

Jeśli jest to bardzo prosta aplikacja, w której obiekt jest mniej więcej powiązany z magazynem danych i odwrotnie (tzn. Może być uważany za właściwość magazynu danych), wówczas zastosowanie metody .save () dla tej klasy może mieć sens.

Ale myślę, że byłoby to dość wyjątkowe.

Raczej zwykle lepiej pozwolić klasie zarządzać swoimi danymi i funkcjonalnością (jak dobry obywatel OO) i przekazać mechanizm trwałości innej klasie lub zestawowi klas.

Inną opcją jest użycie frameworka trwałości, który deklaruje trwałość deklaracyjnie (jak w przypadku adnotacji), ale nadal eksternalizuje trwałość.

Obrabować
źródło
-1
  • Zdefiniuj metodę dla tej klasy, która serializuje obiekt i zwraca sekwencję bajtów, którą można umieścić w bazie danych lub pliku
  • Mieć konstruktor dla tego obiektu, który przyjmuje taką sekwencję bajtów jako dane wejściowe

Jeśli chodzi o RetrieveAllStudents()metodę, twoje poczucie jest słuszne, w rzeczywistości jest prawdopodobnie niewłaściwe, ponieważ możesz mieć wiele różnych list studentów. Dlaczego po prostu nie trzymać list (y) poza Studentklasą?

philfr
źródło
Wiesz, że widziałem taki trend w niektórych ramach PHP, na przykład Laravel, jeśli go znasz. Model jest powiązany z bazą danych i może nawet zwrócić szereg obiektów.
Ahmad