image Привет, Хаброжители!

Большинство разработчиков ПО тратят тысячи часов на создание излишне сложного кода. Девять основных принципов книги «Искусство чистого кода» научат вас писать понятный и удобный в сопровождении код без ущерба для функциональности. Главный принцип — это простота: сокращайте, упрощайте и перенаправляйте освободившуюся энергию на самые важные задачи, чтобы сэкономить бесчисленное количество часов и облегчить зачастую очень утомительную задачу поддержки кода. Автор бестселлеров Кристиан Майер помог тысячам людей усовершенствовать навыки программирования и в своей новой книге делится опытом с читателями.

Для кого эта книга?
Вы — действующий программист, мечтающий создать более значимый продукт с быстрым кодом и меньшей головной болью?
Вы когда-нибудь зацикливались на поиске багов?
Сложность кода частенько приводит вас в замешательство?
Трудно ли вам решить, что изучать дальше, выбирая из сотен языков программирования: Python, Java, C++, HTML, CSS, JavaScript — и тысяч фреймворков и технологий: приложения Android, фреймворк Bootstrap, библиотеки TensorFlow, NumPy?
Если ваш ответ на любой из этих вопросов «ДА!» (или просто «да»), то вы держите в руках нужную книгу!
Она предназначена для всех программистов, заинтересованных в повышении своей продуктивности — делать больше с меньшими затратами. Она для вас, если вы стремитесь к простоте и свято верите в принцип бритвы Оккама: «Не следует множить сущности без крайней необходимости».


Шесть типов преждевременной оптимизации


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

Однако не стоит верить мне на слово. Вот что говорит о преждевременной оптимизации один из самых влиятельных ученых-компьютерщиков всех времен Дональд Кнут (Donald Knuth):
Программисты тратят огромное количество времени, размышляя или беспокоясь о скорости работы некритичных частей своих программ, а на самом деле такие попытки повысить эффективность приводят к негативному результату с учетом дальнейшей отладки и поддержки кода. Нас в 97 % случаев не должна сильно заботить низкая эффективность: преждевременная оптимизация — корень всех зол.

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

Оптимизация функций кода


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

Оптимизация функциональности


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

Оптимизация планирования


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

Оптимизация масштабируемости


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

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

Оптимизация разработки тестов


Преждевременная оптимизация тестов — это один из основных факторов, приводящих к напрасной трате времени разработчика. У процесса разработки, ориентированной на тестирование, есть много ревностных последователей, которые неправильно понимают идею проведения тестов до реализации функциональности как необходимость всегда первым делом писать тесты — даже если функция кода экспериментальна или изначально не вполне пригодна для тестирования. Написание экспериментального кода — это проверка концепций и идей, а добавление к нему лишнего уровня тестирования может затормозить продвижение проекта, а кроме того, не укладывается в философию быстрого прототипирования. Предположим, вы строго следуете принципам разработки через тестирование и настаиваете на 100-процентном покрытии тестами. При этом некоторые функции кода, например те, которые обрабатывают произвольный пользовательский текст, не слишком хорошо проходят юнит-тесты из-за непредсказуемости входных данных. Эффективно такие функции может протестировать только человек — в этом случае реальные пользователи и являются единственным значимым тестом. Тем не менее вы все равно проводите преждевременную оптимизацию для идеального прохождения юнит-тестов. Такой подход не имеет смысла: он замедляет цикл разработки, внося при этом ненужную сложность.

Оптимизация объектно-ориентированной картины мира


Объектно-ориентированные подходы зачастую несут с собой излишнюю сложность и преждевременную «концептуальную» оптимизацию. Предположим, вы хотите смоделировать мир своего приложения с помощью сложной иерархии классов. Вы пишете небольшую игру об автомобильных гонках. Вы создаете иерархию классов, в которой класс Porsche наследует от класса Car, который в свою очередь наследует от класса Vehicle. Ведь каждый Porsche — это автомобиль, а каждый автомобиль — это транспортное средство. Однако многоуровневая иерархия классов приводит к усложнению кодовой базы, и другим программистам будет трудно разобраться, что делает ваш код. Нередко такие типы многоуровневых структур наследования привносят излишнюю сложность. Избегайте их, используя идею MVP: начинайте с самой простой модели и расширяйте ее только при необходимости. Не оптимизируйте свой код под моделирование мира с большим количеством деталей, чем вашему приложению нужно на самом деле.

