ORM Is an Offensive Anti-Pattern

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

TL;DR ORM - ужасный антипаттерн, который нарушает все принципы объектно-ориентированного программирования, разрушая объекты и превращая их в глупые и пассивные наборы данных. Нет никаких оправданий для существования ORM в любом приложении, будь то небольшое веб-приложение или корпоративная система с тысячами таблиц и манипуляций CRUD с ними. Какова альтернатива? Объекты, говорящие на SQL.

Отображение объектно-реляционных данных (ORM) - это техника (также известная как паттерн проектирования), позволяющая получать доступ к реляционной базе данных из объектно-ориентированного языка программирования (например, Java). Существует множество реализаций ORM практически на любом языке программирования, например: Hibernate для Java, ActiveRecord для Ruby on Rails, Doctrine для PHP и SQLAlchemy для Python. В Java даже существует стандартизированный дизайн ORM под названием JPA.

Давайте для начала рассмотрим, как работает ORM, на примере. Для этого воспользуемся языком Java, СУБД PostgreSQL и Hibernate. Предположим, у нас есть одна таблица в базе данных с названием post:

Теперь мы хотим выполнять CRUD-манипуляции с этой таблицей из нашего Java-приложения (CRUD означает создание, чтение, обновление и удаление). Сначала мы должны создать класс Post (простите, что он такой длинный, но это лучшее, что я могу сделать).

Перед любой операцией с Hibernate нам необходимо создать фабрику сессий.

Этот завод будет предоставлять нам «сеансы» каждый раз, когда мы захотим манипулировать объектами Post. Каждая манипуляция с сеансом должна быть обернута в этот блок кода:

Когда сеанс готов, вот как мы получаем список всех сообщений из этой таблицы базы данных:

Думаю, здесь понятно, что происходит. Hibernate - это большой и мощный движок, который создает подключение к базе данных, выполняет необходимые запросы SQL SELECT и извлекает данные. Затем он создает экземпляры класса Post и заполняет их данными. Когда объект приходит к нам, он заполнен данными, и мы должны использовать геттеры, чтобы их извлечь, как в примере с getTitle() выше.

Когда мы хотим выполнить обратную операцию и отправить объект в базу данных, мы делаем все то же самое, но в обратном порядке. Мы создаем экземпляр класса Post, заполняем его данными и просим Hibernate сохранить его.

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

Вот как работает объектно-реляционное отображение.

Что не так с этим, вы можете спросить? Все!

Серьезно, что не так? Hibernate уже более 10 лет является одной из самых популярных библиотек Java. Почти каждое приложение с интенсивным использованием SQL в мире использует его. Каждый учебник по Java упоминает Hibernate (или, возможно, другую ORM типа TopLink или OpenJPA) для приложений, связанных с базой данных. Это стандарт de-facto, и тем не менее я говорю, что это неправильно? Да.

Я утверждаю, что сама идея ORM неправильна. Ее изобретение, возможно, было второй большой ошибкой в ООП после NULL-ссылки.

На самом деле, я не единственный, кто говорит нечто подобное, и определенно не первый. Многое на эту тему уже было опубликовано очень уважаемыми авторами, включая OrmHate Мартина Фаулера (не против ORM, но все равно стоит упомянуть), Object-Relational Mapping Is the Vietnam of Computer Science Джеффа Этвуда, The Vietnam of Computer Science Теда Ньюарда, ORM Is an Anti-Pattern Лори Восса и многих других.

Однако мой аргумент отличается от того, что они говорят. Несмотря на то, что их причины являются практическими и обоснованными, такими как “ORM медленный” или “обновления базы данных сложны”, они упускают основной момент. Вы можете увидеть очень хороший и практический ответ на эти практические аргументы, данной Божидаром Божановым в его блоге ORM Haters Don’t Get It.

Основной момент заключается в том, что ORM, вместо инкапсуляции взаимодействия с базой данных внутри объекта, извлекает его, буквально разрывая солидный и цельный живой организм. Одна часть объекта хранит данные, а другая, реализованная внутри движка ORM (фабрика сессий), знает, как работать с этими данными и передает их в реляционную базу данных. Посмотрите на эту картину; она иллюстрирует, что делает ORM.

Я, как читатель постов, должен иметь дело с двумя компонентами: 1) ORM и 2) “обрезанным” объектом, который мне возвращается. Поведение, с которым я взаимодействую, должно обеспечиваться через единую точку входа, которой является объект в ООП. В случае ORM я получаю это поведение через две точки входа - движок ORM и “вещь”, которую мы даже не можем назвать объектом.

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

