Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Стив Макконнелл - Совершенный код

cover

Steve McConnell https://stevemcconnell.com/books/

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

Конструирование — главный этап разработки ПО, без которого не обходится ни один проект.

Основные этапы конструирования: детальное проектирование, кодирование, отладка, интеграция и тестирование приложения разработчиками (блочное тестирование и интеграционное тестирование).

Строительная метафора: построение ПО

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

Какие еще параллели можно провести между сооружением дома и разработкой ПО? При возведении дома никто не пытается конструировать вещи, которые можно купить. Здравомыслящему человеку и в голову не придет самостоятельно разрабатывать и создавать стиральную машину.

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

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

Из книги Мартина Фаулера. "Рефакторинг. Улучшение проекта существующего кода."

Глава 2. Семь раз отмерь, один раз отрежь: предварительные условия

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

Если вы спроектировали автомобиль «Понтиак Ацтек», то сколько бы вы его ни тестировали, он никогда не превратится в «Роллс-Ройс». Конструирование — средний этап работы, поэтому ко времени начала конструирования успех проекта уже частично предопределен. А первоначальный этап — это планирование.

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

Глава 5. Уровни проектирования

Уровни проектирования:

  1. Программная система (вся система)
  2. Разделение системы на подсистемы/пакеты
  3. Разделение подсистемы/пакетов на классы
  4. Разделение классов на данные и методы
  5. Проектирование методов

Уровень 2

Подсистемы - это модуль работы с БД, модуль GUI, создание отчетов, бизнес-правила модуль. Суть разделение подсистемы в знании составных блок и как они будут взаимодействовать и ограничивать доступ. Если все подсистемы будут взаимодействовать то выгода от их разделения исчезает.

Вопросы:

  • В скольких разных частях системы нужно разобраться чтобы внести изменение в одну из частей системы?
  • Что будет если задействовать модуль бизнес-правил в другой системы?
  • Что будет если добавить новый пользовательский интерфейс (инструмент тестирования командной строки)
  • Что произойдет если вы захотите перенести модуль хранения данных на удаленный компьютер?

Часто используемые подсистемы:

  • Бизнес-правила - это реализуемые законы,процедуры бизнеса в компьютерной системе. Для упрощения бизнес-логики используйте ее выражение в форме таблиц (глава 18)
  • Пользовательский интерфейс, его изоляция позволяет при его изменении не изменять остальную программу.Состоит из подсистем или классов, отвечающих за GUI, интерфейс командной строки, работу меню, работу с окнами.
  • Доступ к БД,вы должны скрыть детали реализации доступа к БД, что бы программа работала с данными в терминах бизнес-проблемы, т.е. должны быть отдельные обьекты посредники данных. Что бы можно было изменить структуру БД и самому БД без внесения изменений в остальную часть программы.

Уровень 3

Определить все классы программы Например, подсистема доступа к БД может быть разделена на классы доступа к данным и классы хранения данных и класс метаданных БД. Важно определить детали взаимодействия каждого класса с остальными элементами системы, особенно интерфейса.Суть в декомпозиции подсистемы до уровня детальности классов.

Уровень 4

Разделение каждого класса на методы. Полное определение методов класса позволяет лучше понять его интерфейс, что может подтолкнуть к изменению интерфейса т.е. вернуться на Уровень 3

Уровень 5

Определение детальной функциональности отдельных методов. Написание псевдокода, поиск алгоритмов, оптимальный организацией фрагментов метода и написание кода.

Сопряжение

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

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

Критерии оценки сопряжения:

  • Обьем: чем больше связей между модулями или классами тем выше сопряжение чем больше передаваемых параметров тем выше
  • Видимость: чем лучше видно, явная связь между модулями или классами тем меньше сопряжение и если модули или классы работают через общее состояние тем выше сопряжение. Связь через глобальные данные должна быть задокумментированна.
  • Гибкость: высокая сопряженность если модули обмениваются обьектами а не простыми данными слабая сопряженность будет если обьекты будут меньше знать о работе друг друга."

Виды сопряжения (стр. 98):

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

Применение шаблонов - это эффективный способ управления сложностью: Популярные шаблоны: Adapter, Bridge, Decorator, Facade, Factory Method, Observer, Singleton, Strategy, Single Method.

