Як повинен бути розроблений клас `Співробітник`?


11

Я намагаюся створити програму для управління працівниками. Однак я не можу зрозуміти, як створити Employeeклас. Моя мета - вміти створювати та маніпулювати даними працівника в базі даних за допомогою Employeeоб’єкта.

Основна реалізація, про яку я думав, була така проста:

class Employee
{
    // Employee data (let's say, dozens of properties).

    Employee() {}
    Create() {}
    Update() {}
    Delete() {}
}

Використовуючи цю реалізацію, я зіткнувся з декількома проблемами.

  1. Чи не IDпрацівник визначається в базі даних, тому , якщо я використовую об'єкт для опису нового співробітника, буде не IDзберігати ще, в той час як об'єкт , який представляє існуючий співробітник буде мати ID. Отже, у мене є властивість, яка інколи описує об'єкт, а іноді не (Що може означати, що ми порушуємо SRP ? Оскільки ми використовуємо той же клас для представлення нових та існуючих співробітників ...).
  2. CreateМетод передбачається створити працівник по базі даних, в той час як Updateі Deleteповинні діяти на основі існуючого співробітника (знову ж , SRP ...).
  3. Які параметри повинен мати метод "Створити"? Десятки параметрів для всіх даних працівника чи, можливо, Employeeоб’єкта?
  4. Чи повинен клас бути незмінним?
  5. Як буде Updateпрацювати? Чи візьме це властивості та оновить базу даних? А може знадобиться два об’єкти - «старий» і «новий» та оновити базу даних з різницями між ними? (Я думаю, що відповідь має відношення до відповіді про незмінність класу).
  6. Яка відповідальність була б у конструктора? Які параметри це потрібно? Чи отримає дані працівника з бази даних за допомогою idпараметра і вони заповнюють властивості?

Отже, як бачите, у мене в голові трохи безлад, і я дуже розгублений. Не могли б ви допомогти мені зрозуміти, як повинен виглядати такий клас?

Зауважте, що я не хочу думок, аби зрозуміти, як загалом створений такий часто використовуваний клас.


3
Вашим порушенням SRP є те, що у вас є клас, який представляє і сутність, і відповідальний за логіку CRUD. Якщо ви розділите це, що CRUD-операції та структура сутності будуть різними класами, то 1. і 2. не порушують SRP. 3. слід взяти Employeeоб'єкт для надання абстракції, питання 4. і 5., як правило, невідповідні, залежать від ваших потреб, і якщо ви розділите структуру та операції CRUD на два класи, то цілком зрозуміло, що конструктор Employeeне може отримати дані від db більше, так що відповіді 6.
Енді

@DavidPacker - Дякую Чи можете ви це відповісти?
Сіпо

5
Не повторюйте, не майте, щоб ваш ctor потягнувся до бази даних. Це так щільно з’єднує код з базою даних і робить речі бога жахливими для перевірки (навіть ручне тестування стає складніше). Подивіться на шаблон сховища. Подумайте про це на секунду, чи ви Updateпрацівник, чи оновлюєте запис працівника? Ви Employee.Delete()чи це робите Boss.Fire(employee)?
RubberDuck

1
Окрім сказаного, чи має для вас сенс те, що вам потрібен працівник для створення працівника? У активному записі може бути більше сенсу створити Співробітника, а потім зателефонувати Зберегти на цьому об'єкті. Вже тоді ви маєте клас, який відповідає за логіку бізнесу, а також за власну стійкість даних.
Містер Кохез

Відповіді:


10

Це більш добре сформована транскрипція мого початкового коментаря під вашим запитанням. Відповіді на запитання ОР можна знайти внизу цієї відповіді. Також будь ласка, перевірте важливу примітку, що знаходиться там же.


Те, що ви зараз описуєте, Sipo, - це шаблон дизайну, який називається Активний запис . Як і у всьому, навіть цей знайшов своє місце серед програмістів, але був відкинутий на користь сховищ та моделей картографічних даних з однієї простої причини - масштабованості.

Коротше кажучи, активний запис - це об'єкт, який:

  • представляє об’єкт у вашому домені (включає бізнес-правила, знає, як поводитися з певними операціями над об’єктом, наприклад, якщо ви можете чи не можете змінити ім’я користувача та інше),
  • знає, як отримати, оновити, зберегти та видалити об'єкт.

Ви звертаєтесь до декількох питань із вашим поточним дизайном, і головна проблема вашого дизайну вирішена в останньому, шостому, пункті (я думаю, останнє, але не менш важливе). Коли у вас є клас, для якого ви проектуєте конструктор, і навіть не знаєте, що конструктор повинен робити, клас, ймовірно, робить щось не так. Це сталося у вашому випадку.

