Происхождение сложности

Писать программное обеспечение сложно.

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

В ответ на это список советов о том, как лучше всего разрабатывать программное обеспечение, с годами постоянно расширялся. Иногда советы оказывались весьма полезными, а другие - противоречивыми.

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

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

Давайте перепишем список и добавим почему, чтобы получить подсказку:

  • Не повторяйтесь - это улучшает координацию.
  • Сделайте его маленьким - это снижает координацию.
  • Избегайте изменяемого состояния -, потому что оно улучшает координацию.
  • Используйте чистые функции, потому что они ухудшают координацию.
  • Используйте интерфейсы - потому что они ухудшают координацию.
  • Используйте полиморфизм - потому что он снижает координацию.
  • Предпочитайте композицию, потому что она снижает координацию .
  • Используйте декларативный стиль, потому что он снижает координацию .
  • Избегайте временной привязки, поскольку она способствует координации
  • Работайте в небольших группах, потому что это снижает координацию.
  • Делайте небольшие коммиты, потому что это снижает координацию.
  • Избегайте долгоживущих ветвей, потому что они улучшают координацию.
  • Протестируйте свой код, чтобы выявить некорректную координацию!

Как видите, все дело в координации!

Теперь вы можете подумать, а что не так с координацией?

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

Здесь мы вставили заголовок в простом текстовом редакторе:

Проблема в том, что если мы изменим заголовок,

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

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

Решение состоит в том, чтобы переключиться на более продвинутый текстовый редактор, например:

Если мы теперь изменим текст на:

подчеркивание будет автоматически обновлено редактором, что решит нашу проблему координации с помощью автоматизации на компьютере.

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

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

Изменчивость

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

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

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

Вот что нам нужно сделать, чтобы добиться изменяемости:

  • Повысьте удобство использования
  • Снижают координацию

Юзабилити

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

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

Вот что Википедия говорит об удобстве использования:

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

Основные способы достижения удобства использования в программном обеспечении:

  1. Упростите поиск, повторное использование и рассуждение.
  2. Используйте инструменты и методы, которые позволят вам работать быстро и обеспечат быструю обратную связь.
  3. Организуйте людей так, чтобы облегчить общение, обучение и мастерство.

Удобство использования можно повысить разными способами. Вот три примера:

Пример 1. Дайте вещам хорошее имя.

Пример 2: Используйте REPL, чтобы повысить производительность за счет сокращения цикла обратной связи.

Пример 3: соглашайтесь на вездесущий язык при общении.

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

Координация

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

Причина согласования

Необходима координация по трем основным направлениям:

  • Размер - когда становится больше
  • Порядок - когда порядок имеет значение
  • Последовательность - когда последовательность имеет значение

Причина согласования - Размер

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

По сути, есть два способа минимизировать размер:

  • Разделите это на части - чтобы снизить координацию
  • Сделайте его компонуемым - чтобы сделать его меньше и проще в использовании

Разделить

Например, если мы удвоим количество людей в команде, увеличившись с трех до шести человек:

Количество способов общения увеличивается с трех до пятнадцати, то есть в 5 раз! Этот экспоненциальный рост координации и сложность - одна из самых сложных проблем, которые нужно решить в программном обеспечении . Это неудачное поведение является причиной того, что некоторые большие проекты терпят неудачу.

Способ бороться со сложностью - разбивать вещи на части:

Здесь у нас есть две команды по три человека, где один человек в каждой команде отвечает за общение с другой командой и своей собственной. Это уменьшило количество подключений с 15 до 7, что более чем на 50% снижает координацию!

Те же принципы применимы к коду и данным, поэтому лучше иметь маленькие функции, чем большие, и почему система Polylith, состоящая из небольших повторно используемых компонентов, предпочтительнее большого Monolith.

Сделайте его компонуемым

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

Чтобы достичь компонуемости, мы должны ввести соответствие в способ обработки вещей:

Функциональный способ - иметь данные в качестве материала и функции в качестве инструментов. Объектно-ориентированный способ - иметь данные или интерфейсы в качестве материала и объекты в качестве инструментов.

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

В Unix материал состоит из строк строк, а инструменты представлены набором команд, которые можно комбинировать вместе, как LEGO ©:

tr -cs A-Za-z ‘\n’ | 
tr A-Z a-z | 
sort | 
uniq -c | 
sort -rn | 
sed ${1}q

Этот пример взят из решения, которое Дуглас Макилрой написал, когда давал отзыв Дональду Кнуту. Вы можете узнать больше об истории вопроса здесь, а также найти список решений проблемы, написанный на разных языках.

Причина согласования - Заказ

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

Иногда важно, чтобы все происходило в определенном порядке. Код не исключение.

Здесь оператор 2 должен дождаться завершения оператора 1, что привело к необходимости координации между двумя операторами. В результате мы не можем поменять местами порядок или распараллелить инструкции.

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

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

Приведем несколько примеров, в которых порядок важен:

Пример 1. Мы планируем свою работу до того, как приступим к работе.

Пример 2: функция init должна быть вызвана перед продолжением другой работы (временное связывание).

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

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

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

Причина согласования - Последовательность

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

По сути, есть два способа синхронизировать данные:

  • Имейте только одну вещь
  • Двигайтесь маленькими шагами

Иметь только один

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

Каждый «строительный блок», например оператор if, определяется только один раз, но может использоваться более чем в одном месте кода:

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

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

Если позже нам понадобится изменить один из них, например чтобы исправить ошибку, нам также нужно не забыть обновить другую службу. Идея о том, что копирование и вставка может использоваться для «разделения» сервисов, часто является иллюзией, поскольку не устраняет необходимость координации.