Глава 6. Классы

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

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

Хорошая инкапсуляция (стр. 138)

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

Наследование (стр. 141)

Барбара Лисков заявила, что наследование стоит использовать только если производный класс действительно "является" более специализированной версией базового иначе если производный класс не будет полностью придерживаться контракта интерфейса базового класса то не следует использовать наследования, а сделайте включение или измените абстракцию наследования. Если вам нужна реализация класса но не его интерфейс не наследуйте его, использйуте включение. Цель наследования уменьшить сложность и использовать более простой код без дублирования.

Остерегайтесь пустых переопределенных методов, лучше избавиться от причины проблемы чем от ее следствий. Это нарушение абстракции изменяя семантика интерфейса.Как следствие развитие такого подхода поведение производных класов будет сильно отличаться от базовых интерфейсов. Решение: включить обьект проблемы в класс. (стр. 143)

Управление сложностью (стр. 146)

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

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

Разумные причины создания классов (стр. 148)

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

Ключевые моменты (стр. 156)

  • Интерфейс класса должен формировать согласованную абстракцию
  • Интерфейс класса должен что-то скрывать особенно взаимодействия с системой, детали реализации
  • Включение обычно предпочтительней наследованию но если только вы не моделируете отношения ""является"" так как наследование повышает сложность
  • Классы - главное средство управления сложностью.

Глава 7. Высококачественные методы

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

Удачные имена методов (стр. 167) Главной задачей имени метода это ясное понятное описание сути метода.

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

Порядок параметров методов: "входные - изменяемые - выходные"

Когда использовать функции (стр. 177)

  • Функция - это метод, возвращающий значение
  • Процедура - это метод, не возвращающий значение

Глава 8. Защитное программирование

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

  • Утверждение asset используется для проверки данных из проверенного источника.
  • Обработчик ошибок для данных поступивших из внешнего источника.

Утверждения asset применяются для ошибок, которые никогда не должны происходить.

Способы обработки ошибок: (стр. 189)

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

Корректность требует верных результатов.

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

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

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

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

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

Путь данных: (стр. 200)

  1. Источник небезопасных данных
    • графический интерфейс
    • интерфейс командной строки
    • данные в режиме реального времени
    • внешние файлы
    • др. внешние обьекты
  2. Проверенный класс отвечающий за очистку данных, "баррикада".
  3. Внутренний класс, который может считать данные корректными.

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

Глава 9. Процесс программирования псевдокодом

Псевдокод (стр. 212) должен описывать намерение/назначение/поведение кода, а не метод/способ решения или то как его писать.

Методика создания классов и методов:

  • псевдокод
  • через тестирование т.е. сначало тестируем потом пишем код
  • рефакторинг, усовершенствование "пахнущего" кода
  • проектирование по контракту, пре- и пост- условиям

Пример псевдокода:

Отследить текущее число 
Если другой ресурс доступен
    Выделить структуру для диалогового окна
        Если структура для диалогового окна может быть выделена
        ...
    Конец если
Конец если
Вернуть true  если новый ресурс был создан, иначе вернуть false

Глава 11. Сила имен переменных

Сила имен переменных (стр. 254-261)

Имя переменной должно полно и точно описывать сущность. Можно сформулировать суть переменной в словах.

Пример: число членов олимпийской зборной команды США "numberOfPepleOnTheUsOlympicTeam", это имя не нужно расшифровывать его можно просто прочитать. Имя переменной "date" ничего не говорит о дате, иначе выглядит "currentDate", это уже лучше, мы уже значем, что это текущая дата.

Хорошее имя выражает "что" , а не "как".

Оптимальная длина имени до 40 символв, встреднем от 9-15.

  • "NumCustomer" это общее число заказчиков
  • "CustomerNum" это номер текущего заказчика

Именование переменных цикла i,j,k в случае вложенных циклов имеет смысл давать индексам осмысленные имена "firstItem" "temIndex"

Именование переменных статуса. Не используйте имя "flag". Нужно присваивать выразительные имена, сравниваемые со значениями перечислений, именованных констант.

Пример:

не выразительных и загадочных имен:                                 выразительно:
if(flag)                                                            if(dataReady)
if(statusFlag & 0x0F)                                               if(characterType & PRINTABLE_CHAR) перечисление
if(printFlag == 16)                                                 if(reportType == ReportType_Annual) именованная константа

