OO Design Goals

Зачем нам все это?

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

4 Симптома гниющего дизайна

Существует четыре основных признака, говорящих нам, что наши проекты гниют:

  • Rigility (Жесткость)

Смысл: Разработчику тяжело вносить изменения (дерево изменений по коду). Каждое изменение вызывает каскад последующих изменений в зависимых модулях.

Последствия: Боязнь фиксить не критичные изменения.

Итог: В тот момент, когда менеджеры отказываются позволять вносить изменения в продукт,
жесткость приводится в движение. Это приводит в последствии к неблагоприятному процессу управления.

  • Fragility (Хрупкость)

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

Последствия: Боязнь непредсказуемого поведения при внесении изменений в код.

Итог: Господство недоверия и потеря авторитета.

  • Immobility (Неподвижность)

Смысл: Потеря времени при попытке вынести кусок кода.

Неподвижность - неспособность переиспользования продукта из других проектов или
кусочков кода внутри одного проекта.

Последствия: Продукт переписывается вместо переиспользования.

Итог:

  • Viscosity (Вязкость)

Смысл: В проекте возникают проблемы, не связанные с внесением изменений. (не может запуститься приложение, упал сервер и так далее).

Вязкость насчитывает две формы: вязкость проекта и вязкость среды.

Появляется: Когда разработка медленная и неэффективная.

Последствия: Просто сделать неправильную вещь, трудно сделать правильную вещь.

Общий Итог: Эти четыре симптома - контрольные признаки плохой архитектуры.

Причины появления симптомов

  • Непредсказуемость изменений требований (должны быстро внести изменения в существующий дизайн системы не ломая структуру и не приводя к "гниению" кода);
  • Управление зависимостями при стабильных элементах кода (новые и незапланированные изменения для зависимостей между модулями программного обеспечения). Решение: управление зависимостями между модулями: создание брандмауэров зависимости.

Принципы Объектно Ориентированного Дизайна

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

Проблема:

Создавайте программное обеспечение легким для изменениям, когда вам это нужно

· В случае, когда хотим изменить класс или пакет для добавления новой функциональности, изменить бизнес-правила или улучшить дизайн

· В случае, когда необходимо изменить класс или пакет из-за изменения другого класса или пакета, от которого он зависит (например, изменение сигнатуры метода),

· Управление зависимостями между классами и пакетами классов для минимизации влияния изменения(й) на другие части приложения

· Минимизация причин, по которым модули или пакеты необходимо будет изменить из-за изменений в модуле или пакете, от которого они зависят

Cat Poetry (между хорошо определенными интерфейсами)

A Hazelnut In Every Bite (Лесной орех в каждом байте)

· Большая часть объектно-ориентированого проекта = управлению зависимости

· Сложно писать объектно-ориентированный код без создание зависимости от чего-либо

· 99.9% строк кода содержит, по крайней мере, одно значимое проектное решение

· Любой, кто пишет код, определяет дизайн системы

Class Design

· Class Cohesion Связность класса

· Open-Closed Принцип открытости-закрытости

· Single Responsibility Принцип единственной ответственности

· Interface Segregation Разделение интерфейсов

· Dependency Inversion Инверсия зависимости

· Liskov Substitution Принцип подстановки Барбары Liskov

· Law of Demeter закон Дементры

· Reused Abstractions Повторно используемые абстракции

Class Cohesion

Reasoning: Проектирование класса должно сократить потребность редактирования многочисленных классов при внесении изменений в логику приложения. Фундаментальная цель объектно-ориентированного проектирования состоит в том, чтобы поместить поведение (методы) настолько близко к данным, на которые они воздействуют (атрибуты) насколько это возможно, так, чтобы изменения, наименее вероятно распространились через многочисленные классы.

Схема:

Рефакторинг:Идея:

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

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

Метрика:

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

Ссылка на ассоциативный объект = атрибуту

Open-Closed Principle

Reasoning: Как только рабочий класс протестирован, изменение его кода может привести к новым дефектам. Этого можно избежать, расширив класс, оставляя его код неизменным, чтобы добавить новое поведение.

Классы должны быть открыты для расширения, но закрыты для модификации.

Схема:

Рефакторинг:

Идея Класс был унаследован, расширен новым атрибутом, и переопределена используемая функция. Таким образом, протестированный рабочий класс Аккаунт расширился, но не был изменен.

Метрика За успешную регистрацию -> классы расширились и не изменились / классы расширились и/или изменились

Single Responsibility

Reasoning: Изменение кода в протестированном классе может привести к новым дефектам. Мы стремимся минимизировать причины, по которым класс может измениться. Чем больше обязанностей у класса, тем больше причин по которым он должен будет измениться.

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

· изменения в доменной логике

· изменения в XML формате

Схема:

Рефакторинг:

Идея Мы разделяем обязанности на несколько классов по концептуальной нагрузке

Метрика => обязанности / класс

INTERFACE SEGRETATION

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

Схема:

Рефакторинг:

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

