Generic-репозиторий - просто ленивый антипаттерн

Оригинал статьи



Generic-репозиторий часто используется для ускорения разработки слоя доступа к данным (data layer). В большинстве случае обобщение заходит слишком далеко и становится ловушкой для ленивых разработчиков.



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



T= TypeVar("T", bound=Base)



class Repository(Protocol[T]):

model: Type[T]



def get_all(self) -> List[T]: ...

def find_by(self, **kwargs) -> List[T]: ...

def get_by_id(self, id: int) -> T: ...

def add(self, item: T) -> None: ...

def update(self, item: T) -> None: ...

def delete(self, item: T) -> None: ...



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



Это протекающая абстракция



Мартин Фаулер определяет репозиторий как "объект, который является посредником между доменным слоем и слоем Data Mapper". Цель его в том, чтобы изолировать слой бизнес логики от деталей реализации доступа к данным.



Обобщенные (generic) репозитории позволяют разработчикам делать обертки над объектами нижележащей технологии (ORM, Entity Framework). В результате зависимость от технологии доступа к данным может протечь в основную логику приложения.



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



Это слишком сильное обобщение.



Большинство репозиториев нуждаются в методах "Delete" или "Save"... ну, вообще, а нуждаются ли? Одно из возражений против обобщенного репозитория состоит в том, что ленивый разработчик просто не выделил время на обдумывание, как произвольный код будет использовать репозиторий. Например, нужны ли вам какие-то специализированные методы чтения данных, которые могут поддерживать, скажем, пагинацию? Будет ли репозиторий специализироваться на чтении или обновлении данных?



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



Это определение бессмысленного контракта



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



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



def find(self, query: Any) -> Iterable[T]: ...

def find_customer_by_name(self, name: str) -> Iterable[Customer]: ...



Вторая строка намного более конкретная. Она четко определяет отношение между доменными объектом и хранилищем. Но кроме определенности контракта, её реализация будет намного более читаемой.



Для generic-репозитория есть место... но не на передовой



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



(далее в комментарии)