Мы завершаем изучение объектно-ориентированного программирования, и теперь нам нужен хороший пример, на котором можно было бы всё это повторить.
Пусть мы хотим сделать систему для веб-приложения, работающего с электронными документами. Это большая и сложная задача, но зато она приближена к тому, что делают в реальной жизни.
Электронные документы каждого вида можно создавать (Create), читать (Read), обновлять (Update) и удалять (Delete). Подобные системы называются CRUD - по первым буквам действий.
Если в предыдущих статьях вы уже пользовались гитом, переключитесь на ветку master (необходимо сначала сделать коммит) и начните от неё новую ветку services_example. Если нет - то Git - это необязательно.
Классы сущностей
Какие виды электронных документов наиболее распространены? Приказ о найме, приказ об отпуске, приказ о денежном поощрении, больничный. Какие виды объектов у нас будут - такие классы мы и должны создать.
А какие свойства будут у них - такие поля и будут в наших классах. У всех приказов есть номер приказа, дата, сотрудник, автор приказа. Значит, все приказы должны наследоваться от базового класса приказа, в котором будут эти поля. Вообще, если в нескольких классах есть одинаковые поля - значит, мы должны попытаться вынести эти поля в общий базовый класс.
Кроме того, все электронные документы должны иметь идентификатор Guid Id, чтобы можно было отличать один документ от другого. Например, пользователь может открыть один приказ об отпуске и поменять в нём номер, дату и сотрудника. Все видимые поля в приказе поменяются, но мы должны как-то понять, что это тот самый приказ, который пользователь ранее открыл на редактирование, чтобы изменить именно его, а не создавать новый. Поэтому везде нужен Id.
Далее, сотрудник имеет внутри себя фамилию, имя, отчество, дату рождения. То есть, это составной тип данных. А под любой составной тип данных нужно создавать свой класс. То есть, хотя изначально мы это и не планировали, но потребуется создать класс "Сотрудник". Внутри него, конечно, тоже будет поле Id. А если какое-то поле есть в нескольких классах - значит, это поле нужно вынести в базовый класс. Так мы получаем, что у нас будет класс "Базовый документ", ещё более базовый, чем "Базовый приказ" - от него будут наследоваться не только приказы, но и люди, и больничные.
Наконец, поскольку у нас будет много классов, то мы разделим их на разные namespace. У всех классов этого примера будет namespace ServiceExample, а у сущностей в частности будет ServiceExample.Entity:
Можно приступать к созданию классов сущностей:
Класс "Базовая сущность" абстрактный, потому что мы не можем создать документ не-пойми-какого типа.
Отчество у человека может отсутствовать, поэтому MiddleName имеет nullable-тип "string?".
Обратите внимание, что у класса BaseOrder полями являются объекты класса Person.
Назначение может быть как временное, так и постоянное, поэтому дата окончания может отсутствовать.
У отпуска дата окончания всегда присутствует, а вот у больничного её может не быть (человека ещё не выписали):
Также у больничного нужно явно указать человека (поле Employee), а в приказе об отпуске человек уже есть, потому что он есть в BaseOrder.
Чтобы открыть диаграмму связей между классами в Rider, нажмём правой кнопкой на BaseEntity и выберем Go to - Derived Symbols. Далее выберем Show on Diagram:
Здесь стрелки означают наследование. Точечная стрелка от BaseOrder к Person означает, что внутри BaseOrder используются объекты класса Person. Такое отношение между классами называется агрегацией. Если мы отключим эту кнопку, то точечная стрелка исчезнет и вся схема перестроится:
Классы сервисов. Выдача сущностей в ответ.
Делать много изменений в коде без тестирования плохо - потом трудно будет найти ошибку. Когда мы сделали мало изменений, то ошибка может быть только в тех строках, которые мы изменили, и найти её легче. Чтобы как можно скорее проверить работоспособность программы, начнём с сервиса для людей и сделаем там наполнение тестовыми данными:
На строке 7 мы заводим список людей. Согласно принципу абстракции, мы указываем минимальный необходимый нам тип переменной Persons - всего лишь IEnumerable, чтобы в случае чего потом можно было заменить список List на что-то другое. Здесь строки 8-25 занимает инициализатор списка, в котором через запятую написаны элементы списка-люди. Всего людей у нас двое: на 9-16 строках и на 17-24 строках. Для ясности мы могли бы создать этих людей отдельно от списка и положить их туда через метод Append(). Это только начальное заполнение списка, в процессе работы приложения пользователь сможет добавлять/редактировать/удалять данные о людях.
Список хранится в статическом поле, так как эти данные общие для всей программы (так мы имитируем базу данных). Модификатор readonly означает, что мы не можем перезаписать список целиком, что не мешает изменять состав элементов внутри списка.
Сделаем метод, который позволит пользователю просматривать список и искать что-либо в нём:
Метод принимает на вход фильтры, которые могут быть null, если пользователь не хочет ничего отфильтровать (в таком случае список людей должен выдаваться полностью). Если фильтры не null, то они применяются к списку. Условия фильтрации - лямбды (на строках 32-35 и 41), которые получают на вход человека и возвращают в ответ bool - оставлять его в списке или отфильтровывать. Лямбда в строках 32-35, проверяя человека p, конструирует его ФИО через пробелы, приводит к верхнему регистру, а затем проверяет, что оно содержит строку, набранную пользователем в фильтре (тоже приведённую к верхнему регистру). Приведение к верхнему регистру делает поиск регистронезависимым - если пользователь наберёт "пётр" вместо "Пётр", то оно превратится в "ПЁТР", а ФИО человека превратится в "ПЕРВЫЙ ПЁТР ПЕТРОВИЧ", так что мы всё равно найдём нужного человека. Результатом метода Where() является не список, а всего лишь IEnumerable, поэтому переменная result имеет тип IEnumerable. Результат приводится к списку в конце - в строке 44.
Теперь протестируем наш сервис:
Чтобы удобно печатать человека, переопределим у Person метод ToString():
Сейчас, если у человека отсутствует отчество (MiddleName), вместо него будут печататься два пробела. Чтобы убрать лишний пробел, сделаем печать второго пробела вместе с MiddleName по условию:
Нужно тестировать все возможные случаи, а не только когда все фильтры null. Поэтому сделаем вызов GetList() с ненулевым fio, с ненулевой birthDate (допишите самостоятельно) и с обоими ими:
Результат:
Также нужен тест, где будет выдан пустой список.
Далее
Это только начало нашей практики. Далее мы будем делать CRUD-сервисы и учиться тестировать код:
Оглавление