Метрика If type T exposes N methods, and client C uses n of them, then T`s interface is n/N specific with respect to C.

Среднее n/N для всех клиентов

DEPENDENCY INVERTION

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

Схема:

Рефакторинг: (супер тип и подтипы)

Идея Появился общий супер тип, который насчитывает три варианта подтипа.

Метрика зависимости от абстракций / общее количество зависимостей

Вопрос: опять же метрика применяется для случая до рефакторинга?

LISKOV SUBSTITUTION

Reasoning:

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

Схема:

На схеме видно, что предварительное условие для класса Settlement Account было нарушено

Рефакторинг:

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

Метрика каждый класс должен пройти все модульные тесты (юнит тесты) для всех его супер типов

LAW OF DEMENTER (ЗАКОН ДЕМЕНТРЫ)

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

Схема:

Рефакторинг:

Идея: В классе Account добавился новый метод isHolderMonitored(), в котором происходит дальнейшее взаимодействие с соседом класса - customer (вызов его метода), для класса Funds Transfer теперь возможно работать только со своим соседом и избавиться от "долгой навигации".

Метрика средняя глубина навигации

REUSED ABSTRACTIONS

Reasoning: абстракции должны быть расширяемы / реализованы более чем одним классом.

смысл indirection

Схема:

Метрика Для абстрактного класса или интерфейса T, который расширен или реализован N классами или интерфейсами, должно соблюдаться условие: N > 1

PACKAGE DESIGN

Cohesion

· Release-Reuse Equivalency (Эквивалентность повторного использования / выпуска)

· Common Closure (Общая закрытость)

· Common Reuse (Общее переиспользование)

Coupling

· Acyclic Dependencies (Нециклические зависимости)

· Stable Dependencies (Стабильные зависимости)

· Stable Abstractions (Стабильные абстракции)

Release-Reuse Equivalency

Reasoning: Когда разработчикам необходимо повторно использовать класс, они не хотят перекомпилировать свой код каждый раз, когда класс будет изменяться. Должен существовать управляемый процесс выпуска (релиза) через который класс можно будет повторно использовать. В .NET такой модуль выпуска (релиза) - это сборка, таким образом, модуль повторного использования = сборке. Потому что классы, которые сильно зависят друг от друга - и поэтому будут повторно использованы вместе - должны быть упакованный в одну сборку.

Смысл: Модуль повторного использования = модуль выпуска (релиза)

Схема:

Common Closure

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

Смысл: Классы, которые совместно изменяются, принадлежат друг другу.

Схема:

Common Reuse

Reasoning: Когда пакеты сильно связаны друг с другом, тогда зависимость от пакета означает зависимость от каждого класса в этом пакете.

Смысл: Классы, которые не используются повторно вместе, друг другу не принадлежат.

Схема:

Package Cohesion Metrics

Reasoning: Класс C прямо или косвенно зависит от N классов в одном пакете P. Всего в пакете М классов. Общее повторное использование & общее замыкание относительно C равно N / (M - 1). Сцепление пакета для P - это среднее число N / (M - 1) по всем классам в P. Кроме случая, когда M <= 1, в этом случае сцепление пакета - ноль (в противоположность 0 / (1 - 1), который был бы не определен!)

Схема:

Acyclic Dependencies

Reasoning: Чтобы создать и выпустить пакет, вначале необходимо создать и выпустить пакеты, от которых он зависит. Если так или иначе пакет зависит косвенно от себя, то следует потенциально создать намного более длинную сборку (build) и релизный цикл.

Смысл: Пакеты не должны косвенно зависеть от себя

Схема:

Рефакторинг:

Идея Каждая структура пакета должна быть Направленным Нециклическим Графом

Метрика Количество циклов в пакете графа не должно быть > 0

Stable Dependencies

Reasoning: Существует две причины для изменения кода в пакете.

  1. Поскольку мы хотим внести изменения (из-за изменений в логике / дизайне)

  2. Изменения в другом пакете вынуждают

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

КЛАССЫ: если говорить о стабильности классов, мы вносим изменения в классы, если

  1. Мы меняем контракт взаимодействия классов
  2. Логика меняется в определенных функциях класса

Класс будет называться стабильным, в случае, когда изменения в классе наименее вероятны.

Если у класса нет зависимостей, он будет называться не стабильным, поскольку он может быть изменен в любой момент.

Схема:

Метрика

Stable Abstractions

Reasoning: Должно ли программное обеспечение быть стабильным? Если наша цель - простое изменение, то полностью устойчивый пакет будет представлять проблему. Принцип открытости-закрытости предлагает лазейку: устойчивый пакет может быть расширен. Преобразуя стабильный пакет в абстрактный, он может быть легко расширяемыми менее устойчивыми пакетами, которые проще изменить. Это - Инверсия Зависимостей на уровне пакета.

Пакеты должны зависеть от более абстрактных пакетов

Схема:

Метрика (аналогия с классами)

Абстрактность A = абстрактные типы / все типы в пакете

Пакет X зависит от набора пакетов S

Количество пакетов P в S где абстрактность Р - абстрактности X > 0 / общее количество пакетов в S

Abstractness vs. Instability - Metrics

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

Расшифровка:

Интерфейс будет стабильным, если на него возложено мало ответственностей (single-responsibility).

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

Если базовый класс не корректно выделен, необходим интерфейс (без реализации).

В случае тестирования -> А - не тестируем, поскольку ему без разницы как реализован интерфейс.

При использовании интерфейсов, нам становится проще локализовывать дефекты.

  • Существует одновременно с этим необходимость в интеграционном тестировании (между оттестированными модулями).

Схема:

results matching ""

    No results matching ""