четверг, 2 февраля 2017 г.

"Контуры" сложности

Как понятие "сложность" может мигрировать из электрофизики
или почему наложение контуров сложности друг на друга создает много дополнительных проблем?

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

Что такое "модуль"? Модулем обычно называют логически независимую и практически самодостаточную часть системы. Например, функционалы печати и управления БД, в совершенстве, находятся в двух обособленных друг от друга модулях. Здесь уместна аналогия с компьютерным железом - модуль ОЗУ, модуль видеокарты, сетевой карты и т.д. Аналогия хороша потому, что отчетливо демонстрирует "независимость" компонентов друг от друга. Так, любой современный модуль ОЗУ, хоть и подчиняется множеству конвенций и контрактов проектирования, принятым на уровне "железных" производителей всего мира, но, тем не менее, является относительно независимой частью, с которой все остальное оборудование работает по принятому контракту, прося(отсылка к статье, предшествующей предыдущей) ОЗУ(или любой другой похожий модуль) через шину данных об осуществлении каких-либо операций. О деталях же думает сам аппаратный модуль на своей внутренней кухне.

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

Итак, что такое "контур сложности"? Предположим, у нас имеется некоторая сложная система, занимающаяся анализом вредоносного траффика.
Вот ее часть:


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

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

В процессе дальнейшего развития системы может произойти следующее:


или так:


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


Возникает логичный вопрос: каким размером должен обладать идеальный модуль(сколько атомарных классов)? К сожалению, я не в силах дать ответ на этот вопрос, потому что его нет. Все зависит от того, какого уровня абстракции в вашей системе принято называть атомарными. Если они слабо похожи на абстракции и с ними нельзя работать как с абстракциями(просить что-либо сделать), то единым модулем вскоре может стать и вся система! Что плохо. 

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

Ответ: следует использовать интерфейсное(абстрактное) проектирование, чтобы модули, зависимостей между которыми не устранить, превращались в "водопроводные узлы".

Иллюстрация исправления ситуации с зависимостями выше:


Теперь TrafficTable зависит от абстракции AbstractTraffic, а элемент ConcreteTraffic из другого модуля реализует эту абстракцию так, как удобно его внутренней кухне. В итоге все довольны.
Зависимый модуль(нижний) зависит от контракта, а реализующий(верхний) - реализует этот контракт. О контрактном программировании мы еще как-нибудь поговорим.

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



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


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


Удачных вам абстракций!
(ваш мастер абстракций)



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

Абстрактный код

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

В этом посте речь пойдет о важности абстракций при разработке сложных систем. Сперва небольшой оффтоп: зачем вообще нужны абстракции? Часто работать с конкретикой гораздо удобнее, чем с некой абстрактной сущностью, детали которой размазаны и не ясны до конца. Но на самом деле, нет ничего кроме абстракций как в окружающем мире, так и в разработке ПО(тем более в разработке ПО). Вопрос лишь в том, на каком уровне абстракции рассматривается конкретный предмет. Уровней абстракций может быть бесконечно много. Все это звучит слишком аБсТрАкТнО, так что пора привести несколько примеров. 

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

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

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



Вот, что позволяет сделать абстракция (класс, интерфейс - не важно):


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

Таким образом количество зависимостей становится равным не количеству всех стрелок(18) в иерархии, а только количеству стрелок от модуля к абстракциям(3). А сложность модуля(системы) и определяется именно количеством таких зависимостей.

Теперь модуль можно рассматривать обособленно от всей остальной системы, держа в уме лишь зависимости от абстракций:


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

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

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


среда, 25 января 2017 г.

Использование "черных ящиков" в борьбе со сложностью

Почему "черные ящики" - это хорошо.
Или в чем суть инкапсуляции.

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

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

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

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

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

Почему это плохо? 
Представьте себе: вы пришли в кабинет к стоматологу, который обладает всем необходимым для качественного лечения зуба. Но вместо того, чтобы садиться в удобное(хотя не всегда) кресло и не думать о том, что происходит в вашей ротовой полости, вы просите у стоматолога все инструменты, которые только можно запросить(через гет-методы), после чего стоматолог сам по себе(пошел пить чай), а вы с грудой инструментов и больным зубом - сами по себе.
Ситуация не из приятных. Было бы гораздо удобнее "попросить" стоматолога все сделать за вас, не вникая в детали его работы, ведь он в этой сфере мастер.

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

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

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