Pull to refresh

10 лет практики. Часть 1: построение программы

Reading time 6 min
Views 21K
Десять лет я пишу на С++. Первые пять лет моей задачей было писать эффективный код для компьютерных игр, потом основным требованием была стабильность, так как речь шла об автоматизации промышленных объектов. Я бы хотел поделиться своим личным опытом и практическими наработками, которые помогут создавать эффективные и в то же время стабильно работающие программы.
image

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

Итак, коллеги, мы пишем на С++. Как правило, это значит что наши программы сложны. Некоторые — очень сложны. Чтобы программа работала как нужно мне, её создателю, требуется держать в уме то как она работает, вплоть до мельчайших деталей. Это необходимое условие, так как всё что не удалось полностью продумать — это гарантированные баги.

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

Если я пишу программу, которая использует неотработанную технологию или элемент, я сначала делаю этюд. Небольшую консольную программку, которая содержит в себе нужный кусок. Этот этюд я мучаю до посинения, пока чётко не пойму как и чего работает, какие ограничения на вводные данные, какие особенности и подводные камни. Да, у меня целая директория из нескольких сотен этюдов. Каждый в конечном итоге доводится до независимого куска кода (чаще всего, класса или связки классов), который я вставляю в программу как батарейку: вытащил старую версию — воткнул новую.

«Склейка» элементов иногда представляет собой довольно сложный механизм сама по себе, а потому требует отдельного этюда. Но этого мало, когда речь идёт о сложных механизмах. Все сложные алгоритмы я записываю на бумагу. Невозможно написать на С++ то, что не можешь описать простым русским языком. Когда описываешь что-то словами, мозг натыкается на подводные камни и нестыковки. Они становятся видны в самом начале, а не в конце, когда код уже написан. Сам текст должен быть отшлифован, тонкие места — упомянуты. Ещё плюс: можно вернуться к проекту через год и не тратить недели на вспоминание почему это сделано именно так.
Для каждого заказчика я завёл по тетради. В них — все нетривиальные моменты, от создания реалистичной пены вокруг парусного фрегата, до вычисления оптимальных траекторий подъёмных кранов на узком пятачке вокруг раскалённых металлов и кислот. Если мне встречается похожая задача, я просто заглядываю в тетрадь. Там, среди перечёркнутых текстов и схемок, за 10-15 минут я воспроизвожу в памяти весь ход создания алгоритма со всеми его особенностями. Как правило, задачи не повторяются полностью, но какая-то идея оказывается нужной через три-четыре года.

Очевидно? Отлично! Чуть усложним и заглянем внутрь элементов, о которых я говорил выше…

С++ очень прост в том смысле, что классы С++ зачастую повторяют описание объектов реального мира (не в смысле прямого копирования, а в смысле названий, частично — функциональности и взаимоотношений, кроме того, в С++ бывают синтетические вспомогательные классы). Когда объект реального мира является или содержит другой объект реального мира (только один), в С++ применяется наследование классов. Если объект-потомок может содержать нескольких однотипных — применяем включение объектов в класс потомка. Однотипное поведение классов выделяем в отдельный класс (который я буду называть классом подмешивания). Всё это отлично расписано у Алена Голуба [2], так что не буду повторять мастерски написанный текст. Скажу только что это простое мнемоническое правило следует запомнить и применять на автомате.

Гораздо менее канонический вопрос в распределении прав доступа к членам и функциям-членам класса. Все конечно знают про инкапсуляцию, все читали книжки. Что со всем этим делать на практике?

На практике происходит следующее. Я делаю класс Бункер, который описывает реальный бункер, к которому подключены датчики. Бункеры объединяются в цеха, всё это выводится в красивый интерфейс, как-то взаимодействует между собой и с человеком.
Спрашивается, сколько внутренней информации о Бункере я открыл другим классам? Отвечаю: вообще ничего. Понимаете? Все члены класса — приватные. Никто извне не должен знать что у класса внутри. Причина ясна: поменял что-то в классе — и цепочка изменений потянулась по всему коду, который за секунду до этого был чист и отлажен как швейцарские часы.
Итак, Бункер не выдаёт наружу подробности своей реализации. При этом, он сам выводит себя в интерфейс, сам обрабатывает нажатия на свои контролы, сам выполняет всю работу, которая требует знания о том, как он устроен. Наружу открываются функции-члены, которые просят класс выполнить ту или иную работу: выведи себя в интерфейс, обработай нажатие мышки, сохрани себя в XML и т.д.

Вы наверное спросите: может не стоило так усложнять, почему бы не сделать классы более открытыми и дружелюбными? «Ни в коем случае» — отвечу я,- и вот почему.
Проходит два месяца работы программы, и заказчик просит меня сделать удалённое управление бункером. Потом удалённое управление требуется по разным каналам связи — от Интернет до сотовой сети и последовательного порта, причём каналы связи должны дублироваться. Время идёт, программа работает, и постепенно выясняется необходимость в удалённой синхронизации журналов действий операторов. А ещё через пару месяцев я выясняю что необходимо делать инкрементальную синхронизацию состояния бункеров и журнала при плохой связи (например, зашумлённый RS-485 или Wi-Fi на больших расстояниях).

Что я сделал? Я усложнил класс Бункер, который теперь работает либо локально, либо удалённо. Добавил ещё кое-какой код, который подхватывает настройки GSM/Ethernet/RS-232. И главное: всё остальное осталось как было. Все месяцы работы по тонкой настройке алгоритмов управления остались со мной! То же самое относилось и к журналу, и ко всему остальному.
Если бы я вынес наружу хотя бы что-то из внутренней кухни Бункера, я даже боюсь представить сколько всего мне пришлось бы переписывать из 250 кБ управляющего кода.

Ещё раз повторю важный момент: структура программы на С++ отличается от С с точностью до наоборот. Там у нас были структуры и общие переменные. Здесь у нас объекты, которые суть чёрные ящики. Каждый объект — сам себе жнец, швец, на дуде игрец.
С другой стороны, сам код класса должен быть простым. Если требуется сложный функционал, я реализую его включением в класс внутренних объектов-помощников, либо использую классы подмешивания (но более осторожно, поскольку наследование является более сильной формой сцепления, чем включение).

Напоминаю, что get/set доступ — это чуть более завуалированный вариант открытого члена класса. С этим подходом нам не по пути.

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

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

Хочу успокоить: всё это совершенно нормально. Программой пользуются — значит программа живёт, а значит меняется и развивается её структура. Прежде чем программа станет действительно хорошей, её, почти наверняка, придётся несколько раз переписать (мои самые серьёзные программы, как правило, переписываются дважды или трижды). Такая программа обладает отличным потенциалом роста, она больше не страдает «детскими болезнями» и, как правило, работает как те самые швейцарские часы. А все наработки из прошлой версии переносятся тем проще, чем более изолированными были изначальные классы.

Литература:
  1. Бьёрн Страуструп. Язык программирования С++.
  2. Ален И. Голуб «Верёвка достаточной длины чтобы… выстрелить себе в ногу».

В следующей части: прощаюсь с проблемами new/delete, избавляюсь от необходимости в умных указателях и мусоросборщиках, заменяя их на более простую и естественную методику работы с ресурсами.
Tags:
Hubs:
+98
Comments 195
Comments Comments 195

Articles