суббота, 23 января 2016 г.

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

Глава посвящена тому, как нужно именовать переменные.
Основной принцип - имя должно полно и точно описывать сущность, представляемую переменной.
Также при именовании переменной нужно ориентироваться на проблему, которую она решает, а не на конкретную реализацию решения.
Хороший диапазон имён переменных 8-20 символов.
Важное правило - чем короче область видимости переменной, тем короче может быть её имя.
Полезно заранее оговорить, как будут применяться спецификаторы вычисляемых значений(Total, Sum, Average, Record, String, Pointer ...) - на какой они будут позиции в имени.
Также, как и для методов, полезно последовательно использовать антонимы:


  • begin/end
  • first/last
  • locked/unlocked; min/max
  • next/previous
  • old/new
  • opened/closed
  • visible/invisible
  • source/target
  • source/destination
  • up/down
Для переменных некоторых типов нужно использовать дополнительные соглашения по именованию:

  • Индексы именуются обычно i, j, k
  • Переменным статуса нужно назначать имя описывающее возможные значения статуса, например reportType вместо statusFlag
  • К временным переменным нужно относиться с подозрением, если встречается переменная temp, возможно существует более подходящее название, описывающее суть переменной, или эта переменная служит для хранения разных сущностей, тогда лучше разбить её на две отдельные
  • Булевым переменным присущи типичные имена:
    • done - означает, что операция завершилась(результат не важен)
    • error - признак возникновения ошибки
    • found - признак того, что обнаружено искомое значение
    • success или ok - признак успешного завершения операции
  • Также при именовании булевых переменных часто используют префикс is + не нужно использовать отрицательные переменные(notFound, notDone) - это мешает красивым условиям и получается что-то типа if not notFound
  • Переменные хранящие элемент перечисления можно называть с учётом имени перечисления
В больших и крупных проектах желательно заранее сформировать конвенции именования переменных.
Конвенции бывают двух степени формальности - формальные и неформальные.
Конвенция, не зависящая от языка:

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

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

Если название переменной вышло очень длинным, нужно его сократить.
Вот некоторые советы по сокращению имён:

  • Использовать стандартные аббревиатуры
  • Удалять все гласные, не являющиеся первыми буквами имён
  • Удалять артикли и союзы
  • Сохранять одну или несколько первых букв каждого слова
  • Обрезать слова согласованно(по 1, 2 или 3 буквы - везде одинаково)
  • Сохранять первую и последнюю буквы слова
  • Сохранять до трёх выразительных слов
  • Удалять бесполезные суффиксы(ing, ed ...)
  • Сохранять наиболее выразительный звук каждого слога
  • Проверять, что смысл названия не изменялся при сокращении
Не стоит использовать фонетические аббревиатуры, так как другие не смогут угадать сходу, как вы заменили некоторые слоги, когда захотят использовать объявленную вами переменную.
Что ещё не нужно делать для сокращения имён:

  • Сокращать слова только на 1 символ
  • Сокращать одинаковые слова в разных переменных по-разному
  • Сокращать так, что имя потом нельзя чётко произнести
  • Не нужно сокращать до таких имён, которые легко неверно прочитать или произнести
Полезные советы:
  • Если для разных имён получаются одинаковые сокращения нужно найти синонимы
  • Нужно документировать короткие имена прямо в коде при помощи таблиц
  • Указывать все сокращения в проектном документе
  • Помнить, что имена создаются для программистов, читающих код
  • Избегать имён, содержащих цифры
  • Избегать орфографических ошибок
  • Избегать слов, при написании которых люди часто делают ошибки
  • Проводить различия между именами не только по регистру букв(не нужно делать переменные FRG и frg)
  • Избегать смешения естественных языков(пусть всё будет на английском)





четверг, 14 января 2016 г.

Глава 10. Общие принципы использования переменных

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

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

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

