В статье, перевод которой предложен ниже, Роберт Мартин вроде как начинает с мыслей, очень похожих на те, что можно увидеть в рассуждениях Егора Бугаенко про ORM, но выводы делает другие. Лично мне подход Егора импонирует, но я считаю, что Мартин подробнее раскрывает тему. Мне кажется, с его мнением стоит познакомиться всем, кто когда-либо задумывался о том, какое место должен занимать ORM и вообще, зачем нужны объекты у которых все поля открыты. Статья написана в жанре "Диалог", где более опытный программист обсуждает проблему с тем, у кого опыта меньше.


Что такое класс

Класс это спецификация множества сходных объектов


Что такое объект?

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


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

В смысле "подразумевается"?


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

Разве данные не находятся в объекте?


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

Ну допустим.


Хорошо, а что такое структура данных?

Структура данных это набор связанных элементов.


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

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


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

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


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

Ух ты! Так выходит что объекты и структуры данных это не одно и то же!


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

И таблицы в базах данных это тоже не объекты, правда?


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

Погоди. Разве ORM не отображает таблицы из БД в объекты?


Конечно нет. Отобразить таблицы БД в объекты нельзя. Таблицы в БД это структуры данных, а не объекты.

Тогда чем занимаются ORM?


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

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


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

Но мне говорили, что ORM собирают бизнес объекты.


Нет, ORM вытаскивают из БД данные, с которыми работают бизнес объекты

Но разве эти структуры данных не попадают в бизнес объекты?


Может и попадают, а может и нет. ORM ничего про это не знает.

Но разница же чисто семантическая.


Да нет. Тут есть далеко идущие следствия.

Например?


Например дизайн схемы БД и дизайн бизнес объектов. Бизнес объекты задают бизнес поведение. Схема базы данных задает структуру бизнес данных. Эти структуры ограничены очень разными силами. Структура бизнес данных не обязательно лучшая структура для бизнес поведения.

Ээээ. Это непонятно.


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

Это понятно.


Хорошо. А теперь подумай про каждое отдельное приложение. Объектная модель каждого приложения описывает как структурировано поведение приложения. У каждого приложения будет собственная объектная модель, чтобы лучше соответствовать поведению приложения.

Ааа, понятно. Так как схема данных это компромисс между разными приложениями, схема не будет ложиться на объектную модель каждого отдельного приложения.


Правильно! Объекты и Структуры ограничены разными вещами. Они очень редко подходят друг другу. Люди называют это рассогласованием объектно реляционного импеданса

Что-то такое я помню. Но вроде рассогласование импеданса как раз исправили с помощью ORM.


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

Чего?


Они противоположности, а не что-то похожее.

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


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

Что?


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

Почем фигуры пихают во все примеры код с объектами?


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

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


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

Ладно. Конечно. Объект знает как реализованы его методы. Естественно.


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

Дискриминейтед что?


Discriminated Unions. Ну, С++, указатели, ключевое слово union, флаг для определения типа структуры, Discriminated Unions. В нашем случае это просто две разные структуры данных. Одна для Квадрата, а другая для Круга. У Круга есть центральная точка и радиус. И код типа, из которого можно понять, что это Круг.

Поле с код будет enum?


Ну да. А у Квадрата будет верхняя левая точка и длина стороны. И ещё enum для указания типа.

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


Правильно. Теперь посмотрим на функцию для площади. Там наверняка будет switch, правда?

Ну. Конечно, для двух классов. Ветка для Квадрата и для Круга. И для периметра тоже нужен похожий switch.


И опять верно. Теперь подумай об этих двух сценариях. В сценарии с объектами две реализации функций для площади, независимы друг от друга и принадлежат (в некотором смысле) непосредственно типу. Функция для площади Квадрата принадлежит Квадрату, а функция для определения площади Круга принадлежит Кругу.

Ладно, я понимаю к чему ты ведёшь. В сценарии со структурами данных обе реализации функции для площади находятся в одной и той же функции, они не "принадлежат" (что бы это слово не значило) типу.


Дальше лучше. В случае с объектами если нужно добавить тип Треугольник, какой код надо поменять?