Преждевременная оптимизация: пример


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

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

Листинг 5.1. Простой скрипт для отслеживания транзакций и балансов

   transactions = []
   balances = {}

❶ def transfer(sender, receiver, amount):
      transactions.append((sender, receiver, amount))
      if not sender in balances:
         balances[sender] = 0
      if not receiver in balances:
         balances[receiver] = 0
      ❷ balances[sender] -= amount
         balances[receiver] += amount

  def get_balance(user):
      return balances[user]
  def max_transaction():
      return max(transactions, key=lambda x:x[2])

❸ transfer('Alice', 'Bob', 2000)
❹ transfer('Bob', 'Carl', 4000)
❺ transfer('Alice', 'Carl', 2000)

  print('Balance Alice: ' + str(get_balance('Alice')))
  print('Balance Bob: ' + str(get_balance('Bob')))
  print('Balance Carl: ' + str(get_balance('Carl')))

  print('Max Transaction: ' + str(max_transaction()))

❻ transfer('Alice', 'Bob', 1000)
❼ transfer('Carl', 'Alice', 8000)

  print('Balance Alice: ' + str(get_balance('Alice')))
  print('Balance Bob: ' + str(get_balance('Bob')))
  print('Balance Carl: ' + str(get_balance('Carl')))

  print('Max Transaction: ' + str(max_transaction()))

В скрипте есть две глобальные переменные — transactions и balances. Список transactions отслеживает транзакции между участниками в течение игрового вечера по мере их совершения. Каждая транзакция представляет собой кортеж из идентификаторов sender и receiver, а также величины amount, которая должна быть переведена от отправителя получателю ❶. Словарь balances отслеживает текущий баланс игрока, то есть сопоставляет идентификатор пользователя с количеством имеющихся у него средств на основе транзакций на данный момент ❷.

Функция transfer(sender, receiver, amount) создает и сохраняет новую транзакцию в глобальном списке, а также создает новые балансы для sender и receiver, если их еще нет, и обновляет балансы в соответствии с заданным значением amount. Функция get_balance(user) возвращает баланс пользователя, указанного в качестве аргумента, а max_transaction() перебирает все транзакции и возвращает ту, которая имеет максимальное значение в третьем элементе кортежа — сумме транзакции.

Изначально все балансы равны нулю. Приложение передает 2000 единиц от Алисы Бобу ❸, 4000 — от Боба Карлу ❹ и 2000 — от Алисы Карлу ❺. В этот момент Алиса должна 4000 (с отрицательным балансом –4000), Боб должен 2000, а Карл имеет 6000 единиц. После вывода максимальной транзакции Алиса переводит 1000 единиц Бобу ❻, а Карл переводит 8000 Алисе ❼. Теперь счета изменились: Алиса имеет 3000, Боб –1000, а Карл –2000 единиц. При этом приложение возвращает следующий результат:

Balance Alice: -4000
Balance Bob: -2000
Balance Carl: 6000
Max Transaction: ('Bob', 'Carl', 4000)
Balance Alice: 3000
Balance Bob: -1000
Balance Carl: -2000
Max Transaction: ('Carl', 'Alice', 8000)

Но Алиса недовольна этим приложением. Она понимает, что вызов функции max_transaction() приводит к повторяющимся вычислениям — поскольку функция вызывается дважды, скрипт два раза пролистывает список transactions, чтобы найти транзакцию с максимальной суммой.

Но при выполнении max_transaction() во второй раз она частично повторяет те же действия, перебирая все транзакции для нахождения максимума (включая те, для которых он уже известен, то есть первые три транзакции ❸–❺). Алиса справедливо считает, что здесь есть перспектива для оптимизации — можно ввести новую переменную max_transaction, которая отслеживает максимальную транзакцию на данный момент всякий раз при создании новой транзакции.

В листинге 5.2 показаны три строки кода, добавленные Алисой для реализации этого изменения.

Листинг 5.2. Реализованная оптимизация для сокращения повторяющихся вычислений

transactions = []
balances = {}
max_transaction = ('X', 'Y', float('-Inf'))

