понедельник, 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. Поскольку наследник является лишь более специфичной версией своего базового класса, то для него должен работать принцип подстановки Лисков - там где используется базовый класс, можно использовать любого его наследника. При наследовании стоит перемещать общие интерфейсы и данные на более высокий уровень иерархии наследование, если при этом они не нарушают абстракцию. Если у класса только один наследник, то что-то в этом мире не так. Не стоит переопределять метод, оставляя его пустым - это нарушает абстракцию и этот код трудно сопровождать. Многоуровневое наследование плохо потому, что тяжело удержать в голове всё дерево или даже его ветвь. В общем и целом к наследованию стоит прибегать довольно редко потому, что оно чаще приводит к дополнительной сложности, чем к избавлению от неё.
Закон Деметры: объект класса А может вызывать любые из собственных методов, если он к тому же владеет экземпляром класса Б, то он может вызывать методы этого экземпляра, но ему не следует вызывать методы объектов, которые включает в себя класс Б. В целом, сотрудничество класса с другими классами должно быть минимизировано.
В отношении конструкторов можно сказать следующее: инициализируем в них все данные-члены и по возможности выполняем полное копирование экземпляров вместо ограниченного( передаём аргументы по значению, а не по ссылке).

вторник, 22 декабря 2015 г.

Глава 5. Проектирование при конструировании(часть 2)

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

Некоторые часто встречающиеся на практике подсистемы:

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

Эвристические методики проектирования:

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


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

  • Итерация - проектируем, ищем, что получилось неочень, делаем изменения и так по кругу.
  • Разделяй и властвуй - разбиваем программу на области и проектируем их отдельно
  • Нисходящий и восходящий подходы - при нисходящем мы начинаем с верхнего уровня абстракций и путём декомпозиции добираемся до самых деталей, при восходящем наоборот, начинаем с деталей и постепенно, находя между ними общее добираемся до верхнего уровня
  • Экспериментальное прототипирование - сделали прототип и выкинули его
  • Совместное проектирование
Во время проектирования пишем разную документацию, а именно: документация в коде, вики-статьи, UML диаграммы, резюме обсуждений, CRC-карточки

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

Глава 5. Проектирование при конструировании(часть 1)

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

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


Главный Технический Императив Разработки ПО: управление сложностью

 Сложность бывает двух видов - существенная и несущественная. Существенная сложность возникает из за сложности предметной области, которую мы абстрагируем в нашей программе. Несущественная сложность - необязательная сложность, которую мы вносим самостоятельно во время разработки, она возникает из-за несовершенства проектирования. Основные проявления несущественной сложности: сложное решение простой проблемы, простое, но неверное решение сложной проблемы и неадекватное сложное решение сложной проблемы. Основная задача при проектировании - снижение существенной сложности, которую нам приходится держать в голове в каждый конкретный момент времени(декомпозировать большие сложные проблемы на более мелкие) и сдерживание роста несущественной сложности.
Идеально спроектированный проект обладает следующими свойствами:
  • Минимальная сложность
  • Простота сопровождения(изменения)
  • Слабое сопряжение(loose coupling)
  • Расширяемость
  • Возможность повторного использования компонентов
  • Высокий коэффициент объединения по входу(к одному классу обращается множество других)
  • Низкий или средний коэффициент разветвления по выходу(класс обращается к малому количеству других классов)
  • Портируемость
  • Минимальная, но полная функциональность
  • Стратификация(разделение на части, которые могут быть познаны целиком)
  • Соответствие стандартным методикам(чтоб другим было легче воспринимать систему)
Уровни проектирования:
  1. Вся система
  2. Разделение на подсистемы(интерфейс, доступ и хранение данных, бизнес-логика и т.д.)
  3. Разделение подсистемы на классы
  4. Выделение данных и методов в классе
  5. Реализация каждого отдельного метода


суббота, 19 декабря 2015 г.

Глава 4. Основные решения, которые приходится принимать при конструировании

Я бы сказал, что это основные решения, которые нужно принять перед конструированием.
Среди них:

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

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

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

Вообще эти самые предварительные условия состоят из трёх следующих этапов:

  1. Определение проблемы
  2. Выработка требований
  3. Создание архитектуры
Определение проблемы - формулировка задачи, которую хочет решить пользователь разрабатываемой нами системы.
Эта формулировка должна быть полностью понятна пользователю и не должна содержать технический деталей или способа решения проблемы, только саму проблему.

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

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

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

Глава 2. Метафоры, позволяющие лучше понять разработку ПО

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

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

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