Объекты Против Структур Данных

В статье, перевод которой предлагается ниже, Роберт Мартин вроде бы начинает с мыслей, очень похожих на те, которые можно увидеть в рассуждениях Егора Бугаенко об ОРМ, но выводы он делает иные.

Лично мне нравится подход Егора, но думаю, что Мартин раскрывает тему более подробно.

Мне кажется, с его мнением стоит ознакомиться всем, кто хоть раз задумывался о том, какое место должен занимать ORM и вообще, зачем нужны объекты со всеми полями открытыми.

Статья написана в жанре «Диалог», где более опытный программист обсуждает проблему с менее опытным.

Что такое класс
Класс — это спецификация набора похожих объектов.

Что такое объект?
Объект — это набор функций, которые выполняют действия над инкапсулированными данными.

Или лучше сказать, объект — это набор функций, которые выполняют действия над данными, существование которых подразумевается.

Что значит «подразумевается»?
Поскольку у объекта есть функции, то можно предположить, что данные там тоже есть, но прямого доступа к данным нет и их вообще не видно снаружи.

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

С точки зрения пользователя объект — это не что иное, как набор функций.

Данные, с которыми работают эти функции, должны существовать, но местонахождение этих данных пользователю неизвестно.

Давайте предположим.

Хорошо, что такое структура данных?
Структура данных — это совокупность связанных элементов.

Или, другими словами, структура данных — это набор элементов, с которыми работают функции, существование которых подразумевается неявно.

ЛАДНО ЛАДНО.

Я понимаю.

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

Верно.

И что можно отметить по поводу этих двух определений?

В каком-то смысле они являются противоположностями друг друга.

Действительно.

Они дополняют друг друга.

Как рука и перчатка.

  • Объект — это набор функций, которые работают с элементами данных, существование которых неявно.

  • Структура данных — это набор элементов данных, с которыми оперируют функции, существование которых подразумевается неявно.

Ух ты! Вот и получается, что объекты и структуры данных — это не одно и то же!
Верно.

Структуры данных — это DTO.

И таблицы в базах данных тоже не являются объектами, верно?
Опять правда.

Базы данных содержат структуры данных, а не объекты.

Подождите минуту.

Разве ORM не сопоставляет таблицы из базы данных с объектами?

Конечно, нет. Невозможно сопоставить таблицы базы данных с объектами.

Таблицы в базе данных представляют собой структуры данных, а не объекты.

Тогда что делают ORM?
Они передают данные из одной структуры в другую.

Получается, они не имеют никакого отношения к Объектам?
Ничего вообще.

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

Но мне сказали, что ОРМ собирают бизнес-объекты.

Нет, ORM извлекают данные из базы данных, с которой работают бизнес-объекты.

Но разве эти структуры данных не относятся к бизнес-объектам?
Может быть, они туда доберутся, а может быть, и нет. ОРМ ничего об этом не знает.
Но разница чисто смысловая.

Не совсем.

Здесь есть далеко идущие последствия.

Например?
Например, проектирование схемы базы данных и проектирование бизнес-объектов.

Бизнес-объекты определяют бизнес-поведение.

Схема базы данных определяет структуру бизнес-данных.

Эти структуры сдерживаются совершенно разными силами.

Структура бизнес-данных не обязательно является лучшей структурой для делового поведения.

?Эм-м-м.

Это не ясно.

Подумайте об этом таким образом.

Схема данных не предназначена для одного приложения, она предназначена для использования в масштабах всего предприятия.

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

Ясно.

Отлично.

Теперь подумайте о каждом отдельном приложении.

Объектная модель каждого приложения описывает структуру поведения приложения.

Каждое приложение будет иметь собственную объектную модель, которая лучше соответствует поведению приложения.

ААА понятно.

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

Верно! Объекты и структуры ограничены разными вещами.

Они очень редко подходят друг другу.

Люди называют это объектно-реляционным несоответствием импедансов.

Я помню что-то вроде этого.

Но похоже рассогласование импедансов исправили с помощью ORM.

И теперь вы знаете, что это не так.

Несоответствие импедансов между объектами и структурами данных является дополнительным, а не изоморфным.

Что?
Это противоположности, а не нечто подобное.

Противоположности?
Да, очень интересным образом.

Видите ли, объекты и структуры данных предполагают наличие диаметрально противоположных управляющих структур.

Что?
Представьте себе набор классов, реализующих некий общий интерфейс.

Например, представьте себе классы, представляющие двумерные фигуры, имеющие функции для вычисления площади и периметра фигуры.

Почему во все примеры цифры пихают код с объектами?
Давайте рассмотрим два разных типа фигур: квадраты и круги.

Понятно, что функции вычисления площади и периметра этих классов используют разные структуры данных.

Также ясно, что эти операции вызываются с использованием динамического полиморфизма.

Помедленнее, пожалуйста, ничего не ясно.

Существует две разные функции для расчета площади: одна для квадрата и одна для круга.

Когда функция вызывается для вычисления площади конкретного объекта, именно этот объект решает, какую конкретную функцию вызывать.

Это называется динамическим полиморфизмом.

ХОРОШО.

Конечно.

Объект знает, как реализованы его методы.

Естественно.

Теперь давайте превратим эти объекты в структуры данных.

Мы используем дискриминационные профсоюзы.

Дискриминировал что?
Дискриминационные профсоюзы.

Ну, C++, указатели, ключевое слово объединения, флаг для определения типа структуры, Размеченные объединения.

В нашем случае это просто две разные структуры данных.

Один для квадрата, другой для круга.

Круг имеет центральную точку и радиус.

И код типа, по которому можно понять, что это Круг.

Поле с кодом будет перечислением?
Ну да.

