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

1 Коротко и неясно


  1. ЧИТАЙ КОД!
  2. Пиши код, который удобно читать всем остальным
  3. Пиши в комментарии не ЧТО делает код, а ЗАЧЕМ
  4. Предупреждения и подсказки опаснее ошибок компиляции — проект собирается без их устранения

2 Цикл разработки


  1. Постановка задачи руководителем
  2. Выработка решения
  3. Рецензирование решения
  4. Реализация решения
  5. Рецензирование кода
  6. Размещение в системе контроля версий

3 Оформление исходного кода


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

4 Комментарии


  • Первая строка комментария перед объявлениями процедур, классов и т.п. должна давать понять их назначение. Последующие строки описывают те или иные особенности реализации.
  • Комментарии к виртуальным методам должны описывать обстоятельства вызова, а не реализацию — она может быть перекрыта в наследниках.
  • Комментарии в теле процедур и методов не должны описывать, ЧТО делает тот или иной оператор или блок, а должны указывать ЦЕЛЬ, для которой он (оператор) используется.
    1. Цели меняются гораздо реже, чем средства ее реализации
    2. Понять что делает код можно из него самого, понять ЗАЧЕМ по коду бывает нереально.


5 Выбор имен


  • Классы — существительное единственного числа с обязательным префиксом T и опциональными в виде названия фирмы (SMS), если существует библиотечный класс с похожим именем и проекта (ZVK, Plan), если класс не может быть задействован в нескольких наших проектах.
  • Интерфейсы — существительные с префиксом I
  • Процедуры — глагол.
  • Функции — существительное единственного числа при возврате значения и множественного — коллекции. Используется префикс Get для методов чтения свойств и функций, предназначенных для получения внешних данных, префикс Is для логических функций, префикс Create для создающих функций. Функции, которые вызываются для выполнения действия (а не только получения результата) называются как процедуры, так как де-факто ими и являются.
  • Свойства — существительное единственного числа, за исключением свойств-массивов. Для свойств-массивов с целочисленным индексом определяется свойство-спутник с суффиксом Count. Свойство-событие должно иметь префикс On, Before или After. Методы чтения и записи свойств имеют обязательные префиксы Get и Set.
  • Виртуальные методы — глагол с префиксом Do для protected и без префикса для public методов.
  • Название виртуального метода должно отражать цель его вызова, а не реализацию.

6 Операторы


  • Глубина вложения операторов друг в друга должна быть как можно меньшей — большая глубина очень сильно ухудшает читаемость кода.
  • Для уменьшения глубины вложения ветвлений полезно использовать операторы перехода (кроме goto) и raise
  • Для уменьшения глубины вложения конструкций try..finally полезно использовать присвоение очищаемым объектам nil перед try

7 Инкапсуляция


  • Не следует использовать без крайней необходимости имеющиеся глобальные переменные и никогда не стоит добавлять свои
  • Все поля классов — только private
  • Один класс должен выполнять одну задачу, а не совмещать в себе хранение, отображение, редактирование и запись в БД
  • Доступ к полям для потомков и извне класса может осуществляться через свойства
  • Все методы, реализующие интерфейс — обязательно protected и невиртуальные
  • Все, что не используется вне модуля — в implementation
  • Если использование класса тянет за собой большую часть проекта, следует написать для данного использования интерфейс, реализовать его в классе и пользоваться только интерфейсом

8 Обработка ошибок


  • Для обработки ошибок следует использовать только исключения
  • Исключения следует использовать только для обработки ошибок
  • В блоках except следует перевозбуждать (raise) все исключения, которые не обрабатываются явно
  • Категорически не следует «глотать» исключения (except end)
  • Категорически не следует возбуждать исключения базового класса Exception: если нет подходящего библиотечного — надо определить свой
  • Не следует обрабатывать исключения только для записи в лог — это делается автоматически.
  • Любой код должен быть написан с учетом того, что в любом его месте может возникнуть исключение.
  • Всегда возбуждайте исключение при ошибках в конструкторе и никогда не возбуждайте и не ловите исключений в деструкторе — деструктор должен корректно завершаться всегда.

Исчерпывающий источник по обработке ошибок в Delphi

9 Отладка


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

Настройки проектов Delphi с точки зрения поиска ошибок
Как читать лог-файлы
Access Violation в деталях
Почему всегда нужно использовать FreeAndNil вместо Free

10 Утечки ресурсов


  • Возможность утечки ОДНОГО любого ресурса, используемого в пределах подпрограммы, устраняется с помощью try..finally, при этом захват ресурса производится непосредственно перед try, а освобождение — в finally.
  • Возможность утечки ресурсов в создающей (захватывающей) функции, устраняется с помощью try..except, при этом захват ресурса производится перед try, а высвобождение в except с последующим raise.
  • Возможность утечки ресурсов в конструкторе устраняется с помощью сохранения ссылок на захваченные ресурсы в полях объекта и учетом и написанием деструктора, корректно удаляющего частично инициализированный объект. Это связано с порядком создания объекта в Delphi

  1. Выделяется память под объект и заполняется нулями
  2. Выполняется конструктор
  3. Если на предыдущем шаге возбуждено исключение, то вызывается деструктор, освобождается занятая память и перевозбуждается исключение.

  • Если ресурсы представляют собой ссылки на объекты, то в деструкторе достаточно использовать для них FreeAndNil
  • В пределах подпрограммы можно использовать аналогичную технику вместо многократного вложения блоков try..finally, помещая «конструктор» сразу после try и «деструктор» в finally. Так как локальные переменные не инициализируются, то это надо делать вручную, присваивая им nil перед try.
  • Если ресурс не реализован в виде класса и используется неоднократно — надо создать класс, в конструкторе которого осуществляется захват ресурса, а в деструкторе — освобождение. Это сделает использование ресурса более простым и единообразным, а отлов его утечек сведется к отлову утечек памяти.

