Как должен быть разработан класс «Сотрудник»?

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. Какими параметрами должен обладать метод Create? Десятки параметров для всех данных сотрудника или, может быть, Employeeобъекта?
  4. Должен ли класс быть неизменным?
  5. Как будет Updateработать? Будет ли он принимать свойства и обновлять базу данных? Или, может быть, он возьмет два объекта - «старый» и «новый» и обновит базу данных с учетом различий между ними? (Я думаю, что ответ связан с ответом об изменчивости класса).
  6. За что отвечает конструктор? Какие параметры он принимает? Будет ли он получать данные о сотрудниках из базы данных, используя idпараметр, и они заполняют свойства?

Итак, как вы видите, у меня в голове немного беспорядка, и я очень растерялся. Не могли бы вы помочь мне понять, как должен выглядеть такой класс?

Обратите внимание, что мне не нужны мнения, просто чтобы понять, как обычно разрабатывается такой часто используемый класс.

Sipo
источник
3
Ваше настоящее нарушение SRP заключается в том, что у вас есть класс, представляющий как сущность, так и отвечающую за логику CRUD. Если вы разделяете его, что операции CRUD и структура сущностей будут разными классами, то 1. и 2. не нарушают SRP. 3. должен взять Employeeобъект для предоставления абстракции, вопросы 4. и 5. обычно не отвечают, зависят от ваших потребностей, и если вы разделяете структуру и операции CRUD на два класса, тогда совершенно ясно, что конструктор Employeeне может получить данные от БД больше, так что отвечает 6.
Энди
@DavidPacker - Спасибо. Не могли бы вы поставить это в ответ?
Сипо
5
Не повторяю, что ваш ctor не обращается к базе данных. Это тесно связывает код с базой данных и делает тестирование ужасно трудным (даже ручное тестирование становится сложнее). Посмотрите на шаблон хранилища. Подумайте об этом на секунду, являетесь ли вы Updateсотрудником или обновляете запись сотрудника? Вы Employee.Delete()или нет Boss.Fire(employee)?
RubberDuck
1
Помимо того, что уже было упомянуто, имеет ли смысл для вас, что вам нужен сотрудник, чтобы создать сотрудника? В активной записи может иметь больше смысла, чтобы создать сотрудника и затем вызвать Save для этого объекта. Но даже тогда у вас теперь есть класс, который отвечает за бизнес-логику, а также за собственную сохранность данных.
Мистер Кочезе

Ответы:

10

Это более правильная запись моего первоначального комментария под вашим вопросом. Ответы на вопросы, заданные ФП, можно найти внизу этого ответа. Также, пожалуйста, проверьте важную заметку, расположенную в том же месте.


То, что вы сейчас описываете, Sipo, - это шаблон проектирования, называемый Active record . Как и во всем, даже этот нашел свое место среди программистов, но был отброшен в пользу репозитория и шаблонов отображения данных по одной простой причине - масштабируемости.

Короче говоря, активная запись - это объект, который:

  • представляет объект в вашем домене (включает бизнес-правила, знает, как обрабатывать определенные операции над объектом, например, если вы можете или не можете изменить имя пользователя и т. д.),
  • знает, как извлечь, обновить, сохранить и удалить объект.

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

Но исправить дизайн на самом деле довольно просто, разбив представление сущностей и логику CRUD на два (или более) класса.

Вот как выглядит ваш дизайн сейчас:

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

Вау, класс кажется огромным.

Это будет предлагаемое решение:

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

Вы слышали о разделении интересов ? Нет, ты будешь сейчас. Это менее строгая версия принципа единой ответственности, в которой говорится, что класс должен иметь только одну ответственность, или, как сказал дядя Боб:

Модуль должен иметь одну-единственную причину для изменения.

Совершенно очевидно, что если бы я смог четко разделить ваш начальный класс на два, которые все еще имеют хорошо округленный интерфейс, то начальный класс, вероятно, делал слишком много, и это было.

Что хорошо в шаблоне репозитория, он не только выступает в качестве абстракции для обеспечения промежуточного уровня между базами данных (который может быть любым, файловым, noSQL, SQL, объектно-ориентированным), но он даже не должен быть конкретным класс. Во многих ОО-языках вы можете определить интерфейс как фактический interface(или класс с чисто виртуальным методом, если вы находитесь в C ++), а затем иметь несколько реализаций.