Интервал - среднее расстояние в количестве строк между обращениями к переменной. Интервал должен быть как можно меньше, тогда нам будет легче сконцентрироваться на смысле хранимой в переменной информации => программу легче читать.
Время жизни - количество строк между созданием и последним использованием переменной. Для него полностью справедливо предыдущее утверждение.
Общие советы по минимизации области видимости переменной:
  • Инициализировать переменные, используемые в цикле, непосредственно перед циклом, а не в начале метода, содержащего цикл 
  • Не присваивать переменной значения вплоть до его использования
  • Группировать связанные команды
  • Разбивать группы связанных команд на отдельные методы
  • Начинать с самой ограниченной области видимости и расширять её только при крайней необходимости

Время связывания - момент, когда переменная и её значение связываются вместе. Например:
  • При написании кода - захардкоженные значения
  • Во время компиляции - через константы или макросы
  • В период выполнения - при загрузке программы(из реестра, файла и т.д.), при создании объекта, по требованию(например при каждой перерисовке окна)
Чем раньше время связывания, тем ниже гибкость, но и тем ниже сложность программы. Так что в этом деле важен баланс.

Виды данных по типу управления ими:
  • Последовательные - нужно несколько последовательных команд, чтобы их все обработать
  • Селективные - обрабатываются либо одни, либо другие данные(управление с помощью if-else)
  • Итеративные - обрабатываются при помощи перебора

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

понедельник, 11 января 2016 г.

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

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

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

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

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


Проектирование метода: 

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

Кодирование метода:

  • Написать объявление метода
  • Изменить псевдокод на высокоуровневые комментарии
  • Написать код под каждым комментарием
  • Проверить, нужна ли дальнейшая декомпозиция кода, если нужна, то выделить код в отдельный вспомогательный метод

Проверка кода:

  • Умозрительно проверить ошибки в метода - пройтись по всем веткам выполнения
  • Компиляция метода - с наивысшим уровнем предупреждений компилятора
  • Проход по всему коду отладчиком
  • Тестирование кода
  • Удаление найденных ошибок

Наведение глянца:

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

Repeat n times...


Альтернативы проектированию через псевдокод:
  • Разработка через тестирование
  • Рефакторинг
  • Проектирование по контракту
  • Бессистемное программирование - тяп-ляп и в продакшен

четверг, 7 января 2016 г.

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

Защитное программирование - это способ обезопасить себя от неверных входных данных и ошибок программистов.
Для того чтобы защитить программу от неправильных входных данных нужно:

  • Проверять все данные из внешних источников
  • Проверять все входные параметры метода
  • Решить, как обрабатывать неверные входные данные
Защитное программирование как правило увеличивает объём и сложность программы, так что использовать его нужно с умом.
Для поиска программистских ошибок используются утверждения(assertions). Они помогают выявить случаи неправильного использования кода другими программистами. Утверждения должны проверять условия, которые всегда соблюдаются при правильном использовании метода, например:

  • Значение входного параметра попадает в ожидаемый интервал
  • Файл или поток открыт, когда метод начинает выполняться
  • Указатель файла или потока находится в начале, когда метод начинает выполняться
  • Файл открыт только для чтения, только для записи или для чтения и записи
  • Значение входной переменной не меняется в методе
  • Указатель нулевой
  • Результаты работы сложного, хорошо оптимизированного метода совпадают с результатами метода более простого, но медленного метода
Утверждения также полезно использовать для документирования и проверки предусловий и постусловий.
Если требуется большая надёжность, то следует кроме утверждений также обрабатывать возможные ошибки.
Когда мы обнаруживаем ошибку нашим обработчиком ошибок у нас есть несколько вариантов, как поступить с неверным значением:

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

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

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

понедельник, 4 января 2016 г.

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

Метод - это отдельная функция или процедура, выполняющая одну задачу.
Причины создания методов:

  • Снижение сложности - за счёт абстрагирования деталей реализации различных операций
  • Формирование понятной промежуточной абстракции - за счёт хорошего именования метода
  • Предотвращение дублирования кода
  • Поддержка наследования - переопределять отдельные маленькие методы проще, чем один большой
  • Сокрытие очерёдности действий - если два действия должны идти в определённом порядке, то нужно их обернуть в метод, чтобы не допустить где-нибудь неправильного порядка
  • Улучшение портирования - непортируемый(платформозавизимый) код нужно вынести в метод, чтобы его легко можно было заменить при переезде на другую платформу
  • Упрощение сложных булевых проверок - упрощает восприятие при чтении
  • Повышение быстродействия - легче оптимизировать код в одном месте