Але виправити дизайн насправді досить просто, розділивши представлення сутності та логіку CRUD на два (або більше) класи.

Ось як виглядає ваш дизайн зараз:

  • Employee- містить інформацію про структуру працівника (її атрибути) та методи, як змінити сутність (якщо ви вирішили піти змінним шляхом), містить логіку CRUD для Employeeсутності, може повернути список Employeeоб'єктів, приймає Employeeоб'єкт, коли ви хочете оновити працівника, може повернути особу Employeeчерез такий методgetSingleById(id : string) : Employee

Нічого собі, клас здається величезним.

Це буде запропоноване рішення:

  • Employee - містить інформацію про структуру працівника (його атрибути) та методи, як змінити сутність (якщо ви вирішили йти змінним шляхом)
  • EmployeeRepository- містить логіку CRUD для Employeeсутності, може повернути список Employeeоб'єктів, приймає Employeeоб'єкт, коли ви хочете оновити працівника, може повернути одиночку Employeeза допомогою методу, якgetSingleById(id : string) : Employee

Ви чули про роз'єднання проблем ? Ні, ви зараз. Це менш сувора версія принципу єдиної відповідальності, яка говорить про те, що клас повинен насправді мати лише одну відповідальність, або, як каже дядько Боб:

Модуль повинен мати одну і єдину причину зміни.

Цілком зрозуміло, що якщо мені вдалося чітко розділити ваш початковий клас на два, які все ще мають добре закруглений інтерфейс, початковий клас, ймовірно, робив занадто багато, і це було.

