пятница, 26 октября 2012 г.

Фреймворки, библиотеки и зависимости

В одной из последних статей Ayende привел очень толковое определение разницы между библиотекой и фреймворком:

«Главное отличие библиотек от фреймворков в том, что фреймворк запускает ваш код, и, в общем случае, контролирует свое собственное окружение; в то время, как библиотека – это нечто, что вы используете из своего кода, контролируя свое окружение самостоятельно».

И это очень правильное определение. Фреймворк определяет окружение, используя «push» модель взаимодействия с приложением: в большинстве случаев фреймворк сам вызывает код приложения через механизм виртуальных методов, методы обратного вызова и т.п. Пользователю нужно лишь «вклинить» свой собственный код в «слоты», представленные фреймворком (не зря ведь в русскоязычном сообществе для термина “framework” используется «каркас»).

В случае с библиотекой ситуация иная. Приложение формирует окружение вокруг библиотеки и использует “pull” модель взаимодействия: приложение в нужные ему моменты времени вызывает код библиотеки, а не наоборот.

Фреймворк «рулит» кодом приложения, а в случае с библиотекой – код приложения «рулит» библиотекой.

image

Эта разница может показаться незначительной, однако, на самом деле, это не так. С точки зрения дизайна, фреймворк обычно значительно сильнее связан с приложением (“more tightly coupled”), нежели библиотека, а это значит, что любые изменения фреймворка или переход с одного фреймворка на другой, приведут к значительно более серьезным последствиям, нежели изменения библиотеки.

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

Применение MVx паттернов в контексте Windows Forms или WPF также служит и этой роли тоже: вью модели обычно не абстрагируют приложение полностью от UI технологии, но в значительной степени уменьшает эту зависимость, позволяя думать о доменной области приложения в один момент времени, и о визуальной стороне дела – в другой.

Конечно, такое абстрагирование не всегда нужно и/или возможно, и я не предлагаю «абстрагироваться» от используемого языка или технологии, ведь для этого придется написать свой собственный язык или технологиюJ Тем не менее, во многих случаях мы можем свести использование фреймворка или библиотеки к разумному минимуму.

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

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

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

ПРИМЕЧАНИЕ
Я очень рекомендую прочитать хотя бы первые несколько глав замечательной книги “Framework Design Guidelines”, чтобы получить общее впечатление о том, что такое «фреймворк» и с чем его едят.

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

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

Отступление от темы: управление зависимостями

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

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

image

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

Заключение

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

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

В следующий раз: поговорим об управлении зависимостями более детально.

Дополнительные ссылки

7 комментариев:

  1. Помните, что все разработчики достаточно похожи друг на друга, поэтому, чем СИЛЬНЕЕ вы связываете руки вашим потенциальным клиентам, тем МЕНЬШЕ людей будут гореть желанием воспользоваться вашим велосипедом и сядут за написание своего.

    Сереж, по-моему ты здесь ошибся. Чем сильнее связываем руки, тем БОЛЬШЕ людей сядут за написание велосипедов. Поправь, если не прав я.

    ОтветитьУдалить
  2. Я же это и имею ввиду: чем СИЛЬНЕЕ связаны руки, тем МЕНЬШЕ людей будут пользоваться ВАШИМ велосипедом.

    ОтветитьУдалить
  3. Да, ты прав. Перечитал вдумчиво. Так и есть.

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

    Что делать, если используется инъекция через конструкторы? Ведь инъекция через свойства часто не подходит по идеологическим соображениям: есть мнение, что property injection годится только для опциональных зависимостей.

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

    ОтветитьУдалить
  6. Не совсем понятно, как здесь помогут наблюдатели. Кое-где, конечно, можно применить push-модель и поджигать события, но ведь это связь односторонняя.

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

    Я, на самом деле, на старом добром С++ долгое время реализовывал наблюдатели с помощью интерфейсов ICustomClassObserver, аналогично тому, как мы выделяем зависимости в интерфейсы.

    ОтветитьУдалить