Próbuję stworzyć program do zarządzania pracownikami. Nie mogę jednak wymyślić, jak zaprojektować Employee
klasę. Moim celem jest możliwość tworzenia i manipulowania danymi pracowników w bazie danych za pomocą Employee
obiektu.
Podstawowa implementacja, o której myślałem, była prosta:
class Employee
{
// Employee data (let's say, dozens of properties).
Employee() {}
Create() {}
Update() {}
Delete() {}
}
Korzystając z tej implementacji, napotkałem kilka problemów.
- Dane
ID
pracownika są podane w bazie danych, więc jeśli użyję obiektu do opisu nowego pracownika, nie będzie jeszczeID
możliwości przechowywania, podczas gdy obiekt reprezentujący istniejącego pracownika będzie miałID
. Mam więc właściwość, która czasami opisuje przedmiot, a czasem nie (co może wskazywać, że naruszamy SRP ? Ponieważ używamy tej samej klasy do reprezentowania nowych i istniejących pracowników ...). Create
Metoda ma stworzyć pracownika w bazie danych, podczas gdyUpdate
iDelete
mają działać na istniejącym pracownika (ponownie, SRP ...).- Jakie parametry powinna mieć metoda „Utwórz”? Dziesiątki parametrów dla wszystkich danych pracowników, a może
Employee
obiekt? - Czy klasa powinna być niezmienna?
- Jak będzie
Update
działać? Czy zajmie właściwości i zaktualizuje bazę danych? A może zajmie dwa obiekty - „stary” i „nowy” i zaktualizuje bazę danych o różnice między nimi? (Myślę, że odpowiedź ma związek z odpowiedzią na temat zmienności klasy). - Jaka byłaby odpowiedzialność konstruktora? Jakie byłyby parametry? Czy pobierałby dane pracownika z bazy danych za pomocą
id
parametru i wypełniałby właściwości?
Jak więc widzicie, mam w głowie trochę bałaganu i jestem bardzo zdezorientowany. Czy mógłbyś mi pomóc zrozumieć, jak powinna wyglądać taka klasa?
Proszę zauważyć, że nie chcę opinii, tylko po to, aby zrozumieć, w jaki sposób tak często używana klasa jest ogólnie zaprojektowana.
Employee
obiekt w celu zapewnienia abstrakcji, pytania 4. i 5. są generalnie niemożliwe do odpowiedzi, zależą od twoich potrzeb, a jeśli podzielisz strukturę i operacje CRUD na dwie klasy, to jest całkiem jasne, konstruktorEmployee
nie może pobrać danych od db już, więc to odpowiada 6.Update
pracownikiem, czy aktualizujesz dokumentację pracownika? Czy tyEmployee.Delete()
, czy nieBoss.Fire(employee)
?Odpowiedzi:
To jest lepiej sformułowana transkrypcja mojego pierwszego komentarza do twojego pytania. Odpowiedzi na pytania skierowane przez PO można znaleźć na dole tej odpowiedzi. Sprawdź również ważną notatkę znajdującą się w tym samym miejscu.
To, co obecnie opisujesz, Sipo, to wzorzec projektowy o nazwie Aktywny rekord . Jak wszystko, nawet ten znalazł swoje miejsce wśród programistów, ale został odrzucony na rzecz repozytorium i wzorców mapowania danych z jednego prostego powodu - skalowalności.
Krótko mówiąc, aktywny rekord to obiekt, który:
W swoim obecnym projekcie rozwiązujesz kilka problemów, a główny problem twojego projektu jest omówiony w ostatnim, szóstym punkcie (chyba, że nie mniej ważne). Kiedy masz klasę, dla której projektujesz konstruktor, a nawet nie wiesz, co powinien robić konstruktor, klasa prawdopodobnie robi coś złego. Tak było w twoim przypadku.
Ale naprawienie projektu jest w rzeczywistości dość proste poprzez podzielenie reprezentacji encji i logiki CRUD na dwie (lub więcej) klasy.
Tak teraz wygląda Twój projekt:
Employee
- zawiera informacje o strukturze pracownika (jego atrybuty) i metody modyfikowania encji (jeśli zdecydujesz się na modyfikację), zawiera logikę CRUD dlaEmployee
encji, może zwrócić listęEmployee
obiektów, akceptujeEmployee
obiekt, gdy chcesz zaktualizować pracownika, może zwrócić jedenEmployee
za pomocą metody podobnej dogetSingleById(id : string) : Employee
Wow, klasa wydaje się ogromna.
To będzie proponowane rozwiązanie:
Employee
- zawiera informacje o strukturze pracownika (jej atrybuty) i metodach modyfikacji encji (jeśli zdecydujesz się na zmienną metodę)EmployeeRepository
- zawiera logikę CRUD dlaEmployee
encji, może zwrócić listęEmployee
obiektów, akceptujeEmployee
obiekt, gdy chcesz zaktualizować pracownika, może zwrócić pojedynczyEmployee
za pomocą metody takiej jakgetSingleById(id : string) : Employee
Czy słyszałeś o rozdzieleniu obaw ? Nie, teraz będziesz. Jest to mniej rygorystyczna wersja zasady pojedynczej odpowiedzialności, która mówi, że klasa powinna faktycznie mieć tylko jedną odpowiedzialność, lub jak mówi wujek Bob:
Jest całkiem jasne, że gdybym był w stanie wyraźnie podzielić twoją klasę początkową na dwie, które wciąż mają dobrze zaokrąglony interfejs, klasa początkowa prawdopodobnie robiła zbyt wiele i tak było.
Co jest wspaniałe we wzorcu repozytorium, działa nie tylko jako abstrakcja, zapewniając warstwę pośrednią między bazą danych (która może być dowolna, plikowa, bez SQL, SQL, obiektowa), ale nawet nie musi być konkretna klasa. W wielu językach OO możesz zdefiniować interfejs jako rzeczywisty
interface
(lub klasę z czystą wirtualną metodą, jeśli jesteś w C ++), a następnie mieć wiele implementacji.To całkowicie eliminuje decyzję, czy repozytorium jest faktyczną implementacją, polegając na interfejsie, polegając na strukturze ze
interface
słowem kluczowym. A repozytorium jest dokładnie tym, jest to fantazyjny termin na abstrakcję warstwy danych, a mianowicie mapowanie danych na twoją domenę i odwrotnie.Kolejną wielką rzeczą w dzieleniu go na (przynajmniej) dwie klasy jest to, że teraz
Employee
klasa może wyraźnie zarządzać swoimi danymi i robić to bardzo dobrze, ponieważ nie musi zajmować się innymi trudnymi rzeczami.Pytanie 6: Co zatem powinien zrobić konstruktor w nowo utworzonej
Employee
klasie? To jest proste. Powinien wziąć argumenty, sprawdzić, czy są poprawne (np. Wiek prawdopodobnie nie powinien być ujemny lub nazwa nie powinna być pusta), zgłosić błąd, gdy dane są nieprawidłowe, a jeśli weryfikacja się powiedzie, przypisz argumenty do zmiennych prywatnych podmiotu. Nie może teraz komunikować się z bazą danych, ponieważ po prostu nie ma pojęcia, jak to zrobić.Pytanie 4: Nie można w ogóle odpowiedzieć, nie ogólnie, ponieważ odpowiedź w dużej mierze zależy od tego, czego dokładnie potrzebujesz.
Pytanie 5: Teraz, gdy już oddzielił nadęty klasę na dwie części, można mieć wiele metod aktualizacji bezpośrednio na
Employee
zajęciach, jakchangeUsername
,markAsDeceased
, które będą manipulować danymi oEmployee
klasie tylko w pamięci RAM , a następnie można wprowadzić metody takie jakregisterDirty
z Wzorzec jednostki pracy do klasy repozytorium, przez który informujesz repozytorium, że ten obiekt zmienił właściwości i będzie wymagał aktualizacji po wywołaniucommit
metody.Oczywiście w przypadku aktualizacji obiekt musi mieć identyfikator, a zatem być już zapisany, a repozytorium jest odpowiedzialne za wykrycie tego i zgłoszenie błędu, gdy kryteria nie są spełnione.
Pytanie 3: Jeśli zdecydujesz się zastosować wzór Jednostki Pracy,
create
metoda będzie terazregisterNew
. Jeśli nie, prawdopodobnie nazwałbym tosave
zamiast tego. Celem repozytorium jest zapewnienie abstrakcji między domeną a warstwą danych, dlatego zalecałbym, aby ta metoda (czy toregisterNew
lubsave
) akceptowałaEmployee
obiekt i to do klas implementujących interfejs repozytorium, który przypisuje atrybuty decydują się usunąć z bytu. Przekazanie całego obiektu jest lepsze, więc nie musisz mieć wielu opcjonalnych parametrów.Pytanie 2: Obie metody będą teraz częścią interfejsu repozytorium i nie będą naruszać zasady pojedynczej odpowiedzialności. Zadaniem repozytorium jest zapewnienie operacji CRUD dla
Employee
obiektów, czyli to, co robi (oprócz odczytu i usuwania CRUD przekłada się zarówno na tworzenie, jak i aktualizację). Oczywiście, możesz podzielić repozytorium jeszcze bardziej, mającEmployeeUpdateRepository
i tak dalej, ale jest to rzadko potrzebne i jedna implementacja może zwykle zawierać wszystkie operacje CRUD.Pytanie 1: Skończyłeś z prostą
Employee
klasą, która teraz (między innymi atrybutami) będzie miała id. To, czy identyfikator jest wypełniony czy pusty (lubnull
) zależy od tego, czy obiekt został już zapisany. Niemniej jednak identyfikator jest nadal atrybutem, który posiada jednostka, a obowiązkiemEmployee
jednostki jest dbanie o jej atrybuty, a tym samym dbanie o jej identyfikator.To, czy jednostka ma identyfikator, czy nie, zwykle nie ma znaczenia, dopóki nie spróbujesz wykonać na nim logiki trwałości. Jak wspomniano w odpowiedzi na pytanie 5, repozytorium jest odpowiedzialne za wykrycie, że nie próbujesz zapisać encji, która została już zapisana, lub próbujesz zaktualizować encję bez identyfikatora.
Ważna uwaga
Należy pamiętać, że chociaż rozdzielenie problemów jest świetne, w rzeczywistości zaprojektowanie funkcjonalnej warstwy repozytorium jest dość żmudną pracą, a moim doświadczeniem jest nieco trudniejsze do uzyskania niż aktywne podejście do nagrywania. Ale skończysz z projektem, który jest znacznie bardziej elastyczny i skalowalny, co może być dobrą rzeczą.
źródło
Najpierw utwórz strukturę pracownika zawierającą właściwości pracownika koncepcyjnego.
Następnie utwórz bazę danych z pasującą strukturą tabeli, na przykład mssql
Następnie utwórz repozytorium pracowników dla tej bazy danych EmployeeRepoMsSql z różnymi wymaganymi operacjami CRUD.
Następnie utwórz interfejs IEmployeeRepo pokazujący operacje CRUD
Następnie rozwiń swoją strukturę pracownika do klasy z parametrem konstrukcyjnym IEmployeeRepo. Dodaj różne wymagane metody Save / Delete itp. I użyj wstrzykniętego EmployeeRepo, aby je wdrożyć.
Kiedy następuje identyfikacja Id, sugeruję użycie identyfikatora GUID, który można wygenerować za pomocą kodu w konstruktorze.
Aby pracować z istniejącymi obiektami, kod może pobrać je z bazy danych za pośrednictwem repozytorium przed wywołaniem ich metody aktualizacji.
Alternatywnie możesz wybrać spoglądający na (ale moim zdaniem lepszy) model Anemic Domain Object, w którym nie dodajesz metod CRUD do swojego obiektu, i po prostu przekazujesz obiekt do repozytorium, aby go zaktualizować / zapisać / usunąć
Niezmienność to wybór projektu, który będzie zależał od twoich wzorów i stylu kodowania. Jeśli zamierzasz funkcjonować, postaraj się również pozostać niezmiennym. Ale jeśli nie masz pewności, obiekt zmienny jest prawdopodobnie łatwiejszy do wdrożenia.
Zamiast Create () wybrałbym Save (). Twórz działa z koncepcją niezmienności, ale zawsze uważam, że warto skonstruować obiekt, który nie został jeszcze „zapisany”, np. Masz interfejs użytkownika, który pozwala wypełnić obiekt lub obiekty pracownika, a następnie zweryfikować je ponownie kilka reguł przed zapisywanie do bazy danych.
***** przykładowy kod
źródło
UserUpdate
usługi zchangeUsername(User user, string newUsername)
metodą, skoro równie dobrze mogę bezpośrednio dodaćchangeUsername
metodę do klasyUser
. Tworzenie do tego usługi jest bez sensu.Przegląd twojego projektu
Twój
Employee
jest w rzeczywistości rodzaj proxy dla obiektu zarządzanego uporczywie w bazie danych.Dlatego sugeruję, aby pomyśleć o tym identyfikatorze, tak jakby był odwołaniem do obiektu bazy danych. Mając tę logikę na uwadze, możesz kontynuować projektowanie, tak jak w przypadku obiektów innych niż baza danych, identyfikator umożliwiający wdrożenie tradycyjnej logiki kompozycji:
Employee
może być jeszcze utworzony lub mógł zostać właśnie usunięty.Będziesz także musiał zarządzać statusem obiektu. Na przykład:
Mając to na uwadze, moglibyśmy wybrać:
Aby móc w niezawodny sposób zarządzać statusem obiektu, musisz zapewnić lepszą enkapsulację, ustawiając właściwości na prywatne i umożliwiając dostęp tylko za pośrednictwem programów pobierających i ustawiających, które ustawiają stan aktualizacji.
Twoje pytania
Myślę, że właściwość ID nie narusza SRP. Jego jedynym obowiązkiem jest odwoływanie się do obiektu bazy danych.
Twój pracownik jako całość nie jest zgodny z SRP, ponieważ jest odpowiedzialny za połączenie z bazą danych, ale także za utrzymywanie tymczasowych zmian i za wszystkie transakcje mające miejsce w tym obiekcie.
Innym rozwiązaniem może być przechowywanie zmiennych pól w innym obiekcie, który byłby ładowany tylko wtedy, gdy trzeba uzyskać dostęp do pól.
Można zaimplementować transakcje bazy danych na pracowniku przy użyciu wzorca poleceń . Tego rodzaju projekt ułatwiłby również oddzielanie od siebie obiektów biznesowych (pracownika) i bazowego systemu bazy danych, izolując specyficzne dla bazy danych idiomy i interfejsy API.
Nie dodałbym kilkunastu parametrów
Create()
, ponieważ obiekty biznesowe mogłyby ewoluować i bardzo utrudniać utrzymanie tego wszystkiego. A kod stałby się nieczytelny. Masz tutaj 2 możliwości: albo przekazać minimalistyczny zestaw parametrów (nie więcej niż 4), które są absolutnie wymagane do utworzenia pracownika w bazie danych i wykonać pozostałe zmiany poprzez aktualizację, LUB przekazać obiekt. Nawiasem mówiąc, w projekcie Rozumiem, że masz już wybrany:my_employee.Create()
.Czy klasa powinna być niezmienna? Zobacz dyskusję powyżej: w swoim oryginalnym projekcie nr. Wybrałbym niezmienny identyfikator, ale nie niezmienny pracownik. Pracownik ewoluuje w prawdziwym życiu (nowe stanowisko, nowy adres, nowa sytuacja małżeńska, nawet nowe nazwiska ...). Myślę, że łatwiej i bardziej naturalnie będzie pracować z myślą o tej rzeczywistości, przynajmniej w warstwie logiki biznesowej.
Jeśli rozważasz użycie poleceń aktualizacji i odrębnych obiektów (GUI?) Do przechowywania pożądanych zmian, możesz wybrać stare / nowe podejście. We wszystkich innych przypadkach wolałbym aktualizować zmienny obiekt. Uwaga: aktualizacja może wyzwalać kod bazy danych, dlatego po aktualizacji należy upewnić się, że obiekt jest nadal zsynchronizowany z bazą danych.
Myślę, że pobieranie pracownika z DB w konstruktorze nie jest dobrym pomysłem, ponieważ pobieranie może się nie udać, aw wielu językach trudno jest poradzić sobie z nieudaną budową. Konstruktor powinien zainicjować obiekt (zwłaszcza identyfikator) i jego status.
źródło