Що чудово стосується шаблону репозиторію, він не тільки виступає як абстракція, щоб забезпечити середній рівень між базою даних (який може бути будь-яким, файловим, noSQL, SQL, об'єктно-орієнтованим), але він навіть не повинен бути конкретним клас. У багатьох мовах OO ви можете визначити інтерфейс як фактичний interface(або клас із чисто віртуальним методом, якщо ви перебуваєте на C ++), а потім мати кілька реалізацій.

Це повністю знімає рішення про те, чи репозиторій є реальною реалізацією, ви просто покладаєтесь на інтерфейс, фактично покладаючись на структуру з interfaceключовим словом. А сховище саме це, це фантазійний термін для абстрагування рівня даних, а саме - зіставлення даних у ваш домен та навпаки.

Ще одна чудова річ, як розділити його на (принаймні) два класи - це те, що зараз Employeeклас може чітко керувати власними даними та робити це дуже добре, оскільки йому не потрібно піклуватися про інші складні речі.

Питання 6: Отже, що повинен робити конструктор у новоствореному Employeeкласі? Це просто. Слід взяти аргументи, перевірити, чи вони дійсні (наприклад, вік, мабуть, не повинен бути негативним чи ім'я не повинно бути порожнім), створити помилку, коли дані були недійсними та якщо передача перевірки призначила аргументи приватним змінним суб'єкта господарювання. Тепер він не може спілкуватися з базою даних, тому що просто не має уявлення, як це зробити.


Питання 4: Не можна відповісти взагалі, не загалом, тому що відповідь сильно залежить від того, що саме вам потрібно.


Питання 5: Тепер, коли ви розділили роздутий клас на два, ви можете мати кілька методів оновлення безпосередньо на Employeeкласі, як changeUsername, наприклад markAsDeceased, який буде маніпулювати даними Employeeкласу лише в оперативній пам'яті, а потім ви можете ввести такий метод, як registerDirtyвід Шаблон блоку роботи до класу сховища, за допомогою якого ви повідомляєте сховищу, що цей об’єкт змінив властивості і його потрібно буде оновити після виклику commitметоду.

Очевидно, що для оновлення об'єкт повинен мати ідентифікатор і таким чином вже бути збереженим, і це відповідальність сховища виявляє це і викликає помилку, коли критерії не виконуються.


Питання 3: Якщо ви вирішили перейти з моделлю одиниці роботи, createтепер це буде метод registerNew. Якщо ви цього не зробите, я б, мабуть, назвав це saveзамість цього. Мета репозиторію - забезпечити абстракцію між доменом і рівнем даних, через це я рекомендував би вам, щоб цей метод (був він registerNewчи save) приймав Employeeоб'єкт, і це залежить від класів, що реалізують інтерфейс сховища, який визначає атрибути вони вирішують вивезти суб'єкт господарювання. Передати цілий об'єкт краще, тому вам не потрібно мати багато необов'язкових параметрів.


Питання 2: Обидва способи тепер будуть частиною інтерфейсу сховища, і вони не порушують принципу єдиної відповідальності. Відповідальність репозиторію полягає в наданні операцій CRUD для Employeeоб'єктів, тобто це робиться (окрім читання та видалення, CRUD перекладається як на створення, так і на оновлення). Очевидно, ви можете розділити сховище ще більше, маючи EmployeeUpdateRepositoryта інше, але це рідко потрібно, і одна реалізація зазвичай може містити всі CRUD-операції.


Питання 1: Ви закінчили простий Employeeклас, у якого тепер (серед інших атрибутів) буде ідентифікатор. Заповнення чи порожній ідентифікатор (або null) залежить від того, чи об'єкт уже збережений. Тим не менш, ідентичний номер все ще є атрибутом, яким володіє суб'єкт господарювання, і відповідальність Employeeсуб'єкта господарювання полягає в тому, щоб дбати про свої атрибути, отже, піклуватися про свій ідентифікатор.

Незалежно від того, чи має суб'єкт господарювання чи не має його, зазвичай це не має значення, поки ви не спробуєте виконати певну послідовність. Як зазначено у відповіді на запитання 5, відповідальність за сховище виявляє, що ви не намагаєтесь зберегти сутність, яка вже збережена, або намагається оновити об'єкт без ідентифікатора.


Важлива примітка

Будь ласка, майте на увазі, що хоча розділення проблем є великим, насправді проектування функціонального шару сховища є досить копіткою роботою, і на мій досвід, трохи складніше отримати правильний підхід, ніж підхід до активного запису. Але ви закінчите дизайн, який є більш гнучким і масштабованим, що може бути хорошою справою.


Хм так само, як і моя відповідь, але не так, як "крайній" накладає відтінки
Ewan

2
@Ewan Я не спростував твоєї відповіді, але я бачу, чому деякі можуть. Це не відповідає безпосередньо на деякі запитання ОП, а деякі ваші пропозиції здаються безпідставними.
Енді

1
Приємна і всебічна відповідь. Вражає ніготь по голові сепаратоном турботи. Мені подобається попередження про те, що важливий вибір робити між ідеальною складною конструкцією та приємним компромісом.
Крістоф

Правда, ваша відповідь вища
Еван

при першому створенні нового об'єкта співробітника ідентифікатор не матиме значення. поле id може залишитись з нульовим значенням, але це призведе до того, що об'єкт працівника знаходиться в недійсному стані ????
Susantha7

2

Спочатку створіть структуру працівника, що містить властивості концептуального працівника.

Потім створіть базу даних з відповідною структурою таблиці, скажімо, наприклад, mssql

Потім створіть службовий сховище для цієї бази даних EmployeeRepoMsSql з різними операціями CRUD, які вам потрібні.

Потім створіть інтерфейс IE EmployeeeRepo, що відкриває операції CRUD

Потім розгорніть структуру Employee до класу з параметром побудови IEposleeeRepo. Додайте різні необхідні вам методи збереження / видалення тощо та використовуйте введений EmployeeRepo для їх реалізації.

Коли він конусується до Id, я пропоную використовувати GUID, який можна генерувати за допомогою коду в конструкторі.

Для роботи з існуючими об'єктами ваш код може отримати їх із бази даних через сховище, перш ніж викликати їх метод оновлення.

Крім того, ви можете обрати нахмурену (але, на мій погляд, превосходную) модель анемічного об’єкта домену, де ви не додаєте методи CRUD до свого об'єкта, а просто передаєте об'єкт в репо, щоб оновити / зберегти / видалити

Незмінність - це вибір дизайну, який залежатиме від ваших шаблонів та стилю кодування. Якщо у вас все функціонує, то спробуйте також бути непорушними. Але якщо ви не впевнені, об'єкт, що змінюється, можливо, простіше здійснити.

Замість створення () я б пішов із Save (). Створюйте роботи з концепцією незмінюваності, але я завжди вважаю корисним створити об'єкт, який ще не "збережений", наприклад, у вас є інтерфейс користувача, який дозволяє заселити працівника або об'єкти, а потім ще раз перевірити їх за допомогою деяких правил збереження в базі даних.

***** Приклад коду

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);

    }
}

1
Модель анемічних доменів дуже мало стосується логіки CRUD. Це модель, яка, хоча належить до доменного шару, не має функціональних можливостей і вся функціональність подається через сервіси, яким ця модель домену передається як параметр.
Енді

Саме в цьому випадку репо - це послуга, а функції - це операції CRUD.
Еван

