Часть 1. Глава 3. Недостатки компонентного подхода

Сергей Васильевич Виноградов
Когда я учился программировать я ни разу ни в одной книге не встречал ни одного плохого слова про компонентный подход.
Однако, как оказалось, и этот приём имеет свои минусы. Да ещё какие существенные.

Как говорят, преимущества компонентного подхода заключаются в том, что:
1. Вы используете код повторно, поэтому пишете меньше кода
2. Если вы нашли дефект в компоненте - вы исправляете его только в одном месте, а не во многих похожих местах
3. Компоненты проще использовать, так как они составляют единое абстрактное логическое целое (см. главу 1 про введение терминов: компонент - это термин, упрощающий работу, им можно думать)

Посмотрим, как эти преимущества первращаются в недостатки.
Начнём со второго пункта: "Если вы нашли дефект в компоненте, вы исправляете его только в одном месте, а не во многих похожих местах".

Если нам нужно изменить компонент, то мы изменяем его сразу для всех его вхождений (мест, где мы его хотим использовать).
Как известно, при изменении часто вносятся ошибки. По моему опыту они вносятся где-то в 2-3 раза чаще, чем при написании нового кода (по статистике на количество написанных или изменённых строк кода). При этом, даже несущественное изменение может повлечь за собой проблемы (дефекты).
Что это значит?
Это значит, что мы часто вносим дефекты, если изменяем компонент. И этот дефект будет внесён во все вхождения этого компонента.
То есть достоинство компонентного подхода
"Если вы нашли дефект в компоненте, вы исправляете его только в одном месте, а не во многих похожих местах"
превращается в недостаток
"Если вы внесли дефект в компонент, вы внесли его сразу во все места, где он используется".

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

То есть риски повышаются. И когда вы пишите новый продукт (новую функцию внутри продукта) с тем же компонентом, вы затрагиваете не только новый продукт, но и все старые продукты с этим компонентом.

Мало того, компонент сложнее изменять. Ведь нужно помнить о том, как они используется во всех местах, чтобы нигде не навредить. Если работа компонента сильно отличается в зависимости от места использования, это может быть очень сложным.
Если хотите, просто попробуйте запрограммировать цикл метода Рунге-Кутты, который в зависимости от параметра функции, интегрирует как слева направо (как обычно), так и справа налево (то есть движется от положительных чисел к отрицательным: с отрицательным шагом). Кажется, что это несущественное усложнение, но если в функции (компоненте) присутствует ещё и адаптивный выбор шага, то усложнение чувствительное.
Да, строк кода почти не прибавится. Зато напряг для мозгов программиста прибавится очень и очень сильно. И время внесения измений, возможно, будет даже больше, чем время написания ещё одной функции специально для интегрирования справа налево. Скопировав уже существующую реализацию вы просто измените лишь несколько параметров и перечитаете код, затем протестируете его, возможно, почти теми же тестами. Думать будет почти не надо.
При том, что написание новой функции ещё и менее рискованное: кто знает, какой дефект вы поставите и чем это аукнется в тех местах, где интегрирование шло слева на право?

Теперь перейдём к первому достоинству. "Вы используете код повторно, поэтому пишите меньше кода".
Здесь тоже есть проблема. Дело в том, что иногда компоненты должны действовать очень по-разному в разных местах кода. Поэтому, они учитывают кучу вариантов и получаются очень сложными и большими. Например, я видел участок кода, повторяющийся в разных местах раз 50 почти без изменений, как казалось. При превращении его в компонент его объём вырос в 150 раз (по сравнению с одним участком кода). То есть количество общего кода выросло более чем в 3 раза по сравнению с кодом, где участок кода был просто скопирован.
Почему?
Потому что эти участки кода были хоть и очень похожи, но различны. И эти различия было не так-то просто запрограммировать. И это при том, что к компоненту прилагалась ещё и документация объёмом где-то в 100 раз превышающая объём изначального одного участка кода. При том, что изначальные участки были настолько простые, что к ним документации не прилагалось. Компонент же был настолько сложным, что не только его изменение, но даже использование вызывало трудности у тех же самых программистов, которые легко справлялись с изменением изначальных участков кода.

При этом, в компоненте присутствовало порядка 15-ти настроек. И хотя часто компонент можно было использовать вообще без параметров, программисты путались в этих настройках, не зная, нужны они сейчас или нет.
Им пришлось изучать новые термины (настройки и их влияние на поведение компонента) и держать их в своей голове. То есть вместо упрощения работы получилось её усложнение.