flag=0x1                                                            dataReady = true
statusFlag=0x80                                                     characterType=CONTROL_CHARACTER
printFlag=16
код не должен содержать загадок, его просто можно прочитать.

Именование временных переменных. Подумайте о ее роли, имя temp плохо об этом говорит. Пример:

плохо temp=sqrt(вычисление) 
хорошо discriminant=sqrt(вычисление)
ясное использование root=(-b + discriminant)/(2*a)

Именование булевых переменных. Полезные имена "done" признак завершения цикла, "error", "found", "succes", "ok". Присваивайте имена которые подразумевают тип данных bolean.

Пример:

имя "status" не означает что оно может содержать bolean, а вот "statusOK" - может, еще имя "sourceFile" можно заменить на "sourceFileAvailable","sourceFileFound","processingComplete". Добавление префикса "is" может затруднить чтение "if(isFound)" хуже читается чем "if(found)" , а "isStatus" вообще не имеет смысла.

Именование значений для перечисления Color может быть таким: Color_Red, Color_Green. Существуют мнения о именовании значений перечислений как обычную переменную или константу.

Именование констант CYCLE_NEEDED должно характеризовать абстрактную сущность, а не конкретное значение. Пример: FIVE=5.5 это плохое имя для константы.

Глава 14. Организация последовательного кода

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

Пример: неявная зависимость:

revenue.ComputeMonthly();
revenue.ComputeQuartle();

явная зависимость:

dataRead=ReadData();
result=CalculateResultFromData(dataRead);
  • Не пишите код требующий порядковой последовательности (или изолируйте эту последовательность за интерфейсом)
  • Если пишите последовательный код то сделайте зависимости явными
  • Если не получается сделатиь явную зависимость тогда задокументируйте этот момент

Глава 22. Тестирование выполняемое разработчиками

Тестирование не доказывает отсутствие ошибок . Если нет ошибок возможно сами тесты неполны. Тестирование не повышает качества ПО - оно указывает на качество, но не влияет на него.

Тип теста:

  • Чистый тест нацелен на проверку работы кода
  • Грязный тест нацелен на попытку нарушить код и грязные тесты должны преобладать в тестировании.(стр. 492)

Глава 24. Рефакторинг

Факторинг - это декомпозиция на составные части.

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

Причины рефакторинга:

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

Глава 25. Стратегии оптимизации кода

Оптимизация кода. Улучшение производительности - это "антирефакторинг" изменения ухудшают внутреннюю структуру программы ради повышения ее производительности.

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

Принцип Парето

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

Не стоит ставить оптимизацию важнее чем архитектуру.

Частые причины снижения эффективности:

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

Глава 26. Методики оптимизации кода

Замена сложных логических выражений на обращение к таблице (табличный метод), экономит время выполнения от 30% (стр. 600)

  • Размыкание цикла. Замыкание цикла - это принятие решения внутри цикла, а размыкание это принятие решения снаружи цикла