SQL не скрыт. Пользователи ORM должны использовать SQL (или его диалект, например HQL). Посмотрите на приведенный выше пример; мы вызываем session.createQuery("FROM Post"), чтобы получить все сообщения. Несмотря на то, что это не SQL, оно очень похоже на него. Таким образом, реляционная модель не инкапсулирована внутри объектов. Вместо этого она выставлена на все приложение. Каждый, с каждым объектом, неизбежно должен иметь дело с реляционной моделью, чтобы получить или сохранить что-то. Таким образом, ORM не скрывает и не оборачивает SQL, а загрязняет всё приложение им.

Трудно тестировать. Когда какой-то объект работает со списком сообщений, ему нужно иметь дело с экземпляром SessionFactory. Как мы можем замокать это зависимость? Мы должны создать его мок? Насколько сложна эта задача? Посмотрите на приведенный выше код, и вы поймете, насколько громоздким и трудоемким будет такой модульный тест. Вместо этого мы можем написать интеграционные тесты и подключить всё приложение к тестовой версии PostgreSQL. В этом случае нет необходимости замокать SessionFactory, но такие тесты будут довольно медленными, и, что еще более важно, наши объекты, не имеющие никакого отношения к базе данных, будут тестироваться на экземпляре базы данных. Ужасный дизайн.

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

Что же является альтернативой? Позвольте мне показать это на примере. Давайте попробуем спроектировать класс Post по-своему. Нам придется разделить его на два класса: Post и Posts, единственное и множественное число. Я уже упоминал в одной из своих предыдущих статей, что хороший объект всегда является абстракцией реальной сущности. Вот как это принцип работает на практике. У нас есть две сущности: таблица базы данных и строка таблицы. Поэтому мы создадим два класса: Posts будет представлять таблицу, а Post будет представлять строку.

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

Вот как будет выглядеть отдельный Post:

Вот как мы будем перечислять все сообщения в таблице базы данных:

Вот как мы будем создавать новый пост:

Как видите, у нас теперь есть настоящие объекты. Они отвечают за все операции и идеально скрывают свои детали реализации. Нет никаких транзакций, сессий или фабрик. Мы даже не знаем, общаются ли эти объекты с PostgreSQL или хранят все данные в текстовых файлах. Все, что нам нужно от Posts, это возможность перечислить все сообщения для нас и создать новое. Детали реализации идеально скрыты внутри. Теперь давайте посмотрим, как мы можем реализовать эти два класса.

Я собираюсь использовать jcabi-jdbc как обертку для JDBC, но вы можете использовать что-то другое, например jOOQ или просто JDBC, если вам нравится. Это не имеет особого значения. Важно, чтобы ваши взаимодействия с базой данных были скрыты внутри объектов. Давайте начнем с Posts и реализуем его в классе PgPosts (“pg” означает PostgreSQL):

Далее, давайте реализуем интерфейс Post в классе PgPost:

Вот как будет выглядеть полная сценарий взаимодействия с базой данных с использованием только что созданных классов:

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

Я слышу, как вы кричите: “А что насчет производительности?” В том скрипте, который находится немного выше, мы делаем много избыточных обращений к базе данных. Сначала мы извлекаем идентификаторы записей с помощью SELECT id, а затем, чтобы получить их заголовки, мы делаем дополнительный запрос SELECT title для каждой записи. Это неэффективно, или, проще говоря, слишком медленно.

Не беспокойтесь; это объектно-ориентированное программирование, что означает, что оно гибкое! Давайте создадим декоратор PgPost, который будет принимать все данные в своем конструкторе и кэшировать их внутренне навсегда:

Обратите внимание: этот декоратор ничего не знает о PostgreSQL или JDBC. Он просто декорирует объект типа Post и предварительно кэширует дату и заголовок. Как обычно, этот декоратор также является неизменяемым.

Теперь давайте создадим другую реализацию Posts, которая будет возвращать “константные” объекты.

Теперь все сообщения, возвращаемые методом iterate() этого нового класса, предварительно оснащены датами и заголовками, полученными в одном проходе к базе данных.

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

Каждый объект должен обрабатывать свои собственные транзакции и инкапсулировать их так же, как запросы SELECT или INSERT. Это приведет к вложенным транзакциям, что является вполне нормальным, при условии, что сервер базы данных их поддерживает. Если такой поддержки нет, создайте объект транзакции, распространяющийся на всю сессию, который будет принимать “вызываемый” класс. Например:

Затем, когда вы хотите объединить несколько операций с объектами в одну транзакцию, сделайте это следующим образом:

Этот код создаст новый пост и оставит комментарий к нему. Если один из вызовов не выполнится успешно, весь транзакционный процесс будет отменен.

Мне кажется, что этот подход имеет объектно-ориентированное представление. Я называю его “объекты, говорящие на SQL”, потому что они знают, как общаться с базой данных на SQL-языке. Это их навык, полностью инкапсулированный внутри их границ.

Translated by ChatGPT gpt-3.5-turbo/42 on 2024-01-09 at 18:05

sixnines availability badge   GitHub stars