Поиск утечек ресурсов
Поиск утечек, продолжение

11 Наследование, композиция, реализация, расширение


  • Если необходим функционал одного класса при реализации другого — проще всего использовать композицию, например, добавить ссылку на объект в список полей класса. Ограничения — у нового класса есть доступ только к публичным членам старого, необходимо писать делегирующий код.
  • Если имеющийся класс уже реализует часть возможностей нового, то можно использовать наследование. Ограничение — класс-наследник должен полностью заменять класс-родитель в любых ситуациях — например, для наследника TPersistent необходима корректная реализация Assign.
  • Если необходимо использовать новый класс так же, как имеющийся, но поведение требуется совершенно другое, то надо написать интерфейс, который и реализовать в обоих классах. В результате классы могут наследовать совершенно разным предкам, но со стороны использоваться через интерфейс одинаково. Ограничение — поведение через интерфейс не наследуется.
  • Если необходимо расширить поведение класса-предка (и дать тем самым новые возможности всем потомкам сразу), не вмешиваясь в его код, то можно написать хелпер. Ограничения — никаких виртуальных методов и новых полей, один хелпер на класс в пределах проекта.
    • Не стоит использовать хелперы без крайней необходимости


12 Интерфейсы


  • Об утечках при использовании интерфейсов можно не беспокоиться.
  • Методы, реализующие интерфейс должны быть невиртуальными и не публичными, обычно защищенными.
  • Любой интерфейс содержит как минимум три метода — QueryInterface, AddRef и Release.
  • Эти методы вызываются неявно: первый приведением к ссылке на интерфейс операцией as, второй и третий — при создании новой ссылки на интерфейс и удалении (или выходе из поля видимости).
  • При реализации интерфейсов потомками TObject необходимо реализовывать все три вышеупомянутых метода.
  • Потомки следующих классов имеют реализации QueryInterface, AddRef и Release по умолчанию:
    • TInterfacedObject реализует автоматическое удаление, когда на интерфейсы класса не остается ссылок, поэтому его потомки сразу после создания надо приводить к ссылкам на интерфейс.
    • TComponent игнорирует ссылки на реализуемые интерфейсы и удаляется только вручную, поэтому надо следить чтобы после удаления его потомка не оставалось висячих ссылок на интерфейсы.


13 Визуальные компоненты


  • Фреймы и формы не должны содержать в себе никакой логики, кроме логики отображения и передачи (не исполнения!) команд пользователя.
  • Все возможные варианты команд пользователя должны оформляться в виде Action и помещаться в один ActionList в том же фрейме, в котором эта команда доступна.
  • Автоматически созданные Delphi имена компонентов можно оставлять только для тех из них, на которые нет ссылок в коде методов и нет обработчиков. Осмысленное имя надо задавать до создания первого обработчика для данного компонента, иначе потом придется править имя обработчика вручную.
  • Автоматически созданные имена обработчиков следует менять только при крайней необходимости.
  • Обработчики событий визуальных компонентов не должны занимать более пяти строк. Комментарии к методам-обработчикам должны разъяснять не просто обстоятельства вызова (их как раз видно уже из имени обработчика), но прежде всего его назначение — зачем этот обработчик вообще понадобился.

14 Модули


  • Модуль, используемый более чем в одном проекте — библиотечный модуль.
  • Модуль, не содержащий ничего, кроме констант, интерфейсов, типов (не классов), исключений и вспомогательных процедур — интерфейсный модуль.
  • Модуль, содержащий код обмена данными с пользователем, файловой системой БД. сетью и тп. — модуль ввода-вывода.
  • Все остальные модули — модули логики.
  • Идеальная структура межмодульных зависимостей: интерфейсные модули зависят только от интерфейсных; модули логики — только от интерфейсных и других модулей логики; модули ввода-вывода — только от интерфейсных и других модулей ввода-вывода. Это позволяет эффективно использовать модульные тесты для логики и менять реализацию разных частей проекта независимо друг от друга.

15 Устранение лишних зависимостей


  • От публичной глобальной переменной — самый плохой вид зависимости, избавляться как минимум преобразуя переменную в функцию
  • От приватной глобальной переменной — чуть лучше, так как локализует проблему в рамках модуля. Необходимо следить за возможностью многопоточной модификации и инициализацией-завершением. Например, для глобальных переменных-интерфейсов при финализации модуля неявно вызывается Release.
  • От интерфейсного модуля — ничего без крайней необходимости менять не надо.
  • От глобальных классов, процедур и функций для любого ввода-вывода (GUI, БД, связь с сервером приложений и т.п.) других модулей. Если модули проекта сами предназначены для ввода-вывода — то ничего менять не надо, иначе желательно избавиться, реализовав эту связь через интерфейс — в противном случае будет крайне затруднено внесение изменений и тестирование.
  • От модулей, содержащих вышеперечисленное — лучше избавиться, выделив из модуля, на который есть ссылка, интерфейсную часть и ссылаясь только на нее.
  • От любого содержимого библиотечного или общего модуля — без крайней необходимости ничего менять не надо.