Однако иногда вам действительно хочется начать с нуля. Например, когда база данных MySql была разделена на MariaDB.

Другой пример устранения координации путем централизации определения в одном месте - использование полиморфизма:

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

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

Двигайтесь маленькими шагами

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

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

Причина, по которой мы хотим двигаться маленькими шагами, например, делать небольшие коммиты в нашей системе VCS или иметь короткие спринты, заключается в том, что объем координации, который нам необходимо выполнять при обратном слиянии, увеличивается экспоненциально с размером Diff:

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

Где возникает координация

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

Как видим, координация идет на всех уровнях:

  1. Оборудование и сеть зависят от реальности, поэтому иногда они ломаются и нуждаются в ремонте или замене.
  2. Концепции из мира становятся моделями в нашем коде .
  3. На людей влияет мир и среда, в которой они живут и работают.
  4. Люди покидают команды / проекты, и появляются новые люди.
  5. Инструменты и службы используются для управления оборудованием и сетью. Иногда вручную людьми, которые используют инструменты, а иногда автоматически.
  6. Инструменты используются для выполнения или отладки исполняемого кода.
  7. Инструменты и службы состоят из исполняемого кода, поэтому их можно запускать.
  8. Оборудование необходимо для выполнения кода времени выполнения.
  9. Исходный код преобразуется в код времени выполнения.
  10. Инструменты используются для работы с исходным кодом.
  11. Инструменты используются другими инструментами и службами.
  12. Люди создают инструменты и услуги.
  13. Люди взаимодействуют с другими людьми.
  14. Модели влияют на то, как написан исходный код.
  15. Инструменты используются для создания моделей.
  16. Инструменты и сервисы используются людьми для выполнения задач: 5) управления оборудованием и сетью, 6) выполнения или отладки кода, 10) работы с исходный код, 15) создавать модели, 16) организовывать людей.

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

Мы управляем всеми этими изменениями следующими способами:

  1. Повысьте удобство использования
  2. Автоматизируйте и минимизируйте координацию

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

Вот некоторые примеры:

Пример 1: Оригинальная инструкция Intel x86 все еще поддерживается, спустя десятилетия после ее первого выпуска.

Пример 2: Исходный набор инструкций байт-кода для виртуальной машины Java по-прежнему поддерживается.

Пример 3: исходная спецификация протокола HTTP все еще поддерживается более чем четыре десятилетия спустя.

Решение состоит в том, чтобы согласовать контракт, превратить его в стандарт, а затем вносить только обратно совместимые изменения. Просто как тот!

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

Из ямы смолы

Прежде чем продолжить, необходимо упомянуть шедевр Бена Мозли и Питера Маркса Out of the Tar Pit. Причина проста. Было бы преступлением не включить это в сообщение в блоге о сложности программного обеспечения!

Если вы еще не читали его, я рекомендую вам это сделать. Хорошо проведенное время!

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

Они заявляют, что:

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

Они продолжают:

Мы считаем, что основной причиной этой сложности во многих системах является обработка состояния

Под состоянием они означают изменяемое состояние, особенно некоординированное изменяемое состояние. Я согласен, что это очень верно, особенно для языков, которые поощряют использование нескоординированного изменяемого состояния.

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

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

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

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

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

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

Их наблюдение таково:

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

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

Они очень хорошо описывают это на страницах 8 и 9:

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

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

Зависимости

Так зачем использовать слово координация, когда у нас уже есть зависимость?

Основная причина в том, что они означают разные вещи.

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

Например, зависимость от виртуальной машины (например, JVM) помогает нам избавиться от необходимости координации между скомпилированным байт-кодом и оборудованием. В зависимости от java.lang.String помогает нам безопасно работать со строками, не беспокоясь об их изменении в будущем!

Практическое правило:

Стабилизируйте то, от чего вы зависите

Связь

Итак, если мы не можем использовать зависимости, чтобы описать, насколько хорошо спроектирована система, может быть, мы можем использовать связывание?

Связь можно описать концепцией соучастия, но здесь мы воспользуемся определением Википедии:

В программной инженерии связь - это степень взаимозависимости между программными модулями; мера того, насколько тесно связаны две процедуры или модуля; сила отношений между модулями.

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

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

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

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

Компромиссы

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

Предположим, вам нужно выбрать между добавлением дубликатов в текстовый файл или использованием механизма шаблонов:

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

Внедрение шаблонизатора решит проблему координации дублированного в противном случае контента, но также добавит сложности и, возможно, снизит удобочитаемость и удобство поиска.

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

Однако чаще всего снижение координации также положительно сказывается на удобстве использования:

Это включает уменьшение размера, удаление дублирования, отделение что от как, сохранение единообразия и многое другое.

Резюме

Эта диаграмма суммирует то, о чем мы говорили:

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

Цель состоит в том, чтобы дать вам инструмент, который направит вас к более простым решениям с повышенным качеством таким образом, чтобы вы могли работать быстро, получать больше удовольствия и тратить меньше денег!

Так что не стесняйтесь использовать эту диаграмму в следующий раз, когда вам нужно будет принять дизайнерское решение!

Выбор языка программирования

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

Это так же неправильно, как утверждать, что не имеет значения, насколько опытны игроки в футбольной команде. Это может быть плохое сравнение, но языки программирования также нуждаются в «навыках», чтобы работать хорошо!

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

Когда мы выбираем язык, мы должны выбрать тот, который ставит отметки в как можно большем количестве полей в столбце Простота:

Если эта диаграмма кажется вам знакомой, то это потому, что она взята из блестящего выступления Simple Made Easy Rich Hickey.

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

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

Пусть начнется война против сложности.

Удачного кодирования!