Это полностью отменяет решение о том, является ли хранилище фактической реализацией. Вы просто полагаетесь на интерфейс, фактически полагаясь на структуру с interfaceключевым словом. И репозиторий - это именно то, что это причудливый термин для абстракции уровня данных, а именно, отображение данных в ваш домен и наоборот.

Еще одна замечательная особенность разделения его на (по крайней мере) два класса заключается в том, что теперь Employeeкласс может четко управлять своими собственными данными и делать это очень хорошо, поскольку ему не нужно заботиться о других сложных вещах.

Вопрос 6: Так что же должен делать конструктор во вновь созданном Employeeклассе? Это просто. Он должен принимать аргументы, проверять, являются ли они действительными (например, возраст не должен быть отрицательным, или имя не должно быть пустым), выдавать ошибку, когда данные были недействительными, и если пройденная валидация присваивает аргументы частным переменным сущности. Теперь он не может связаться с базой данных, потому что просто не знает, как это сделать.


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


Вопрос 5: Теперь, когда вы разделили раздутый класс на два, вы можете иметь несколько методов обновления непосредственно на Employeeклассе, как changeUsername, markAsDeceased, который будет обрабатывать данные о Employeeклассе только в оперативной памяти , и тогда вы могли бы ввести такой метод, как registerDirtyиз Шаблон единицы работы для класса репозитория, с помощью которого вы дадите знать хранилищу, что этот объект изменил свойства и его нужно будет обновить после вызова commitметода.

Очевидно, что для обновления объект должен иметь идентификатор и, следовательно, быть уже сохраненным, и ответственность хранилища заключается в том, чтобы обнаружить это и вызвать ошибку, когда критерии не выполнены.


Вопрос 3: Если вы решите использовать шаблон «Единица работы», createметод теперь будет registerNew. Если вы этого не сделаете, я бы назвал это saveвместо этого. Цель репозитория - обеспечить абстракцию между доменом и уровнем данных, поэтому я бы порекомендовал вам, чтобы этот метод (будь то registerNewили save) принимал Employeeобъект, и это зависит от классов, реализующих интерфейс репозитория, атрибуты которого они решили вывести из сущности. Передача всего объекта лучше, поэтому вам не нужно иметь много дополнительных параметров.


Вопрос 2: Оба метода теперь будут частью интерфейса репозитория, и они не нарушают принцип единой ответственности. Обязанность репозитория состоит в том, чтобы предоставлять CRUD-операции для Employeeобъектов, что он и делает (кроме Read и Delete, CRUD преобразуется как в Create, так и в Update). Очевидно, что вы можете разделить репозиторий еще дальше, имея EmployeeUpdateRepositoryи так далее, но это редко требуется, и одна реализация обычно может содержать все операции CRUD.


Вопрос 1: В результате вы Employeeполучили простой класс, который теперь (среди прочих атрибутов) будет иметь идентификатор. Является ли идентификатор заполненным или пустым (или null), зависит от того, был ли объект уже сохранен. Тем не менее, идентификатор по-прежнему является атрибутом, которым владеет объект, и ответственность Employeeобъекта заключается в том, чтобы заботиться о его атрибутах и, следовательно, заботиться о его идентификаторе.

Независимо от того, имеет ли объект идентификатор или нет, обычно не имеет значения, пока вы не попытаетесь применить к нему некоторую постоянную логику. Как упоминалось в ответе на вопрос 5, хранилище отвечает за обнаружение того, что вы не пытаетесь сохранить уже сохраненную сущность или пытаетесь обновить сущность без идентификатора.


Важная заметка

Пожалуйста, имейте в виду, что, хотя разделение проблем велико, на самом деле разработка функционального слоя репозитория является довольно утомительной работой, и, по моему опыту, сделать ее немного сложнее, чем подход с активной записью. Но в итоге вы получите гораздо более гибкий и масштабируемый дизайн, что может быть полезно.

