Я пытаюсь создать программу для управления сотрудниками. Однако я не могу понять, как спроектировать Employee
класс. Моя цель - иметь возможность создавать и управлять данными о сотрудниках в базе данных, используя Employee
объект.
Базовая реализация, о которой я подумал, была такой простой:
class Employee
{
// Employee data (let's say, dozens of properties).
Employee() {}
Create() {}
Update() {}
Delete() {}
}
Используя эту реализацию, я столкнулся с несколькими проблемами.
- Информация
ID
о сотруднике задается базой данных, поэтому, если я использую объект для описания нового сотрудника,ID
хранить его пока не удастся , а объект, представляющий существующего сотрудника, будет иметьID
. Поэтому у меня есть свойство, которое иногда описывает объект, а иногда нет (что может указывать на то, что мы нарушаем SRP ? Поскольку мы используем один и тот же класс для представления новых и существующих сотрудников ...). - Предполагается, что
Create
метод создает сотрудника в базе данных, а операторUpdate
иDelete
должен действовать на существующего сотрудника (опять- таки , SRP ...). - Какими параметрами должен обладать метод Create? Десятки параметров для всех данных сотрудника или, может быть,
Employee
объекта? - Должен ли класс быть неизменным?
- Как будет
Update
работать? Будет ли он принимать свойства и обновлять базу данных? Или, может быть, он возьмет два объекта - «старый» и «новый» и обновит базу данных с учетом различий между ними? (Я думаю, что ответ связан с ответом об изменчивости класса). - За что отвечает конструктор? Какие параметры он принимает? Будет ли он получать данные о сотрудниках из базы данных, используя
id
параметр, и они заполняют свойства?
Итак, как вы видите, у меня в голове немного беспорядка, и я очень растерялся. Не могли бы вы помочь мне понять, как должен выглядеть такой класс?
Обратите внимание, что мне не нужны мнения, просто чтобы понять, как обычно разрабатывается такой часто используемый класс.
Employee
объект для предоставления абстракции, вопросы 4. и 5. обычно не отвечают, зависят от ваших потребностей, и если вы разделяете структуру и операции CRUD на два класса, тогда совершенно ясно, что конструкторEmployee
не может получить данные от БД больше, так что отвечает 6.Update
сотрудником или обновляете запись сотрудника? ВыEmployee.Delete()
или нетBoss.Fire(employee)
?Ответы:
Это более правильная запись моего первоначального комментария под вашим вопросом. Ответы на вопросы, заданные ФП, можно найти внизу этого ответа. Также, пожалуйста, проверьте важную заметку, расположенную в том же месте.
То, что вы сейчас описываете, 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, хранилище отвечает за обнаружение того, что вы не пытаетесь сохранить уже сохраненную сущность или пытаетесь обновить сущность без идентификатора.
Важная заметка
Пожалуйста, имейте в виду, что, хотя разделение проблем велико, на самом деле разработка функционального слоя репозитория является довольно утомительной работой, и, по моему опыту, сделать ее немного сложнее, чем подход с активной записью. Но в итоге вы получите гораздо более гибкий и масштабируемый дизайн, что может быть полезно.
источник
Сначала создайте структуру сотрудника, содержащую свойства концептуального сотрудника.
Затем создайте базу данных с соответствующей структурой таблицы, например, mssql
Затем создайте репозиторий сотрудников Для этой базы данных EmployeeRepoMsSql с различными требуемыми операциями CRUD.
Затем создайте интерфейс IEmployeeRepo, представляющий операции CRUD
Затем разверните структуру Employee до класса с параметром конструкции IEmployeeRepo. Добавьте различные требуемые методы сохранения / удаления и т. Д. И используйте внедренный EmployeeRepo для их реализации.
Когда это соответствует Id, я предлагаю вам использовать GUID, который может быть сгенерирован с помощью кода в конструкторе.
Для работы с существующими объектами ваш код может извлечь их из базы данных через репозиторий перед вызовом их метода обновления.
В качестве альтернативы вы можете выбрать неодобрительную (но, на мой взгляд, более высокую) модель объекта Anemic Domain, в которой вы не добавляете методы CRUD к своему объекту, а просто передаете объект в репозиторий для обновления / сохранения / удаления.
Неизменность - это выбор дизайна, который будет зависеть от ваших шаблонов и стиля кодирования. Если вы все работаете, постарайтесь быть неизменным. Но если вы не уверены, что изменяемый объект, вероятно, проще реализовать.
Вместо Create () я бы пошел с Save (). Create работает с концепцией неизменяемости, но я всегда нахожу полезным иметь возможность создавать объект, который еще не «сохранен», например, у вас есть некоторый пользовательский интерфейс, который позволяет заполнять объект или объекты сотрудника, а затем проверять их снова перед некоторыми правилами сохранение в базу данных.
***** пример кода
источник
UserUpdate
сервис сchangeUsername(User user, string newUsername)
методом, когда я могу так же добавитьchangeUsername
метод к классуUser
напрямую. Создание сервиса для этого не имеет смысла.Обзор вашего дизайна
Вы
Employee
в действительности своего рода прокси - сервер для объекта управляемом постоянно в базе данных.Поэтому я предлагаю подумать об идентификаторе, как если бы он был ссылкой на объект вашей базы данных. Имея в виду эту логику, вы можете продолжить проектирование, как если бы это делалось для объектов, не относящихся к базе данных, идентификатор, позволяющий реализовать традиционную композиционную логику
Employee
возможно, он еще не создан или просто удален.Вам также необходимо управлять статусом объекта. Например:
Имея это в виду, мы могли бы выбрать:
Чтобы иметь возможность надежно управлять состоянием вашего объекта, вы должны обеспечить лучшую инкапсуляцию, сделав свойства частными и предоставляя доступ только через методы получения и установки, которые устанавливают состояние обновления.
Ваши вопросы
Я думаю, что свойство ID не нарушает SRP. Его единственная обязанность - ссылаться на объект базы данных.
Ваш сотрудник в целом не соответствует требованиям SRP, поскольку он отвечает за связь с базой данных, а также за временные изменения и за все транзакции, происходящие с этим объектом.
Другим вариантом может быть сохранение изменяемых полей в другом объекте, который будет загружен только тогда, когда к полям требуется доступ.
Вы можете реализовать транзакции базы данных на Employee, используя шаблон команды . Такая конструкция также облегчила бы разделение между вашими бизнес-объектами (Сотрудник) и базовой системой баз данных, изолировав специфические для базы данных идиомы и API.
Я бы не стал добавлять дюжину параметров
Create()
, потому что бизнес-объекты могли бы развиваться и делать все это очень сложным в обслуживании. И код станет нечитаемым. Здесь у вас есть 2 варианта: либо передать минималистичный набор параметров (не более 4), которые абсолютно необходимы для создания сотрудника в базе данных, и выполнить оставшиеся изменения с помощью обновления, либо вы передаете объект. Кстати, в дизайне , я понимаю , что вы уже выбрали:my_employee.Create()
.Должен ли класс быть неизменным? Смотрите обсуждение выше: в вашем оригинальном дизайне нет. Я бы выбрал неизменный идентификатор, но не неизменный сотрудник. Сотрудник развивается в реальной жизни (новая должность, новый адрес, новая супружеская ситуация, даже новые имена ...). Я думаю, что будет легче и естественнее работать с этой реальностью, по крайней мере, на уровне бизнес-логики.
Если вы планируете использовать команды для обновления и отдельные объекты для (GUI?) Для хранения желаемых изменений, вы можете выбрать старый / новый подход. Во всех других случаях я бы выбрал обновление изменяемого объекта. Внимание: обновление может инициировать код базы данных, поэтому после обновления необходимо убедиться, что объект все еще действительно синхронизирован с БД.
Я думаю, что выбор сотрудника из БД в конструкторе не является хорошей идеей, потому что выборка может пойти не так, и во многих языках трудно справиться с неудачной конструкцией. Конструктор должен инициализировать объект (особенно идентификатор) и его статус.
источник