Не надо менять вообще ничего. Просто сделать новый класс Треугольник. Хотя нет, наверное надо поправить код, который создаёт объекты.


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

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


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

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


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

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


Ага Добавлять функции просто.

Ух ты. Получается, что эти два подхода прямо противоположны.


Определённо да. Подведём итоги

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

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


Хорошее наблюдение! Но надо сегодня нам надо подумать о ещё одной штуке. Есть ещё один смысл в котором структуры данных и классы являются противоположностями друг друга. Зависимости.

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


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

Ладно, я спрошу. В чём разница?


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

Да, так и есть. И что?


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

А что ты имеешь в виду, когда говоришь "зависит"?


Представь себе, что каждая реализация функции для площади выделена в отдельную функцию. То есть будут функции circleArea, squareArea и triangleArea.

Хорошо, получается что в ветках switch будут просто вызовы этих функций.


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

Тогда в файле со switch будет import или use или include для файлов с функциями.


Точно. Это и есть зависимость на уровне исходного кода. Один исходник зависит от другого исходника. Как направлена эта зависимость?

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


А как насчёт кода, вызывающего функцию для площади?

Вызывающий код зависит от кода со switch, который зависит от всех реализаций.


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

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


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

А это в общем все системы со статической типизацией, да?


Да, а ещё некоторые системы без неё

Это много надо пересобрать.


А ещё много передеплоить.

Ладно, а в случае с классами всё наоборот?


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

Понятно. В коде для класса Квадрат будет import или use или include файла с интерфейсом Фигура.


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

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


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

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


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

Ладно, давай я суммирую это всё. Классы и структуры данных противоположны друг другу в трёх смыслах.


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

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

Комментарии (16)


  1. SbWereWolf
    28.11.2019 00:11

    у меня крыша поехала от этого чтения


    1. babylon
      29.11.2019 16:39

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


  1. indestructable
    28.11.2019 00:28

    Забавно распространить рассуждения о классах vs структурах данных на anemic и rich domain model


  1. paranoya_prod
    28.11.2019 10:23

    Структура данных это набор связанных элементов.

    Связано ли имя человека с местом проживания, то есть, на улице Вязов живут люди с именем Фредди?
    Я это к тому, что не по-русски это как-то. На мой занудный взгляд: структура данных — это элементы, объединённые/сгрупированные по определённому признаку.


  1. dyadyaSerezha
    28.11.2019 12:20

    "называют это Объектно/Реляционным рассогласованием импеданса" — неверно. Верно — рассогласование объектно/реляционного импеданса.


    Ну и количество/время пересборки сейчас вообще никого не волнует.


    1. poxvuibr Автор
      28.11.2019 12:30

      Спасибо, поправил.


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


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


      1. vintage
        28.11.2019 19:13

        В нормальных языках с нормальной модульной системой всё это не является проблемой вообще. Это боль исключительно С++.


        1. poxvuibr Автор
          28.11.2019 19:47

          А в каких языках нормальная модульная система?


          1. vintage
            28.11.2019 20:05
            -1

            Да хотя бы даже в D.


  1. vintage
    28.11.2019 19:18

    Интересно, как в его картину мира вписываются перегрузки функций и объектные субд?


    1. poxvuibr Автор
      28.11.2019 19:46

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


      1. vintage
        28.11.2019 20:09

        Перегрузки функций — механизм времени компиляции

        И? В том же D вы легко можете написать функцию, которая на этапе компиляции пройдётся по всем типам структур и всем функциям и составит карту соответствия типа и функции обработки.


        объекты в объектных субд имеют с объектами из ООП не больше общего, чем ДТО

        Да нет, там полноценные объекты, с классами, наследованием, методами и вот этим всем.


        1. poxvuibr Автор
          28.11.2019 20:30

          Это придётся перекомпилировать весь код, а потом его деплоить.


          А насчёт объекнтых субд с полноценными объектами действительно интересно, что бы сказал Мартин


          1. vintage
            29.11.2019 08:00

            Вы так говорите будто это долгий процесс.


            1. poxvuibr Автор
              29.11.2019 09:55

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


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


              1. vintage
                29.11.2019 10:05

                Зависимые модули сами перекомпилируются. Если же вы про зависимости пакетов, то они публикуются в виде исходников, а не бинарниками.