Ортогональность

Навеяно холиварами на RSDN…

Если вы читали книгу «Программист-прагматик» Эндрю Ханта и Дэйва Томаса, то наверное знаете о чем пойдет речь.

Ортогональность также называют Separation of Concerns или применением принципа “Разделяй и властвуй” в архитектуре программ, принцип SRP является отражением идеи ортогональности.

Сам термин заимствован из геометрии и обозначает что линии пересекаются под прямым углом (на самом деле из алгебры и обозначает что скалярное произведение векторов равно нулю). В программировании этим термином обозначают независимость.

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

Вы сами часто сталкивались с ортогональностью в разных проявлениях:  разделение модели и представления, отделение Data Access Layer от Business Logic Layer, разделение данных и алгоритмов их обработки в паттернах State, Strategy.

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

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

Использование IoC-контейнеров делает управление зависимостями и временем жизни объектов независимым от пользователей этих объектов. AOP ортогонализирует (ух какое слово) так называемую сквозную функциональность. Методу, результат которого кешируется, и вызывающему его коду абсолютно не надо знать что происходит кеширование и как оно происходит. Применение паттернов MVC, MVP, MVVM делает независимыми сам интерфейс, от логики работы с низлежащей моделью, и даже если модель переедет на другую машину в сети, то интерфейс править не придется. Это все примеры ортогональности.

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

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

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

Под конец лирического отступления хочу привести пример очень популярного способа нарушать ортогональность.

Доменная модель или почему Фаулер неправ.

Domain Model – очень популярный паттерн построения бизнес-логики в бизнес-приложении. Заключается как раз в объединении данных, хранимых чаще всего в БД, и методов бизнес-логики в одном объекте. Все это преподносится как соответствие модели предметной области из реального мира.

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

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

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

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

Заключение

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



Применение Enterprise Library

Все возможности Enterprise Library можно применять по-отдельности и радоваться жизни. Но если бы это была единственная возможность я бы не написал этот пост.

Магия начинается в сборке Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers.
В этой сборке в одноименном неймспейсе содержатся call handlerы и атрибуты для навешивания обработчиков на методы. Как использовать call handlerы я рассказывал в этом посте.

Кратко опишу доступные хендлеры:

  • AuthorizationCallHandler – задействует Security Application Block для авторизации при вызове перехваченного метода
  • CachingCallHandler – обработчик, кеширующий результаты вызова перехваченного метода средствами Caching Application Block. Может быть чрезвычайно полезным для веб-разработки.
  • ExceptionCallHandler – позволяет декларативно задавать стратегию обработки исключений в перехваченном методе. Стратегии задаются средствами Exception Handling Application Block.
  • LogCallHandler – задействует Logging Application Block для логгирования вызова перехваченного метода.
  • ValidationCallHandler – вызывает валидацию параметров метода. Параметры валидации должны быть заданы атрибутами или в конфигурации Validation Application Block.
  • PerformanceCounterCallHandler – обработчик, который занимается сбором информации о параметрах быстродействия перехваченного метода. Информация сохраняется в PreformanceCounterах. В самой сборке Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers есть Installer (который можно запустить через installutil), создающий необходимые  счетчики производительности.

Если интересует только runtime AOP, то можно использовать не полновесный IoC-контейнер Unity, а легковесный генератор прокси-классов из Policy Injection Application Block.

Кроме перехватчиков вызовов существует другой способ интеграции всех application blocks с контейнером Uinty. Практический каждый application block из состава enterprise library содержит класс – расширение Unity, которое контейнер возвращать объекты-фасады при запросе определенных типов.

Все расширения можно найти в основной сборке application block, в неймспейсе Configuration.Unity. Классы расширений называются ИмяБлокаExtension.

  • Для Logging Application Block расширение заставляет Unity возвращать фасад логгера по запросу типа LogWriter.
  • Для Exception Handling Application Block  основной тип фасада – ExceptionManager.
  • Для Caching Application Block контейнер возвращает реализации интерфейсов ICacheManager, IStorageEncryptionProvider, IBackingStore.
  • Для Security Application Block контейнер возвращает реализации интерфейсов IAuthorizationProvider, ISecurityCacheProvider.
  • Для Data Access Application Block контейнер возвращает экземпляры класса Database, аналогично DatabaseFactory.CreateDatabase. Можно получать как экземпляр по-умолчанию, так и именованный экземпляр.
  • Для Cryptography Application Block (который находится в сборке Microsoft.Practices.EnterpriseLibrary.Security.Cryptography) расширение заставляет Unity возвращать фасад по запросу типа CryptographyManager, а также реализации интерфейсов IHashProvider и ISymmetricCryptoProvider.