Не стоит бояться создавать методы для очень коротких действий, если они повторяются хотя бы пару раз.
Очень важной характеристикой метода является связность - метод должен выполнять одну строго определённую и понятную задачу. Связность бывает нескольких видов:

  • Функциональная - метод выполняет одну и только одну операцию
  • Последовательная - метод содержит операции, выполняющиеся в определённом порядке, одна операция полагается на результат другой. Такой метод лучше разбить на два, если это возможно.
  • Коммуникационная - если метод содержит несколько операций объединённых только общими данными, к которым они обращаются
  • Временная - в методе несколько операций, объединённых по временному промежутку, в который они выполняются, например Startup(), AfterCreation()...
Остальные виды связности обычно неприемлемы.
Следует уделять много внимания именованию методов:

  • Нужно описать в названии всё, что метод возвращает и его побочные эффекты
  • Избегаем невыразительных и неоднозначных глаголов, например HandleCalculation(), ProcessInput(), DealWithOutput()...
  • Не стоит использовать номера для идентификации методов (Task1(), Task2())
  • Не стоит ограничивать длину имени метода
  • Для именования функции нужно использовать описание возвращаемого значения
  • Для именования процедуры используем выразительный глагол + объект с которым он работает
  • Используем общепринятые антонимы для описания противоположных действий:
    • add/remove
    • begin/end
    • create/destroy
    • first/last
    • get/put
    • get/set
    • increment/decrement
    • insert/delete
    • lock/unlock
    • min/max
    • next/previous
    • old/new
    • open/close
    • show/hide
    • source/target
    • start/stop
    • up/down
  • Определяем конвенции именования часто используемых операций - например операции получения id объекта