@DavidPacker Ви кажете, що анемічна модель домену - це добре?
candied_orange

1
@CandiedOrange Я не висловив свою думку в коментарі, але ні, якщо ви вирішили піти так далеко, як пірнати ваш додаток до шарів, де один шар відповідає лише за бізнес-логіку, я з містером Фаулером, що анемічна модель домену насправді є антитілом. Чому мені потрібна UserUpdateслужба з changeUsername(User user, string newUsername)методом, коли я можу так само добре додати changeUsernameметод до класу Userбезпосередньо. Створювати службу для цього - це не є сенсом.
Енді

1
Я думаю, що в цьому випадку введення репо просто для того, щоб поставити логіку CRUD на Model isnt оптимальним.
Еван

1

Огляд вашого дизайну

Ви Employeeнасправді свого роду проксі - сервер для об'єкта керованому постійно в базі даних.

Тому я пропоную подумати про ідентифікатор так, ніби це посилання на ваш об'єкт бази даних. Маючи на увазі цю логіку, ви можете продовжувати дизайн, як це було б зроблено для об'єктів, що не належать до бази даних, ідентифікатор, що дозволяє реалізувати традиційну логіку композиції:

  • Якщо ідентифікатор встановлений, у вас є відповідний об’єкт бази даних.
  • Якщо ідентифікатор не встановлений, відповідного об’єкта бази даних немає: Employeeможливо, його ще потрібно створити, або його можна було просто видалити.
  • Вам потрібен якийсь механізм для ініціювання відносин для існуючих співробітників та існуючих записів баз даних, які ще не завантажені в пам'ять.

Вам також потрібно буде керувати статусом об’єкта. Наприклад:

  • коли Співробітник ще не пов'язаний з об'єктом БД ні за допомогою створення, ні для пошуку даних, ви не повинні мати змогу виконувати оновлення чи видалення
  • чи дані працівника в об'єкті синхронізовані з базою даних чи внесення змін?

Зважаючи на це, ми могли б вибрати:

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
}

Щоб можна було надійно керувати своїм об'єктом, ви повинні забезпечити кращу інкапсуляцію, зробивши властивості приватними та надайте доступ лише за допомогою геттерів та сетерів, які задають статус оновлення.

Ваші запитання

  1. Я думаю, що властивість ID не порушує SRP. Його єдиною відповідальністю є посилання на об'єкт бази даних.

  2. Ваш працівник в цілому не відповідає SRP, оскільки він відповідає за зв'язок із базою даних, а також за проведення тимчасових змін та за всі транзакції, що відбуваються з цим об’єктом.

    Іншою конструкцією може бути збереження змінних полів в іншому об'єкті, який завантажуватиметься лише тоді, коли потрібно отримати доступ до полів.

    Ви можете реалізувати транзакції бази даних на Співробітника за допомогою шаблону команд . Такий дизайн також полегшить розв'язку між вашими бізнес-об'єктами (працівником) та базовою системою баз даних, виділяючи специфічні ідіоми та API для бази даних.

  3. Я б не додав десяток параметрів Create(), тому що бізнес-об’єкти можуть розвиватися і ускладнювати все це дуже важко. І код став би нечитабельним. Тут у вас є два варіанти: або проходження мінімалістичного набору параметрів (не більше 4), які абсолютно необхідні для створення співробітника в базі даних та виконання інших змін за допомогою оновлення, АБО ви передаєте об'єкт. До речі, в дизайні , я розумію , що ви вже вибрали: my_employee.Create().

  4. Чи повинен клас бути незмінним? Дивіться дискусію вище: у вашому оригінальному дизайні немає. Я б обрав незмінний ідентифікатор, але не незмінний працівник. Співробітник розвивається в реальному житті (нова посада, нова адреса, нова подружня ситуація, навіть нові імена ...). Я думаю, що працювати з цією реальністю буде принаймні легше і природніше, принаймні, на рівні бізнес-логіки.

  5. Якщо ви розглядаєте можливість використання команд для оновлення та окремих об'єктів для (GUI?) Для проведення бажаних змін, ви можете вибрати старий / новий підхід. У всіх інших випадках я б вирішив оновити об'єкт, що змінюється. Увага: оновлення може викликати код бази даних, щоб ви переконалися, що після оновлення об'єкт все ще дійсно синхронізований з БД.

  6. Я думаю, що пошук співробітника з БД у конструкторі - це не дуже гарна ідея, тому що вилучення може піти не так, і багатьма мовами складно впоратися з невдалою побудовою. Конструктор повинен ініціалізувати об'єкт (особливо ідентифікатор) та його статус.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.