Теперь представьте себе следующее. Ранее мы тестировали продукт, где в каждом конкретном участке поведение всегда было совершенно определённым.
Теперь у нас есть целый компонент, где его поведение зависит сразу от 15-ти настроек. Попробуйте протестировать такой компонент модульными тестами, и вы увидите, что покрыть все варианты настроек и убедится, что они правильные, очень сложно или невозможно. Так как количество вариантов его поведения, даже если считать настройки булевыми, составляет 32768 вариантов. Если вы точно знаете, как проверить каждый вариант на правильность автоматически, то вы в выигрыше. Однако, если автоматизированный тест не может гарантировать правильность результата, либо проверка идёт вручную - вы проиграли. Но даже если всё автоматизированно, представьте себе, что тестируемый алгоритм в каждом варианте работает 1 секунду. Автоматизированные тесты компонента займут более 9-ти часов на одном ядре. Либо прийдётся трудиться и писать более сложные тесты, работающие в параллельном режиме. Опять стоимость, сложность, сроки и объём кода.
Конечно, можно сказать, что давайте разделим варианты на классы эквивалентности (вау, какие слова вы знаете! плюс к квалификации, а значит к зарплате, а значит - к стоимости программного продукта) и протестируем только их. Но если вы выделите их неверно, то тестирование не выявит проблемы. То есть риски внесения незамеченной повышаются (заметьте, это всё в одном и том же компоненте). Хорошо, если ничего страшного от ошибки не будет, а если продажи остановятся на несколько часов в магазинах по всей стране или в самолёте откажет электроника?
Интеграционные тесты, при этом, в том случае, который я описываю, сделать автоматическими было слишком сложно.
А ведь раньше можно было просто проверить вручную один раз, что всё работает верно. И забыть про данный код, потому что он не изменялся. Изменения появились лишь тогда, когда мы стали использовать компонентный подход.


Итак, мы увидели недостатки компонентного подхода:
1. Внося один раз ошибку, мы её получаем в совершенно различных участках кода, которые вообще не собирались менять.
2. Компонент может оказаться сложнее, чем обычные участки кода. То есть его написание доставляет больше усталости и требует более высокой квалификации.
3. Компонент нуждается в регрессионном тестировани, причём всех участков кода, которые его используют.
4. У компонента так много вариантов поведения, что его очень сложно протестировать модульными тестами.
5. Изменение компонента также требует более высокой квалификации.
6. Даже использование компонента требует более высокой квалификации и напряжения.
7. Количество кода тоже может сильно увеличиться одновременно с его сложностью.
8. Повышаются риски того, что при тестировании сложного компонента вы не найдёте ошибок.

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

Помните, что когда я говорю о повышенной квалификации программиста, я говорю о том, что для фирмы это будет:
1. Необходимость научиться проверять эту квалификацию на собеседовании
2. Необходимость платить более высокую зарплату
3. Меньшее количество желающих на одно место, так что может возникнуть дефицит кадров даже при высокой зарплате. Кадры могут быть более капризными.

То есть это дополнительные издержки, да ещё и такие, которые довольно трудно организовать. Надо не только потратить деньги, надо ещё и суметь их потратить правильно.


Если вы профессиональный программист, то вы должны, иногда даже сами не замечая этого, выделять повторяющиеся участки кода в отдельные компоненты (функции). Компонентный подход - это очень важно и полезно.
Но, как видите, не всегда.

И да, для особо "квалифицированных". Наследованием проблема тоже не решается. Если есть 15-ть разных вариантов поведения объекта, вы можете отнаследовать родительский класс 15-ть раз. Но если вам нужно, чтобы поведение объекта менялось в зависимости от 15-ти разных переменных. То есть, опять же, 32768 вариантов поведения объекта. Как вы их отнаследуете?


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

Иногда, проблемой является даже попытка написать сложный компонент сразу. Зачем писать сразу сложный компонент для многих задач, которых ещё нет, если можно написать простой для одной задачи? Вы пишете простой компонент для какой-то конкретной задачи и даже не напрягаетесь. Вы тратите меньше времени, вы тратите меньше денег (ваша фирма), вы меньше напрягаетесь, вы сделаете меньшее количество дефектов в этом компоненте (так как пишите проще и написали меньше кода), это будет проще отлаживат. Вы выбросите продукт на рынок раньше, чем конкурент, и продукт будет, возможно, даже более высокого качества. Конечно, всё это верно, если сложный компонент в продукте вам так и не понадобится.
Кроме этого, иногда сложный компонент проще написать из простого, чем писать с нуля. Но не всегда.

Здесь дело даже не в компонентном подходе. А в психологической ловуше. Программист так сильно привык писать компоненты, что вместо решения конкретной задачи может поставить перед собой задачу написания универсального компонента, который сейчас не нужен. И за счёт этого получить лишние дефекты, трудности и т.п. Это особенно касается тех сфер, где вы сами решаете, что писать, а что не писать.