How Data Visibility Hurts Maintainability

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

Я так много пишу о объектно-ориентированном программировании и его недостатках, утверждая, что большинство шаблонов проектирования и “хороших практик”, к которым мы привыкли, на самом деле неправильны и вредны, что я совершенно забыл объяснить большую картину проблемы. Кто-то спросил меня некоторое время назад в блоге о “голых” данных: в чем заключается проблема, которую мы решаем, и почему именно поддерживаемость страдает, если мы недостаточно инкапсулируем наши данные? Вот ответ.

Я перечитал первые несколько страниц Elegant Objects, Volume 1, моей книги, полностью посвященной проблемам современного объектно-ориентированного программирования, и обнаружил, что поддерживаемость прямо упоминается там: “Основная цель, которую я пытаюсь достичь этой книгой, - увеличить поддерживаемость вашего кода”, а затем также объясняется, что поддерживаемость - это “время, требуемое для понимания вашего кода”. Вы можете спорить с этим, но вопрос остается: как отсутствие “настоящей” объектно-ориентированности и правильной инкапсуляции влияет на читаемость?

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

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

Теперь область видимости i составляет десять строк кода. Код работает так же хорошо, как в первом отрывке, но его поддерживаемость ниже, потому что, чтобы понять, что происходит и как его изменить, мне требуется больше времени. Мне нужно прочитать блок кода из десяти строк, а не из двух блоков по три строки. Мне нужно понять весь метод print(), прежде чем я смогу начать вносить изменения. Мне нужно понять алгоритм жизненного цикла этого бедного i и почему, например, он уменьшается на 10, а не присваивается ноль - это сюрприз, который оставили мне предыдущие программисты. Возможно, они не знали о существовании циклов for?

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

Например, самая большая область видимости, которую можно представить в C/C++, Python, Ruby и многих других современных языках, - это глобальная область видимости, например, здесь:

Теперь переменная i видна не только внутри функции print(), но и во всех других местах разрабатываемого приложения. Область видимости i охватывает всю базу кода. Не нужно говорить, что код функции print() становится очень трудночитаемым. Я просто не могу знать, какое значение ожидать в i, когда начинается выполнение print() - мне приходится просматривать всю базу кода, чтобы это выяснить. Если это небольшое приложение, возможно, я справлюсь, но если это большое программное обеспечение, у меня будут большие проблемы. Итак, что насчет создания такого языка программирования, который не разрешит глобальные переменные? Это решит проблему. Программисты не смогут технически определить их, и их области видимости неизбежно будут меньше.

Я считаю, что объекты были изобретены именно для этого: заставить программистов держать области видимости меньше. Ну, на самом деле, функции и подпрограммы тоже были изобретены для этого, но с меньшим акцентом на часть “заставлять”, так как они могли сосуществовать с кодом, который еще не был разделен. Напротив, объекты должны были быть гражданами первого класса в объектном мире, взаимодействуя только с объектами.

Но потом появился C++ и испортил все.

Давайте попробуем добавить объект в наш код на C, так, как бы сделали это большинство некоторые программисты на C++.

And then:

void print() {
  Idx i();
  while (i.get() < 10) {
    printf("%d * 2 = %d", i.get(), i.get() * 2);
    i.add(1);
  }
  i.add(-10);
  while (i.get() < 10) {
    printf("%d * 3 = %d", i.get(), i.get() * 3);
    i.add(1);
  }
}

Что изменилось? Не так уж и много. Вместо обычной “скалярной” переменной i у нас есть “объект”, который хранит целочисленное значение внутри и предоставляет несколько методов для доступа и изменения этого значения. Помогло ли это сократить объем кода? Совсем нет. Более того, длина print() теперь еще на несколько строк длиннее. Но теперь у нас есть объект и мы можем вызывать наш код в объектно-ориентированном стиле!

Вот как основная часть “объектов” используется в настоящее время, в основном благодаря C++: они просто хранители данных, в то время как настоящие пользователи данных все еще находятся вне их. Объект Idx ничего не знает о настоящей цели данных, которые он содержит. Он не знает, что его v используется в качестве счетчика шагов и что его умножают на что-то перед выводом текста. Idx - это хранитель данных, а настоящая логика находится вне его.

Проблема поддерживаемости не решена, область видимости не уменьшена, сложность кода не уменьшена. Более того, она увеличилась, потому что теперь, чтобы понять, как работает print(), мне нужно знать, что находится внутри Idx. Объектная парадигма в этом конкретном примере обещала взять на себя часть проблемы и никогда не беспокоить меня об этом, но на самом деле она только увеличила проблему, представив мне две проблемы: print() и Idx.

Почему это благодаря C++? Потому что C++ добавил объектно-ориентированность поверх процедурного программирования на C, даже не задумываясь о запрете некоторых из них, чтобы заставить программистов писать объекты так, как они должны быть написаны: как черные ящики, которые инкапсулируют все, что им нужно, и никогда не позволяют никому снаружи даже знать, что находится внутри! C++ даже не пытался переключить парадигму от процедур и переменных к объектам и методам. Бьярне Страуструп, создатель C++, просто дал программистам методы и классы и сказал: “Используйте их, они удобнее переменных, … или, может быть, нет, иногда, … Я не знаю” (Я не уверен, что это его цитата, но я верю, что она очень близка к тому, что он имел в виду). Прочтите его книгу, и вы увидите, сколько страниц посвящено философии объектно-ориентированного программирования и сколько - техническим деталям операторов и выражений.

Правильное объектно-ориентированное решение будет выглядеть иначе и включать настоящую инкапсуляцию, где данные никогда не “выходят” за границы своего владельца. Во-первых, вот как я бы разработал Idx… ну, сначала я бы переименовал его и назвал бы его Line:

И вот здесь находится код print():

Как видите, print() не имеет доступа к внутренним данным Line. Все, что может сделать print(), это попросить Line продвинуться вперед и напечатать себя. Как именно эта логика реализована внутри Line - никто не знает и никто не хочет знать. Поскольку у нас нет никаких геттеров в Line, мы не можем извлечь данные из него.

Поскольку мы не можем получить данные, мы не можем построить никакую логику в print(). У нас просто нет ничего, с чем можно работать, никаких данных, никаких целых чисел, никаких чисел. Мы можем работать только с объектами, которые не доверяют нам своим внутренностям. Мы можем только вежливо попросить их что-то сделать для нас. Область видимости print() теперь довольно мала и очень хорошо изолирована от внутренних данных Line. Надлежащая инкапсуляция помогла нам достичь этого: не разглашая внутренних данных Line, мы сделали невозможным для кого-либо приглашать себя в ее собственную область видимости. print() просто не может делать ничего с данными, инкапсулированными Line.

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

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

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-17 at 15:25

sixnines availability badge   GitHub stars