Jak należy zaprojektować klasę „Pracownik”?

11

Próbuję stworzyć program do zarządzania pracownikami. Nie mogę jednak wymyślić, jak zaprojektować Employeeklasę. Moim celem jest możliwość tworzenia i manipulowania danymi pracowników w bazie danych za pomocą Employeeobiektu.

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.

  1. Dane IDpracownika są podane w bazie danych, więc jeśli użyję obiektu do opisu nowego pracownika, nie będzie jeszcze IDmoż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 ...).
  2. CreateMetoda ma stworzyć pracownika w bazie danych, podczas gdy Updatei Deletemają działać na istniejącym pracownika (ponownie, SRP ...).
  3. Jakie parametry powinna mieć metoda „Utwórz”? Dziesiątki parametrów dla wszystkich danych pracowników, a może Employeeobiekt?
  4. Czy klasa powinna być niezmienna?
  5. Jak będzie Updatedział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).
  6. Jaka byłaby odpowiedzialność konstruktora? Jakie byłyby parametry? Czy pobierałby dane pracownika z bazy danych za pomocą idparametru 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.

Sipo
źródło
3
Twoim bieżącym naruszeniem SRP jest to, że masz klasę reprezentującą zarówno byt, jak i odpowiedzialną za logikę CRUD. Jeśli je oddzielisz, operacje CRUD i struktura encji będą miały różne klasy, wówczas 1. i 2. nie będą łamać SRP. 3. powinien wziąć Employeeobiekt 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, konstruktor Employeenie może pobrać danych od db już, więc to odpowiada 6.
Andy
@DavidPacker - Dzięki. Czy możesz podać to w odpowiedzi?
Sipo
5
Nie, powtarzam, nie mieć konstruktor dotarcia do bazy danych. Takie postępowanie powoduje ścisłe powiązanie kodu z bazą danych i sprawia, że ​​wszystko jest okropnie trudne do przetestowania (nawet testowanie ręczne staje się trudniejsze). Sprawdź wzorzec repozytorium. Pomyśl o tym przez chwilę, czy jesteś Updatepracownikiem, czy aktualizujesz dokumentację pracownika? Czy ty Employee.Delete(), czy nie Boss.Fire(employee)?
RubberDuck,
1
Czy oprócz tego, o czym już wspomniano, ma sens, że potrzebujesz pracownika, aby go stworzyć? W aktywnym zapisie bardziej sensowne może być założenie nowego pracownika, a następnie wywołanie Zapisz na tym obiekcie. Ale nawet wtedy masz klasę odpowiedzialną za logikę biznesową, a także za trwałość własnych danych.
Pan Cochese,

Odpowiedzi:

10

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:

  • reprezentuje obiekt w Twojej domenie (obejmuje reguły biznesowe, wie, jak obsłużyć pewne operacje na obiekcie, na przykład czy możesz zmienić nazwę użytkownika itp.),
  • umie odzyskać, zaktualizować, zapisać i usunąć encję.

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 dla Employeeencji, może zwrócić listę Employeeobiektów, akceptuje Employeeobiekt, gdy chcesz zaktualizować pracownika, może zwrócić jeden Employeeza 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 dla Employeeencji, może zwrócić listę Employeeobiektów, akceptuje Employeeobiekt, gdy chcesz zaktualizować pracownika, może zwrócić pojedynczy Employeeza 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:

Moduł powinien mieć tylko jeden powód do zmiany.

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 interfacesł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 Employeeklasa 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 Employeeklasie? 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 Employeezajęciach, jak changeUsername, markAsDeceased, które będą manipulować danymi o Employeeklasie tylko w pamięci RAM , a następnie można wprowadzić metody takie jak registerDirtyz 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łaniu commitmetody.

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, createmetoda będzie teraz registerNew. Jeśli nie, prawdopodobnie nazwałbym to savezamiast tego. Celem repozytorium jest zapewnienie abstrakcji między domeną a warstwą danych, dlatego zalecałbym, aby ta metoda (czy to registerNewlub save) akceptowała Employeeobiekt 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 Employeeobiektó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ąc EmployeeUpdateRepositoryi tak dalej, ale jest to rzadko potrzebne i jedna implementacja może zwykle zawierać wszystkie operacje CRUD.


Pytanie 1: Skończyłeś z prostą Employeeklasą, która teraz (między innymi atrybutami) będzie miała id. To, czy identyfikator jest wypełniony czy pusty (lub null) zależy od tego, czy obiekt został już zapisany. Niemniej jednak identyfikator jest nadal atrybutem, który posiada jednostka, a obowiązkiem Employeejednostki 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ą.