if(true) {
    for()...
else {
    for()...

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

  • Развертывание цикла Способ написания цикла при каждой итерации выполняя два и более случая
i=0
while(i < count-2) {
    a[i]=i;
    a[i+1]=i+1;
    a[i+2]=i+2;
    i=i+3;
}
  • Уменьшения операций в циклах
  • Кеширование часто необходимой и тяжелой информации
  • Замена возведения в степень на умножение, а умножение на сложение. Или замена умножения и деления на операции сдвига
  • Предварительные вычисления выражений

Глава 33. Личность

Эффективное программирование - это поиск способов компенсировать ваши интелектуальные способности к пониманию кода. Так как человек ограничен нужно писать код так что бы было легко его понять другому человеку.

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

Если вы не можете протестировать класс из-за его сильной сопряженности то перепишите класс. Если вы не можете использовать повторно код, то это тоже, сильное сопряжение которое следует понизить. (стр. 829)

Глава 34. Основы мастерства

Боритесь со сложностью:

  • Разделите систему на подсистемы на уровне архитектуры, чтобы концентрироваться в каждый конкретный момент времени на меньшей части системы.
  • Тщательно определяйте интерфейсы классов, чтобы можно было игнорировать внутреннее устройство классов. Поддерживайте абстракцию, формируемую интерфейсом класса, чтобы не запоминать ненужных деталей.
  • Избегайте глобальных данных, потому что их использование значительно увеличивает процент кода, который нужно удерживать в уме в любой момент времени. Глобальные данные вносят в код неопределенность.
  • Избегайте глубокой вложенности циклов и условных операторов, поскольку их можно заменить на более простые управляющие структуры, позволяющие бережнее расходовать умственные ресурсы.
  • Избегайте операторов goto, так как они вносят в программу нелинейность, за которой большинству людей трудно следовать.
  • Используйте ясные, очевидные имена переменных, чтобы не вспоминать детали вроде «i — это индекс счета, а j — индекс клиента или наоборот?».
  • Присваивайте промежуточным переменным промежуточные результаты вычислений с целью документирования этих результатов.

Программирование развивается преимущественно за счет повышения абстрактности программных компонентов. Фред Брукс утверждает, что самым крупным достижением в компьютерных науках можно считать переход от машинного языка к высокоуровневым языкам: он освободил программистов от забот об особенностях отдельных устройств и позволил сосредоточиться на самом программировании (Brooks, 1995).

Функциональное именование переменных, отвечающее на вопрос «что?» уровня проблемы, а не «как?» уровня реализации, повышает уровень абстракции. Если вы говорите: «Я выталкиваю элемент из стека, получая данные о самом последнем сотруднике», — абстракция может избавить вас от выполнения умственного этапа «Я выталкиваю элемент из стека». Вы просто говорите: «Я получаю данные о самом последнем сотруднике». Эта выгода невелика, но если вы пытаетесь сократить диапазон сложности, простирающийся от 1 до 109, важен каждый шаг к цели.

Пишите программы в первую очередь для людей и лишь во вторую — для компьютеров

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

Программируйте с использованием языка, а не на языке

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

Программируйте в терминах проблемной области

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

Высокоуровневый код не должен включать подробных сведений о файлах, стеках, очередях, массивах, символах и подобных объектах, имеющих имена вроде i, j и k. Высокоуровневый код должен описывать решаемую проблему. Он должен быть наполнен описательными именами классов и вызовами методов, ясно характеризующими выполняемые действия, а не подробными сведениями о том, что файл открывается в режиме «только для чтения». Высокоуровневый код не должен быть загроможден комментариями, гласящими, что «здесь переменная i представляет индекс записи из файла о сотрудниках, а чуть позже она используется для индексации файла счетов клиентов».

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

Разделение программы на уровни абстракции

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

Проектируя программу, обдумайте уровни абстракции:

Уровни абстракции
4. Высокоуровневые элементы
3. Низкоуровневые элементы
2. Низкоуровневые структуры реализации
1. Структуры и средства языка программирования
0. Возможности операционной системы и машинные команды

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

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

  • Уровень 1: структуры и средства языка программирования. Структуры языка программирования — это элементарные типы данных, управляющие структуры и т. д. Кроме того, большинство популярных языков снабжено дополнительными библиотеками, предоставляют доступ к вызовам ОС и т. д. Вы используете эти структуры и средства естественным образом, так как программировать без них невозможно. Многие программисты никогда не поднимаются выше этого уровня абстракции, чем значительно осложняют себе жизнь.

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

  • Уровень 3: низкоуровневые элементы проблемной области. На этом уровне вы имеете дело с примитивами, нужными для работы в терминах проблемной области. Это клей, скрепляющий нижележащие структуры компьютерных наук и высокоуровневый код проблемной области. Чтобы писать код на этом уровне, вы должны определить словарь проблемной области и создать строительные блоки, годные для решения поставленной задачи. Во многих приложениях этим уровнем является уровень бизнесобъектов или уровень сервисов.

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

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

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

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

Эклектизм

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

Экспериментирование при разработке ПО позволяет узнать, эффективен ли тот или иной подход.

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

По уровню детализации и абстракции (Стиль проектирования)
  1. Подход "Сверху-вниз" (Top-Down)

    • Суть: Начинается с общего, высокоуровневого представления системы (контекстная диаграмма, ключевые бизнес-сущности и процессы). Затем каждый компонент последовательно детализируется на более мелкие и конкретные части, пока не будет достигнут нужный уровень детализации.
    • Аналогия: Сначала нарисовать эскиз дома (общая форма, этажи), затем план каждой комнаты, затем расстановку мебели.
    • Когда используется: Для новых, сложных и хорошо понятных систем, где требования стабильны.
  2. Подход "Снизу-вверх" (Bottom-Up)

    • Суть: Начинается с анализа уже существующих или очевидных низкоуровневых компонентов, сервисов или модулей. Затем эти компоненты комбинируются и группируются в более крупные подсистемы, чтобы в итоге получить целостную архитектуру.
    • Аналогия: Сначала есть набор кирпичей, досок и гвоздей (готовые библиотеки, сервисы), и из них собирается структура дома.
    • Когда используется: При интеграции унаследованных систем, использовании готовых решений (например, облачных сервисов) или когда требования размыты и нужно быстро получить работающий прототип.
  3. Подход "Внутри-снаружи" (Inside-Out) или "От стержня"

    • Суть: Фокус сначала на самой важной, центральной бизнес-логике (Domain Model). Спроектировав ядро системы, проектировщик затем "обрастает" его внешними интерфейсами (API), слоями доступа к данным и пользовательскими интерфейсами.
    • Аналогия: Сначала построить несущий каркас и сердечник здания, а затем добавить стены, коммуникации и отделку.
    • Когда используется: В Domain-Driven Design (DDD), когда бизнес-логика является самой сложной и ценной частью системы.

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

три конкретные методологии и техники
  1. Объектно-ориентированное проектирование (Object-Oriented Design)

    • Суть: Проектирование системы как набора взаимодействующих объектов, которые инкапсулируют данные и поведение.
    • Ключевые концепции:
      • Инкапсуляция: Сокрытие внутреннего состояния объекта.
      • Наследование: Создание иерархий классов.
      • Полиморфизм: Использование объектов с одинаковым интерфейсом по-разному.
    • Что делаем на детальном уровне: Определяем классы, их атрибуты, методы, отношения (наследование, ассоциация, композиция), продумываем иерархии. Фокус на моделировании предметной области через объекты.
  2. Структурное (или Модульное) проектирование (Structured Design)

    • Суть: Наследие процедурного программирования. Система разбивается на иерархию модулей (функций, процедур), которые обмениваются данными.
    • Ключевые концепции:
      • Сцепление (Coupling): Стремимся к слабому сцеплению между модулями.
      • Связность (Cohesion): Стремимся к сильной связности внутри модуля (чтобы модуль выполнял одну четкую задачу).
      • Сокрытие информации (Information Hiding): Сокрытие деталей реализации модуля.
    • Что делаем на детальном уровне: Проектируем функции, передаваемые между ними параметры, структуры данных. Создаем иерархии вызовов. Фокус на потоке данных и управления.
  3. Проектирование, основанное на данных (Data-Driven Design)

    • Суть: Структура данных является центральной и определяет архитектуру программы. "Покажите мне ваши структуры данных, и я скажу, как работает ваш код".
    • Ключевые концепции:
      • Моделирование данных: Проектирование схемы БД, форматов файлов, структур передаваемых сообщений.
      • Шаблоны: Активное использование таких шаблонов, как Таблица управляющих данных (Table-Driven Approach) для замены сложных условных конструкций (длинных цепочек if-else или switch-case).
    • Что делаем на детальном уровне: Вместо того чтобы проектировать сложную логику, мы проектируем структуру данных (например, таблицу или конфигурационный файл) и простой код, который ее интерпретирует. Фокус на том, что обрабатывается, а не как.

На уровне языка программирования экспериментом может быть написание небольшой экспериментальной программы для изучения плохо знакомых вам аспектов языка (например: многопоточность vs асинхронность, обобщения generics, макросы ...).

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

А как тебе такой эксперимент, Стив Макконнелл?

Эволюционный или инкрементный подход (Evolutionary / Emergent Design)

Суть: Архитектура не проектируется полностью заранее, а "проявляется" и дорабатывается по мере разработки, в ответ на обратную связь и новые требования. Характерен для Agile-методологий.

Эксперимент: Сравнить результат проекта, где архитектура была детально проработана на старте (Big Design Upfront), с проектом, где архитектура развивалась итеративно.