Найти в Дзене
"Мы"-Прогер

Изучаем C# - Пример ООП (часть 3)

Часть 1: Часть 2: Сейчас будет трудная тема, но зато она активно используется на практике. Итак, мы имеем сервис для людей - PersonService. Нам также надо создать сервисы для каждой нашей сущности: AppointmentOrderService, HolidayOrderService, DisabilitySheetService. Однако, если мы начнём их создавать, то увидим, что они похожи друг на друга. Много кода дублируется. А дублирование кода - это плохо, потому что его надо будет поддерживать: со временем придётся делать какой-то новый функционал, что-то менять, и придётся менять каждый сервис по отдельности, что долго и неудобно. Например, вот метод Create() PersonService: А вот этот же метод DisabilitySheetService: Все действия одинаковы, различаются только тип данных и названия переменных. В таких случаях нужно вынести тип данных в универсальный тип TEntity: Мы обращаемся к полю Id сущности. Но у нас нет гарантии, что у сущности есть поле Id. Поэтому здесь мы наложили ограничение на универсальный тип: он должен наследоваться от BaseEntit
Оглавление

Предыдущие статьи

Часть 1:

Часть 2:

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

Универсальные типы и наследование

Итак, мы имеем сервис для людей - PersonService. Нам также надо создать сервисы для каждой нашей сущности: AppointmentOrderService, HolidayOrderService, DisabilitySheetService. Однако, если мы начнём их создавать, то увидим, что они похожи друг на друга. Много кода дублируется. А дублирование кода - это плохо, потому что его надо будет поддерживать: со временем придётся делать какой-то новый функционал, что-то менять, и придётся менять каждый сервис по отдельности, что долго и неудобно.

Например, вот метод Create() PersonService:

А вот этот же метод DisabilitySheetService:

-2

Все действия одинаковы, различаются только тип данных и названия переменных. В таких случаях нужно вынести тип данных в универсальный тип TEntity:

-3

Мы обращаемся к полю Id сущности. Но у нас нет гарантии, что у сущности есть поле Id. Поэтому здесь мы наложили ограничение на универсальный тип: он должен наследоваться от BaseEntity, а у неё есть Id. Теперь всё хорошо.

Напишите методы Read() / Update() / Delete(). При этом у вас может возникнуть такая ошибка:

-4

Она означает, что не удалось выяснить тип данных лямбды. На самом деле, мы просто забыли подключить using для самописного метода расширения IndexOf(), вот C# и предлагает нам свой IndexOf(), который никакую лямбду принимать на вход не умеет.

Итак, мы успешно написали методы в классе, не зависящем от конкретного вида сущности. Поскольку он универсален, то он будет базовым для остальных сервисов, а они будут от него наследоваться. Назовём его BaseEntityService (слово Base в названии означает, что класс является базовым). Класс PersonService теперь будет наследоваться от него:

-5

Обратите внимание, что во время наследования мы указали конкретное значение для универсального типа TEntity - это будет Person.

Например, другим наследником BaseEntityService будет класс DisabilitySheetService - у него универсальный тип TEntity будет иметь значение DisabilitySheet:

-6

В силу наследования классы PersonService и DisabilitySheetService будут иметь внутри себя методы Create() / Read() / Update() / Delete(), потому что эти методы есть в базовом классе. Вот как удобно - только унаследовались, и все методы сразу появились!

Остался метод GetList(). Попытаемся написать его в базовом классе. Но с ним будет проблема:

-7

C# не может найти у сущности поля LastName, FirstName, MiddleName, BirthDate. В самом деле, они есть только в Person, а скажем в DisabilitySheet их нет. Эта часть логики будет зависеть от конкретной сущности. У разных классов эта часть будет разной. В таких случаях мы должны вынести эту часть в protected abstract-метод и доверить его реализацию классам-наследникам, которые будут знать, с какими типами данных они работают. Метод будет делать фильтрацию списка, поэтому назовём его FilterList():

-8

Пока что метод GetList() не будет делать ничего, кроме вызова FilterList(). Мы могли бы пока оставить реализацию GetList() за классами-наследниками и не заморачиваться с protected abstract-методом, но в перспективе у GetList() будет некая дополнительная логика, общая для всех сервисов, поэтому сделаем сразу "как надо".

Теперь надо реализовать FilterList() у сервисов-наследников. Начнём с PersonService. Сразу получаем две проблемы: список Entities не виден в классе-наследнике и мы не знаем значения параметров фильтрации fio и birthDate (красные):

-9

Сейчас Entities - статическое поле внутри класса BaseEntityService:

-10

Поскольку оно статическое, то оно принадлежит классу, а не объектам класса. Из-за этого наследование на статические поля не действует. Мы вынуждены сделать его обычным полем. Также модификатор доступа нужно изменить с private на protected, чтобы оно было видно в наследниках. Итого:

-11

Теперь перейдём к второй проблеме. Метод FilterList() должен получать переменные fio и birthDate на вход. Иного пути узнать, какой фильтр хочет пользователь, нет. Но этот метод вызывается из базового класса, значит, базовый класс должен знать об этих переменных. Но они разные у каждого класса-наследника: список людей имеет фильтры fio и birthDate, список больничных будет иметь фильтры "дату с" и "дату по" и так далее. Разным может быть даже количество переменных фильтрации. В таких случаях переменные пакуют в класс, например, PersonListFilter ("фильтр списка людей"):

-12

Теперь метод FilterList() принимает на вход ровно 1 аргумент - фильтр, а все параметры фильтрации находятся внутри него, проблема решена. Расположим такие классы-фильтры в новом namespace ListFilters.

У каждого сервиса будет свой класс фильтра: у PersonService это PersonListFilter, у DisabilitySheetService это DisabilitySheetListFilter и так далее. Например:

-13
-14

Базовый класс должен знать, какой тип данных имеет фильтр списка, поэтому мы вынесем его в ещё один универсальный тип (назовём его TListFilter):

-15

Значение для TListFilter будем указывать при наследовании:

-16
-17

Всё почти готово. Наконец, осталось сделать начальное заполнение данными поля Entities. Мы можем написать эту логику в конструкторе каждого сервиса:

-18

Однако тут мы столкнёмся с тем, что readonly-поле может быть заполнено только в конструкторе непосредственно того класса. Поэтому придётся убрать модификатор readonly. Проблема решена.

Теперь можно протестировать наш код - выдача в консоли не должна измениться:

-19

Далее

Далее мы продолжим эту же тему, чтобы лучше в ней разобраться.

Часть 4:

Оглавление

Изучаем C# с нуля - Очень краткий курс - Оглавление
"Мы"-Прогер27 января