А у Квадрата будет верхняя левая точка и длина стороны.

А также перечисление для указания типа.

ХОРОШО.

Будет две структуры с кодом типа.

Верно.

Теперь давайте посмотрим на функцию площади.

Наверное, там будет переключатель, да?

Хорошо.

Конечно, за два занятия.

Ветка для Квадрата и для Круга.

А для периметра тоже понадобится аналогичный выключатель.

И снова правда.

Теперь подумайте об этих двух сценариях.

В объектном сценарии две реализации функций площади независимы друг от друга и принадлежат (в некотором смысле) непосредственно типу.

Функция определения площади Квадрата принадлежит Square, а функция определения площади Круга принадлежит Circle.

Хорошо, я понимаю, к чему вы клоните.

В сценарии структуры данных обе реализации функции площади находятся в одной и той же функции; они не «принадлежат» (что бы ни означало это слово) к этому типу.

Дальше лучше.

В случае с объектами, если вам нужно добавить тип «Треугольник», какой код следует изменить?

Нет необходимости вообще что-либо менять.

Просто создайте новый класс Triangle. Хотя нет, наверное надо подправить код, создающий объекты.

Верно.

Итак, при добавлении нового типа изменения незначительны.

Теперь предположим, что нам нужно добавить новую функцию — например, функцию определения центра.

Тогда вам придется добавить его ко всем трем типам: Круг, Квадрат и Треугольник.

Отлично.

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

А вот со структурами данных все по-другому.

Чтобы добавить треугольник, вам придется изменить каждую функцию, чтобы добавить ветви к каждому переключателю для обработки треугольника.

Верно.

Добавлять типы сложно; вам придется редактировать каждую функцию.

Но для того, чтобы добавить функцию для центра, не нужно ничего менять.

Да, добавить функции легко.

Ух ты.

Оказывается, эти два подхода прямо противоположны.

Определенно да.

Давайте подведем итоги

  • Добавлять новые функции в классы сложно; вам придется вносить изменения в каждом классе
  • Добавлять новые функции для структур данных легко, нужно просто добавить функцию, больше ничего менять не нужно.

  • Добавлять новые типы в классы легко, вам просто нужно добавить новый класс.

  • Добавлять новые типы для структур сложно, нужно исправлять каждую функцию
Да.

Противоположности.

Противоположности в любопытном смысле.

То есть, если вы заранее знаете, что нужно добавлять новые функции, удобно использовать структуры данных.

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

Хорошее наблюдение! Но сегодня нам нужно подумать еще об одном.

Есть еще один смысл, в котором структуры данных и классы являются противоположностями друг друга.

Зависимости.

Зависимости?
Да, направление зависимостей в исходном коде.

Хорошо, я спрошу.

В чем разница?

Давайте посмотрим на случай структур.

Каждая функция содержит переключатель, который выбирает нужную реализацию на основе кода типа в объединении.

Да, именно так.

И что?

Давайте посмотрим на вызов функции для площади.

Вызывающий код зависит от функции области, а функция области зависит от каждой конкретной реализации.

Что вы имеете в виду, когда говорите «зависит»?
Представьте, что каждая реализация функции площади выделена в отдельную функцию.

То есть будут функции CircleArea, SquareArea и TriangleArea.

Что ж, получается, что ветки коммутатора будут просто вызывать эти функции.

Представьте, что эти функции находятся в разных файлах.

Тогда в файле переключателя будет импорт или использование или включение для файлов с функциями.

Точно.

Это зависимость на уровне исходного кода.

Один источник зависит от другого источника.

Как направлена эта зависимость?

Исходный код переключателя зависит от исходного кода, в котором находятся реализации.

А как насчет кода, вызывающего функцию площади?
Код вызова зависит от кода коммутатора, который зависит от всех реализаций.

Верно.

Во всех источниках стрелка направлена по направлению вызова, от вызывающего кода к реализации.

Итак, если вы хотите внести небольшие изменения в эти реализации.

Ладно, ладно, я понимаю, к чему вы клоните.

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

Да.

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

И это вообще все системы со статической типизацией, да?
Да и некоторые другие системы без него
Это очень много, чтобы собрать заново.

А еще есть много передислокаций.

Хорошо, а в случае с классами все наоборот?
Да, потому что код, вызывающий функцию площади, зависит от интерфейса, и реализация также зависит от этого интерфейса.

Ясно.

Код класса Square будет импортировать, использовать или включать файл с интерфейсом Shape.

Верно.

Стрелка в файлах реализации указывает в направлении, противоположном вызову.

Он направляется от кода реализации к вызывающему коду.

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

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

О да, я понял.

То есть, если вы внесете изменения в одну из реализаций.

Только код с этими изменениями нужно пересобрать и переразвернуть.

Это связано с тем, что зависимости направлены противоположно направлению вызовов.

Да, мы называем это инверсией зависимостей.

Хорошо, позвольте мне подвести итог.

Классы и структуры данных противоположны по трем причинам.

  • Функции существуют в классах явно, но о существовании данных можно только догадываться.

    Структуры данных явно содержат данные, но о том, какие там функции, можно только догадываться.

  • С классами добавлять типы легко, но добавлять функции сложно.

    Со структурами добавлять функции легко, но добавлять типы сложно.

  • Структуры данных приводят к перекомпиляции и повторному развертыванию вызывающего кода.

    Классы изолируют вызывающий код и не требуют повторной компиляции и развертывания.

Да все верно.

И каждый дизайнер и архитектор программного обеспечения должен помнить об этом.

Теги: #программирование #java #ООП #структуры данных #структуры данных #Роберт Мартин #чистый код #Егор Бугаенко #дядя Боб #объекты
Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.