Шаблон Одиночка в реализации AOP

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

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

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

  • Проверка правильности шаблона с точки зрения дизайна. За это отвечает архитектурный фреймворк в составе PostSharp.
  • Легкая работа с многопоточными моделями. Это важно в контексте создания объекта Одиночки.
  • Легкая навигация и подсветка используемых аспектов в самой Visual Studio.

 

Общая информация

Для полноты картины, я считаю, что надо снова привести основные характеристики и назначение шаблона.

Намерение

Лучше, когда некоторые объекты представлены только в одном экземпляре, для того, чтобы избежать путаницы и хаоса. Например, единая бухгалтерская книга, правительство, спулер принтеров. Когда таких объектов больше одного, то могут и часто возникают неприятные ситуации, которые ведут к нарушению целостности данных, трудностям с понимании логики работы в run-time.

Суть

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

Реализация

Каноническая реализация говорит о следующих особенностях реализации шаблона:

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

Вот в целом и все нехитрые особенности реализации. В UML нотации шаблон выглядит следующим образом:

singleton

ООП реализация

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

Итак, классических реализаций этого шаблона для .NET есть несколько штук:

Двойная проверка при блокировке

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

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

Кстати, MSDN предлагает чуть другой вариант реализации класса, который с точки зрения JetBrains является неверным. И если почитать братьев Альбахари на предмет многопоточности, то можно вспомнить, что MemoryBarrier() предотвращает изменение порядка инструкций в целях оптимизации.

Статическая инициализация

Этот способ в оригинальном издании банды четырех указывается как не очень хороший из-за некоторой неопределенности в инициализации членов класса в С++. Однако в .NET такой проблемы нет и все будет хорошо. Вариант для .NET потокобезопасный.

Использования Lazy<T>

Это по сути своей синтаксический сахар и сокращение записи двойной проверки. Флаг LazyThreadSafetyMode.ExecutionAndPublication как раз и говорит о том, что инициализация будет проведена только одним потоком.

Полное собрание реализаций Singleton можно найти в блоге Джона Скита.

АОП реализация

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

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

Хуже всего то, что:

  • шаблон надо реализовывать каждый раз заново
  • шаблон внедряется в бизнес-логику

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

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

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

Суть

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

Реализация

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

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

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

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

Мы начнем рассмотрение кода с «низов», с самого ядра шаблона. Под ядром я подразумеваю правильную реализацию создания объекта.

Идея

Сначала у меня были мысли вынести в тело аспекта абсолютно все. Но посмотрев на код, я понял, что это было бы не удобно (и невозможно). Судите сами. Допустим, у нас уже есть реализованный аспект Singleton, который применяется к классу и содержит в себе все необходимые поля. PostSharp, к слову, может делать инъекции полей/методов/интерфейсов.

Код мог бы выглядеть примерно так (что не может корректно скомпилироваться):

Постоянно (да даже иногда) кастовать классы таким образом удовольствие ниже среднего. И так делать не хочется. От слова «совсем». Поэтому структуру доступа к экземпляру класса желательно все же сохранить. Т.е. можно оставить только статическое поле. Т.е. примерно так:

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

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

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

В любом случае отследить использование аспектов не составит труда с правильной подсветкой кода:

singleton_helps

На скриншоте видно, что даже для динамически подключаемых аспектов выводится подсветка (сплошная линия под методом) и показывается откуда и как пришел аспект. Ссылки динамические и сразу перенесут в нужное место в коде.

Ядро

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

Класс Single должен применятся к свойствам, поэтому он будет наследоваться от LocationInterceptionAspect. Все аспекты так же должны быть сериализуемыми. В качестве реализации можно использовать классическую модель с двойной проверкой. Или же применить аспект Synchronized из Thread Modeling Framework от PostSharp. Применение его дает эквивалент применения lock к каждому методу.

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

Метод OnGetValue() вызывается, когда обращаются к свойству, к которому был применен аспект. В нашем случае при таком вызове надо проверить наличие экземпляра класса и вернуть его, и если его нет, то создать. Агрументом метода является класс LocationInterceptionArgs который может дать много полезной информации об окружении. Например, можно узнать класс в котором содержится свойство. Это делается в 7й строке: args.Location.DeclaringType.

Дальше необходимо найти приватный конструктор без параметров и вызвать его.

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

Валидация

Сама по себе реализация шаблона уже есть, но это не все. В процессе реализации шаблона, программист может навесить атрибут на любое поле, и наша задача удостоверится, что поле определенного вида. Дальше могут быть проблемы с поиском приватного конструктора, его «вдруг» тоже может не оказаться. В общем много всяких неожиданных вещей может нас поджидать. Некоторые их них можно предусмотреть и проанализировать в момент компиляции. Дальше я покажу как это можно сделать для рассматриваемого случая. К счастью, Singleton достаточно простой и почти все случаи можно предусмотреть.

Начать можно так же с того, класса, который у нас уже есть – Single. На уровне свойства можно проверить, что оно:

  • статическое
  • возвращает тот же тип что и у класса

 

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

UPDATE

Конечно же я разобрался как это должно работать. Основной косяк был в ошибке в последней на тот момент сборке PostSharp 4.0.39. После обновления на 4.0.41 все стало работать как и предполагалось.

Но, необходимо модифицировать код следующим образом:

 Для определения позиции ошибки используется специальный статический класс MessageLocation. С помощью метода Of(), который принимает тип, различные определения свойств, конструкторов и так далее, можно точно указать на место ошибки.

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

Результат работы валидации:

validation_result

Далее идет валидация дизайна на уровне класса. Необходимо проверить, что все конструкторы приватные, что есть публичное статичное поле/свойство, которое вернет значение класса. Этим будет заниматься класс Singleton.

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

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

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

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

Внедрение аспектов

На сайте PostSharp внедрению аспектов посвящена отдельная статья. Но вкратце это выглядит следующим образом:

Этот метод находится в классе Singleton, который реализует интерфейс IAspectProvider. Этот интерфейс требует, чтобы был реализован метод ProvideAspects(). Основная идея состоит в том, что сначала находим точку применения аспекта, затем с помощью класса AspectInstance применяем необходимый аспект.

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

Первой строкой аргумент метода приводится к типу Type, так как аспект Singleton наследуется от TypeLevelAspect, а это уровень типов. Если бы это был LocationInterceptionAspect, то приведение к Type не сработало бы.

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

Выводы

В качестве кратких главных выводов можно сказать, что:

  • аспект можно реализовать с помощью АОП
  • логика полностью выносится в отдельный класс
  • можно написать исчерпывающие проверки на предмет дизайна

 

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

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

Большая часть кода, как и в любом приложении, приходится на описание различных валидаций. Так что их можно в расчет не брать. Тогда получается, что при единичной реализации с помощью АОП разница в строчках кода составляет 1-3 строки (зависит как считать =) ). При многократных реализациях выигрыш растет вместе с использованием Одиночек. Естественно, что с АОП мы получаем чистую бизнес-логику.

 

PDF версия: http://softblog.violet-tape.ru/pdf/AOP_Singleton.pdf

Код: https://github.com/VioletTape/GoF_PostSharp

 

 

 

 

Оставить комментарий