Энди
источник
Хм, так же, как мой ответ, но не так, как «крайние» ставит оттенки
Эван
2
@ Иван, я не понизил твой ответ, но я могу понять, почему некоторые могут иметь. Он не дает прямого ответа на некоторые вопросы ОП, а некоторые из ваших предложений кажутся необоснованными.
Энди
1
Хороший и содержательный ответ. Удары гвоздь по голове с разделителем беспокойства. И мне нравится предупреждение, которое указывает на важный выбор между идеальным сложным дизайном и хорошим компромиссом.
Кристоф
Правда, ваш ответ выше
Эван
при первом создании нового объекта сотрудника значение ID не будет иметь значения. Поле id может оставить нулевое значение, но это приведет к тому, что объект сотрудника находится в недопустимом состоянии ????
Susantha7
2

Сначала создайте структуру сотрудника, содержащую свойства концептуального сотрудника.

Затем создайте базу данных с соответствующей структурой таблицы, например, mssql

Затем создайте репозиторий сотрудников Для этой базы данных EmployeeRepoMsSql с различными требуемыми операциями CRUD.

Затем создайте интерфейс IEmployeeRepo, представляющий операции CRUD

Затем разверните структуру Employee до класса с параметром конструкции IEmployeeRepo. Добавьте различные требуемые методы сохранения / удаления и т. Д. И используйте внедренный EmployeeRepo для их реализации.

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

Для работы с существующими объектами ваш код может извлечь их из базы данных через репозиторий перед вызовом их метода обновления.

В качестве альтернативы вы можете выбрать неодобрительную (но, на мой взгляд, более высокую) модель объекта Anemic Domain, в которой вы не добавляете методы CRUD к своему объекту, а просто передаете объект в репозиторий для обновления / сохранения / удаления.

Неизменность - это выбор дизайна, который будет зависеть от ваших шаблонов и стиля кодирования. Если вы все работаете, постарайтесь быть неизменным. Но если вы не уверены, что изменяемый объект, вероятно, проще реализовать.

Вместо Create () я бы пошел с Save (). Create работает с концепцией неизменяемости, но я всегда нахожу полезным иметь возможность создавать объект, который еще не «сохранен», например, у вас есть некоторый пользовательский интерфейс, который позволяет заполнять объект или объекты сотрудника, а затем проверять их снова перед некоторыми правилами сохранение в базу данных.

***** пример кода

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
источник
1
Модель анемичной области очень мало связана с логикой CRUD. Это модель, которая, хотя и принадлежит к доменному уровню, не имеет функциональных возможностей, и все функциональные возможности обслуживаются через службы, которым эта доменная модель передается в качестве параметра.
Энди
Именно в этом случае репо является сервисом, а функции - операциями CRUD.
Эван
@DavidPacker, вы говорите, что Anemic Domain Model - хорошая вещь?
candied_orange
1
@CandiedOrange Я не высказал свое мнение в комментарии, но нет, если вы решите пойти до того, чтобы погрузить ваше приложение в слои, где один слой отвечает только за бизнес-логику, я с мистером Фаулером представляю модель анемичной области на самом деле это анти-шаблон. Зачем мне нужен UserUpdateсервис с changeUsername(User user, string newUsername)методом, когда я могу так же добавить changeUsernameметод к классу Userнапрямую. Создание сервиса для этого не имеет смысла.
Энди
1
Я думаю, что в этом случае внедрение репо только для того, чтобы поместить модель CRUD в модель, не является оптимальным.
Эван
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, поскольку он отвечает за связь с базой данных, а также за временные изменения и за все транзакции, происходящие с этим объектом.

    Другим вариантом может быть сохранение изменяемых полей в другом объекте, который будет загружен только тогда, когда к полям требуется доступ.

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

  3. Я бы не стал добавлять дюжину параметров Create(), потому что бизнес-объекты могли бы развиваться и делать все это очень сложным в обслуживании. И код станет нечитаемым. Здесь у вас есть 2 варианта: либо передать минималистичный набор параметров (не более 4), которые абсолютно необходимы для создания сотрудника в базе данных, и выполнить оставшиеся изменения с помощью обновления, либо вы передаете объект. Кстати, в дизайне , я понимаю , что вы уже выбрали: my_employee.Create().

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

  5. Если вы планируете использовать команды для обновления и отдельные объекты для (GUI?) Для хранения желаемых изменений, вы можете выбрать старый / новый подход. Во всех других случаях я бы выбрал обновление изменяемого объекта. Внимание: обновление может инициировать код базы данных, поэтому после обновления необходимо убедиться, что объект все еще действительно синхронизирован с БД.

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

Christophe
источник