Статьи

Как видимость данных наносит вред поддержанию

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

Видимость данных

Я перечитал первые несколько страниц « Элегантных объектов», том 1 , моей книги, полностью посвященной проблеме недостатков современного объектно-ориентированного программирования, и обнаружил, что в нем непосредственно упоминается удобство обслуживания: «Основная цель, которую я пытаюсь достичь с этим написанием необходимо повысить удобство сопровождения вашего кода », а затем оно также объясняет, что удобство сопровождения — это« время, необходимое мне для понимания вашего кода ». С этим не поспоришь , но остается вопрос: как отсутствие «истинной» объектной ориентации и правильной инкапсуляции ухудшает читабельность?

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

1
2
3
4
5
6
7
8
void print() {
  for (int i = 0; i < 10; ++i) {
    printf("%d * 2 = %d", i, i * 2);
  }
  for (int i = 0; i < 10; ++i) {
    printf("%d * 3 = %d", i, i * 3);
  }
}

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

01
02
03
04
05
06
07
08
09
10
void print() {
  int i = 0;
  while (++i < 10) {
    printf("%d * 2 = %d", i, i * 2);
  }
  i -= 10;
  while (++i < 10) {
    printf("%d * 3 = %d", i, i * 3);
  }
}

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

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

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

1
2
3
4
5
6
int i = 0;
void print() {
  while (++i < 10) {
    printf("%d * 2 = %d", i, i * 2);
  }
}

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

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

Они были.

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

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

1
2
3
4
5
6
7
class Idx {
public:
  int get() { return v; }
  void add(int a) { v += a; }
private:
  int v = 0;
}

А потом:

01
02
03
04
05
06
07
08
09
10
11
12
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 :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
class Line {
public:
  Line(int m): mul(m) {};
  void print() {
    printf("%d * %d = %d", v, m, v * m);
  }
  bool next() {
    bool n = true;
    if (v < 10) {
      ++v;
    } else {
      n = false;
    }
    return n;
  }
private:
  int mul;
  int v = 0;
}

А теперь вот код print() :

1
2
3
4
5
6
void print() {
  Line a(2);
  while (a.next()) { a.print(); }
  Line b(3);
  while (b.next()) { b.print(); }
}

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

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

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

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

Опубликовано на Java Code Geeks с разрешения Егора Бугаенко, партнера нашей программы JCG . См. Оригинальную статью здесь: как видимость данных мешает возможности обслуживания

Мнения, высказанные участниками Java Code Geeks, являются их собственными.