Предыдущие статьи
Часть 1:
Часть 2:
Сейчас будет трудная тема, но зато она активно используется на практике.
Универсальные типы и наследование
Итак, мы имеем сервис для людей - PersonService. Нам также надо создать сервисы для каждой нашей сущности: AppointmentOrderService, HolidayOrderService, DisabilitySheetService. Однако, если мы начнём их создавать, то увидим, что они похожи друг на друга. Много кода дублируется. А дублирование кода - это плохо, потому что его надо будет поддерживать: со временем придётся делать какой-то новый функционал, что-то менять, и придётся менять каждый сервис по отдельности, что долго и неудобно.
Например, вот метод Create() PersonService:
А вот этот же метод DisabilitySheetService:
Все действия одинаковы, различаются только тип данных и названия переменных. В таких случаях нужно вынести тип данных в универсальный тип TEntity:
Мы обращаемся к полю Id сущности. Но у нас нет гарантии, что у сущности есть поле Id. Поэтому здесь мы наложили ограничение на универсальный тип: он должен наследоваться от BaseEntity, а у неё есть Id. Теперь всё хорошо.
Напишите методы Read() / Update() / Delete(). При этом у вас может возникнуть такая ошибка:
Она означает, что не удалось выяснить тип данных лямбды. На самом деле, мы просто забыли подключить using для самописного метода расширения IndexOf(), вот C# и предлагает нам свой IndexOf(), который никакую лямбду принимать на вход не умеет.
Итак, мы успешно написали методы в классе, не зависящем от конкретного вида сущности. Поскольку он универсален, то он будет базовым для остальных сервисов, а они будут от него наследоваться. Назовём его BaseEntityService (слово Base в названии означает, что класс является базовым). Класс PersonService теперь будет наследоваться от него:
Обратите внимание, что во время наследования мы указали конкретное значение для универсального типа TEntity - это будет Person.
Например, другим наследником BaseEntityService будет класс DisabilitySheetService - у него универсальный тип TEntity будет иметь значение DisabilitySheet:
В силу наследования классы PersonService и DisabilitySheetService будут иметь внутри себя методы Create() / Read() / Update() / Delete(), потому что эти методы есть в базовом классе. Вот как удобно - только унаследовались, и все методы сразу появились!
Остался метод GetList(). Попытаемся написать его в базовом классе. Но с ним будет проблема:
C# не может найти у сущности поля LastName, FirstName, MiddleName, BirthDate. В самом деле, они есть только в Person, а скажем в DisabilitySheet их нет. Эта часть логики будет зависеть от конкретной сущности. У разных классов эта часть будет разной. В таких случаях мы должны вынести эту часть в protected abstract-метод и доверить его реализацию классам-наследникам, которые будут знать, с какими типами данных они работают. Метод будет делать фильтрацию списка, поэтому назовём его FilterList():
Пока что метод GetList() не будет делать ничего, кроме вызова FilterList(). Мы могли бы пока оставить реализацию GetList() за классами-наследниками и не заморачиваться с protected abstract-методом, но в перспективе у GetList() будет некая дополнительная логика, общая для всех сервисов, поэтому сделаем сразу "как надо".
Теперь надо реализовать FilterList() у сервисов-наследников. Начнём с PersonService. Сразу получаем две проблемы: список Entities не виден в классе-наследнике и мы не знаем значения параметров фильтрации fio и birthDate (красные):
Сейчас Entities - статическое поле внутри класса BaseEntityService:
Поскольку оно статическое, то оно принадлежит классу, а не объектам класса. Из-за этого наследование на статические поля не действует. Мы вынуждены сделать его обычным полем. Также модификатор доступа нужно изменить с private на protected, чтобы оно было видно в наследниках. Итого:
Теперь перейдём к второй проблеме. Метод FilterList() должен получать переменные fio и birthDate на вход. Иного пути узнать, какой фильтр хочет пользователь, нет. Но этот метод вызывается из базового класса, значит, базовый класс должен знать об этих переменных. Но они разные у каждого класса-наследника: список людей имеет фильтры fio и birthDate, список больничных будет иметь фильтры "дату с" и "дату по" и так далее. Разным может быть даже количество переменных фильтрации. В таких случаях переменные пакуют в класс, например, PersonListFilter ("фильтр списка людей"):
Теперь метод FilterList() принимает на вход ровно 1 аргумент - фильтр, а все параметры фильтрации находятся внутри него, проблема решена. Расположим такие классы-фильтры в новом namespace ListFilters.
У каждого сервиса будет свой класс фильтра: у PersonService это PersonListFilter, у DisabilitySheetService это DisabilitySheetListFilter и так далее. Например:
Базовый класс должен знать, какой тип данных имеет фильтр списка, поэтому мы вынесем его в ещё один универсальный тип (назовём его TListFilter):
Значение для TListFilter будем указывать при наследовании:
Всё почти готово. Наконец, осталось сделать начальное заполнение данными поля Entities. Мы можем написать эту логику в конструкторе каждого сервиса:
Однако тут мы столкнёмся с тем, что readonly-поле может быть заполнено только в конструкторе непосредственно того класса. Поэтому придётся убрать модификатор readonly. Проблема решена.
Теперь можно протестировать наш код - выдача в консоли не должна измениться:
Далее
Далее мы продолжим эту же тему, чтобы лучше в ней разобраться.
Часть 4:
Оглавление