def transfer(sender, receiver, amount):
     ...
          if amount > max_transaction[2]:
               max_transaction = (sender, receiver, amount)

Переменная max_transaction хранит максимальную сумму транзакции среди всех транзакций, наблюдавшихся на данный момент. Таким образом, нет необходимости пересчитывать максимум после каждой игровой ночи. Первоначально вы устанавливаете в качестве максимального значения отрицательную бесконечную величину, чтобы первая реальная транзакция непременно была больше. Каждый раз при добавлении новой транзакции программа сравнивает ее значение с текущим максимумом, и если оно больше, то она сама становится транзакцией с максимальным значением. Без оптимизации при вызове функции max_transaction() 1000 раз для списка из 1000 транзакций вам пришлось бы выполнить 1 000 000 сравнений, чтобы найти 1000 максимумов, поскольку перебирался бы список из 1000 элементов 1000 раз (1000 * 1000 = 1 000 000). После оптимизации вам нужно извлекать текущее сохраненное в max_transaction значение только один раз при каждом вызове функции. Список состоит из 1000 элементов, и, чтобы определить текущий максимум, потребуется не более 1000 операций. Это приводит к сокращению количества необходимых операций на три порядка.

Многие разработчики кода не могут устоять перед реализацией таких оптимизаций, но при этом сложность накапливается. Например, Алисе вскоре придется фиксировать ряд дополнительных переменных для отслеживания новой статистики, которая может заинтересовать ее друзей, например min_transaction, avg_transaction, median_transaction и alice_max_transaction (для учета ее собственного максимального значения транзакции). Каждая из них повлечет за собой появление в проекте нескольких дополнительных строк кода, увеличивая вероятность появления бага. Например, если Алиса забудет обновить переменную в нужном месте, ей придется потратить драгоценное время на исправление. Хуже того, она может пропустить этот баг, в результате чего данные о балансе в аккаунте Алисы будут испорчены, а ущерб составит несколько сотен долларов. Ее друзья могут даже заподозрить, что Алиса специально написала код в свою пользу! Последнее замечание может показаться несколько ироничным, но в реальных условиях ставки могут быть высоки. Последствия второго порядка от излишней сложности могут быть даже серьезнее, чем более предсказуемые последствия первого порядка.

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

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

Об авторе
Кристиан Майер (Christian Mayer) — основатель популярного сайта Finxter.com, посвященного языку Python. Благодаря этой образовательной платформе более пяти миллионов человек в год обучаются программированию. Кристиан имеет степень PhD в области computer science, он опубликовал ряд книг, включая «Python One-Liners» (No Starch Press, 2020), «Leaving the Rat Race with Python» («Выйти из бешеной гонки с помощью Python») (2021) и серию «Coffee Break Python» («Перерыв на кофе с Python»).
О научном редакторе
Ноа Спан (Noah Spahn) имеет богатый опыт в области разработки ПО. Он получил степень магистра в области программной инженерии в Университете штата Калифорния в Фуллертоне. Сегодня Ноа работает в группе компьютерной безопасности Калифорнийского университета в Санта-Барбаре (University of California, Santa Barbara; UCSB), где ранее преподавал Python в группе междисциплинарного сотрудничества. Также он читал лекции по концепциям языков программирования для студентов старших курсов Вестмонтского колледжа. Ноа всегда рад помочь тем, кто заинтересован в обучении.

Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Код

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


  1. apevzner
    10.08.2023 18:48
    +2

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

    При всем при том, "хорошенько подумав" - дело тоже само по себе затратное (или, иными словами, программа, в которой тщательно избегали преждевременной оптимизации, сама по себе оптимизирована по этому критерию), и избегание этого тоже попадает под действие идей из этой статьи :)


  1. moonster
    10.08.2023 18:48

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


  1. lolikandr
    10.08.2023 18:48

    Преждевременная - значит та, которая выполняется до измерений.
    Измерил, определил те самые 3% кода, которые отнимают больше всего времени - оптимизируй.
    Оптимизировал - измеряй заново. И так - по кругу. Главное при этом, конечно, уметь правильно измерять.
    В остальном - код должен быть понятен, ибо бОльшую часть времени программист его читает, а не правит.
    А культуру "оптимизировать не надо, и вообще некогда" надо изгонять.