Относительно оптимального объёма метода ясности нету, понятно только, что методы длиной более 200 строк уже не очень хорошо поддерживаются и скорее всего у них проблемы со связностью.
Параметры в методы нужно передавать в одном и том же порядке. Наиболее распространённый порядок - входящие значения, изменяемые значения, исходящие значения. Не нужно передавать в метод неиспользуемые параметры. Лучше не использовать входящие переменные в качестве рабочих переменных, а создавать их локально внутри метода. Пишем документацию по параметрам метода - виды(входной, изменяемый, выходной) параметров, единицы измерения, возвращаемые значения, ограничения на входящие значения. 
Количество аргументов метода не должно превышать 7. Для этого иногда стоит передавать не отдельные поля какого-либо класса, а весь экземпляр целиком. Иногда можно использовать именованные параметры( в C# поддерживаются), когда в метод передаются несколько аргументов одного типа.

понедельник, 28 декабря 2015 г.

Глава 6. Классы(2 часть)

Причины создания классов:

  • Моделирование объектов доменной области
  • Моделирование абстрактных объектов - их не существует в реальном мире, но они абстрагируют другие объекты реального мира. Например класс Shape создаёт абстракцию для таких фигур как Circle и Square.
  • Снижение сложности - за счёт сокрытия информации, разбиения её на удобоваримые слои.
  • Изоляция сложности - локализуем возможные проблемы, которые будут возникать из-за высокой сложности.
  • Сокрытие деталей реализации - так её легко изменить
  • Ограничение влияния изменений
  • Сокрытие глобальных данных
  • Упрощение передачи параметров в методы - если в несколько методов передаётся один и тот же параметр, то возможно эти методы должны быть объединены в один класс, который будет владеть этим широко используемым полем
  • Создание центральных точек управления
  • Облегчение повторного использования кода
  • Планирование создания семейства программ - часть классов остаётся такой же, а часть переписываются под каждое конкретное приложение
  • Упаковка родственных операций - например набор тригонометрических функций, методов работы со строками
  • Различные виды рефакторинга
Классы, которых следует избегать:

  • God-object - нарушает single responsibility principle как бы
  • Нерелевантные классы - если класс имеет только данные, но не формы поведения, возможно его свойства стоит просто раздербанить по другим классам
  • Классы, имена которых напоминают глаголы - они обычно содержат только поведение, но не данные. Возможно лучше просто сделать их методами какого-нибудь другого класса
Также не следует пренебрегать более высоким уровнем агрегации - пакетами, модулями, областями имён и т.д.

пятница, 25 декабря 2015 г.

Глава 6. Классы(1 часть)

В основе классов лежат абстрактные типы данных - это набор, включающий данные и выполняемые над ними операции. По сути, АТД - это взаимосвязанные между собой данные и операции с этими данными. Обычно АТД выгоднее использовать, чем примитивные типы, потому что это позволяет нам скрыть реализацию экземпляра, значит мы можем её легко поменять, например, было поле int usersCount, его будет легко изменить на bigint usersCount, если оно обёрнуто в АТД. Интерфейс становится более информативен, например  Pixels fontSize информативнее int fontSize. Также код становится легче оптимизировать и проверять его работу, так как семантические области чётко ограничены и при правильном подборе названий легко читаемы и воспринимаемы. При подборе названия АТД желательно, чтобы в нём не содержалось среды или способа его реализации, например ScoresTable предпочтительнее ScoresFile, так как мы можем захотеть изменить способ хранения счетов с файла на бд например.
Класс - это АТД, который поддерживает наследование и полиморфизм. Самое главное в хорошо спроектированном классе - это его интерфейс. Он должен представлять из себя хорошо ограниченную абстракцию. Публичные методы должны быть хорошо согласованны друг с другом - служить единой цели. Интересный совет - следить за тем, что у метода  есть противоположный ему, то есть, если есть метод добавления элемента, то скорее всего должен быть метод его удаления(но не всегда). Если видим, что какие-то методы имеют отличную от других ответственность, то выносим их в отдельный класс. Нужно постоянно следить за целостностью интерфейса при изменении класса, потому что её легко нарушить.
Вдобавок к хорошей абстракции элементы класса должны иметь правильную инкапсуляцию. Нужно стремиться минимизировать доступность членов классов для других классов - публичных методов не должно быть много, меньше 7. Все данные должны быть закрытыми - открыты только методы. Не стоит делать предположений о клиентах класса. Совсем не стоит нарушать семантическую инкапсуляцию(по сути предыдущее предложение про то же самое). Снижаем связанность с другими классами.
Существуют разные виды отношений между классами, далее идут два из них.
Включение - класс А содержит в качестве одного из своих элементов экземпляр класса B. Использовать это отношение вполне нормально, главное в целом не перебарщивать с количеством членов-данных, их не стоит делать больше 7 штук. Большое их количество чаще всего свидетельствует о нарушении принципа единой ответственности.
Наследование - класс А является более специфическим вариантом класса B. Поскольку наследник является лишь более специфичной версией своего базового класса, то для него должен работать принцип подстановки Лисков - там где используется базовый класс, можно использовать любого его наследника. При наследовании стоит перемещать общие интерфейсы и данные на более высокий уровень иерархии наследование, если при этом они не нарушают абстракцию. Если у класса только один наследник, то что-то в этом мире не так. Не стоит переопределять метод, оставляя его пустым - это нарушает абстракцию и этот код трудно сопровождать. Многоуровневое наследование плохо потому, что тяжело удержать в голове всё дерево или даже его ветвь. В общем и целом к наследованию стоит прибегать довольно редко потому, что оно чаще приводит к дополнительной сложности, чем к избавлению от неё.
Закон Деметры: объект класса А может вызывать любые из собственных методов, если он к тому же владеет экземпляром класса Б, то он может вызывать методы этого экземпляра, но ему не следует вызывать методы объектов, которые включает в себя класс Б. В целом, сотрудничество класса с другими классами должно быть минимизировано.
В отношении конструкторов можно сказать следующее: инициализируем в них все данные-члены и по возможности выполняем полное копирование экземпляров вместо ограниченного( передаём аргументы по значению, а не по ссылке).