Andy
źródło
Hmm tak samo jak moja odpowiedź, ale nie tak, jak „ostry” nakłada odcienie
Ewan
2
@Ewan Nie głosowałem za twoją odpowiedzią, ale rozumiem, dlaczego niektórzy mogą. Nie odpowiada bezpośrednio na niektóre pytania PO, a niektóre z twoich sugestii wydają się nieuzasadnione.
Andy,
1
Ładna i wyczerpująca odpowiedź. Uderza w gwóźdź w głowie z separacją troski. I podoba mi się ostrzeżenie, które wskazuje na ważny wybór pomiędzy doskonałym złożonym projektem a dobrym kompromisem.
Christophe,
To prawda, że ​​twoja odpowiedź jest lepsza
Ewan
kiedy po raz pierwszy utworzysz nowy obiekt pracownika, identyfikator nie będzie miał wartości. pole id może pozostawić wartość zerową, ale spowoduje, że obiekt pracownika będzie w niepoprawnym stanie ????
Susantha7
2

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

public class Employee
{
    public string Id { get; set; }

    public string Name { get; set; }

    private IEmployeeRepo repo;

    //with the OOP approach you want the save method to be on the Employee Object
    //so you inject the IEmployeeRepo in the Employee constructor
    public Employee(IEmployeeRepo repo)
    {
        this.repo = repo;
        this.Id = Guid.NewGuid().ToString();
    }

    public bool Save()
    {
        return repo.Save(this);
    }
}

public interface IEmployeeRepo
{
    bool Save(Employee employee);

    Employee Get(string employeeId);
}

public class EmployeeRepoSql : IEmployeeRepo
{
    public Employee Get(string employeeId)
    {
        var sql = "Select * from Employee where Id=@Id";
        //more db code goes here
        Employee employee = new Employee(this);
        //populate object from datareader
        employee.Id = datareader["Id"].ToString();

    }

    public bool Save(Employee employee)
    {
        var sql = "Insert into Employee (....";
        //db logic
    }
}

public class MyADMProgram
{
    public void Main(string id)
    {
        //with ADM don't inject the repo into employee, just use it in your program
        IEmployeeRepo repo = new EmployeeRepoSql();
        var emp = repo.Get(id);

        //do business logic
        emp.Name = TextBoxNewName.Text;

        //save to DB
        repo.Save(emp);

    }
}
Ewan
źródło
1
Anemiczny model domeny ma bardzo mało wspólnego z logiką CRUD. Jest to model, który choć należy do warstwy domeny, nie ma żadnej funkcjonalności, a cała funkcjonalność jest obsługiwana za pośrednictwem usług, do których ten model domeny jest przekazywany jako parametr.
Andy,
Dokładnie, w tym przypadku repo jest usługą, a funkcjami są operacje CRUD.
Ewan
@DavidPacker czy mówisz, że model domeny anemicznej jest dobrą rzeczą?
candied_orange
1
@CandiedOrange Nie wypowiedziałem się w komentarzu, ale nie, jeśli zdecydujesz się posunąć tak daleko, jak nurkowanie aplikacji do warstw, w których jedna warstwa jest odpowiedzialna tylko za logikę biznesową, jestem z panem Fowlerem, że anemiczny model domeny jest w rzeczywistości anty-wzorem. Dlaczego powinienem potrzebować UserUpdateusługi z changeUsername(User user, string newUsername)metodą, skoro równie dobrze mogę bezpośrednio dodać changeUsernamemetodę do klasy User. Tworzenie do tego usługi jest bez sensu.
Andy
1
Myślę, że w tym przypadku wstrzykiwanie repozytorium tylko po to, aby umieścić logikę CRUD w modelu, nie jest optymalne.
Ewan
1

Przegląd twojego projektu

Twój Employeejest 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:

  • Jeśli identyfikator jest ustawiony, masz odpowiedni obiekt bazy danych.
  • Jeśli identyfikator nie jest ustawiony, nie ma odpowiedniego obiektu bazy danych: Employeemoże być jeszcze utworzony lub mógł zostać właśnie usunięty.
  • Potrzebujesz mechanizmu do zainicjowania relacji dla istniejących pracowników i istniejących rekordów bazy danych, które nie zostały jeszcze załadowane do pamięci.

Będziesz także musiał zarządzać statusem obiektu. Na przykład:

  • gdy pracownik nie jest jeszcze połączony z obiektem DB ani przez tworzenie, ani przez pobieranie danych, nie powinno być możliwości wykonywania aktualizacji lub usuwania
  • czy dane pracownika w obiekcie są zsynchronizowane z bazą danych, czy wprowadzono zmiany?

Mając to na uwadze, moglibyśmy wybrać:

class Employee
{
    ...
    Employee () {}       // Initialize an empty Employee
    Load(IDType ID) {}   // Load employee with known ID from the database
    bool Create() {}     // Create an new employee an set its ID 
    bool Update() {}     // Update the employee (can ID be changed?)
    bool Delete() {}     // Delete the employee (and reset ID because there's no corresponding ID. 
    bool isClean () {}   // true if ID empty or if all properties match database
}

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

  1. Myślę, że właściwość ID nie narusza SRP. Jego jedynym obowiązkiem jest odwoływanie się do obiektu bazy danych.

  2. 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.

  3. 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().

  4. 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.

  5. 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.

  6. 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.

Christophe
źródło