При использовании application blocks совместно с unity все настройки блоков надо задавать в файле конфигурации. Для этого в Enterpise Library есть удобная утилита.

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



Баг в Entity Framework

Обнаружил очень неприятный баг с Entity Framework.
ObjectContext грузит метаданные не при конструировании, а при первом обращении к методам загрузки данных. При этом вызывается this.MetadataWorkspace.LoadAssemblyForType((тип), Assembly.GetCallingAssembly());
Если вызывающая сборка (Assembly.GetCallingAssembly()) не содержит прямой или косвенной ссылки на сборку с метаданными, то загружены они не будут и любой метод чтения отвалится с непонятной ошибкой.

Я наткнулся на такую ситуацию когда вынес extension-метод для ObjecxtContext в отдельную сборку, которая не имела ссылки на сборку с моделью. Сам extension-метод вызывался для только что созданного контекста.

Workaround для этого такой: для только что созданного контекста вызвать context.MetadataWorkspace.LoadFromAssembly(context.GetType().Assembly);



Введение в Enterpise Library

Enterprise Library – библиотека от группы patterns & practicies Microsoft. Проект живет по адресу http://www.codeplex.com/entlib.

Enterprise Library состоит из набора компонент, называемых application blocks, каждый из которых решает определенную задачу, часто возникающую при разработке ПО. 

В состав Enterprise Library входят следующие блоки:

  • Unity Application Block  - IoC-контейнер Unity
  • Policy Injection Application Block – AOP времени выполнения
  • Validation Application Block – небольшой фреймворк для валидации
  • Logging Application Block – логгер
  • Exception Handling Application Block – фреймворк для создания политик обработки исключений в приложении
  • Caching Application Block – фреймворк для кеширования
  • Security Application Block – библиотека для авторизации, практически повторение ASP.NET Membership
  • Data Access Application Block – библиотека, упрощающая работу с ADO.NET, используется другими блоками
  • Cryptography Application Block – библиотека для упрощения работы с криптографическими функциями в .NET

Все эти блоки могут настраиваться через config-файл, для этого в составе Enterprise Library есть утилита упрощающая этот процесс.

Почти все блоки можно использовать по-отдельности, но основная сила Enterprise Library состоит в том, что все блоки можно подключить через Unity.



Фабрики объектов в Unity

В этом посте я описал как с помощью LifetimeManager можно научить Unity использовать фабрику для создания объектов.

На самом деле так делать не стоит. В составе Unity есть сборка Microsoft.Practices.Unity.StaticFactory.dll в которой находится расширение контейнера для использования фабрики объектов.

Регистрация фабрики происходит методом RegisterFactory, который принимает делегат FactoryDelegate. Этот делегат принимает параметром IUnityContainer, и возвращает object.

Пример

var r = new Random();
var container = new UnityContainer();
container
    .AddNewExtension<StaticFactoryExtension>()
    .Configure<StaticFactoryExtension>()
        .RegisterFactory<int>("random", c => r.Next());

Чтобы этим всем было удобнее пользоваться можно написать extension-методы

public static class UnityStaticFactoryExtensions
{
    private static IStaticFactoryConfiguration GetExtension(IUnityContainer container)
    {
        var ext = container.Configure<StaticFactoryExtension>();
        if (ext == null)
        {
            container.AddNewExtension<StaticFactoryExtension>();
            ext = container.Configure<StaticFactoryExtension>();
        }
        return ext;
    }

    public static IUnityContainer RegisterFactory<T>(
        this IUnityContainer container, 
        Func<IUnityContainer, T> factoryMethod)
    {
        GetExtension(container).RegisterFactory<T>(c => factoryMethod(c));
        return container;
    }

    public static IUnityContainer RegisterFactory<T>(
        this IUnityContainer container, 
        string name, 
        Func<IUnityContainer, T> factoryMethod)
    {
        GetExtension(container).RegisterFactory<T>(name, c => factoryMethod(c));
        return container;
    }
}

Тогда код примера, показанного выше можно записать гораздо короче

var r = new Random();
var container = new UnityContainer();
container.RegisterFactory("random", c => r.Next());