- PVSM.RU - https://www.pvsm.ru -
Меня всегда интересовала разработка многоразового и целостного кода. Но проблема многоразового кода останавливается на этапе переноса в другую инфраструктуру. Если приложение расширяется плагинами, то плагины пишутся под конкретное приложение. А что если вынести логику приложения в плагин (далее — модуль), а интерфейс приложения из управляющего звена превратить в управляемый модулем компонент. На мой взгляд, самая главная задача в подобном сценарии, упростить базовые интерфейсы до минимума и дать возможность переписать или расширить любой фрагмент всей инфраструктуры в отдельности. Если интересно, что вышло из идеи модульного кода, то добро пожаловать под кат.
Первое условие к предстоящей системе — возможность динамически расширять систему без необходимости рекомпиляции отдельных модулей. Это относится как к хосту, так и к модулям.
Любое звено решения (кроме базовых интерфейсов) может быть переписано и динамически интегрировано. В довесок к возможности расширения модулей интерфейсами, хотелось иметь возможность получать доступ в динамике к публичным методам, свойствам и событиям, которые доступны в любом модуле. Соответственно, все элементы класса реализующего базовый интерфейс IPlugin, которые помечены доступностью как public, должны быть видимы извне другими модулями.
Любой модуль, может изыматься и добавляться в инфраструктуру, но при этом, при решении заменить один модуль другим модулем, придётся реализовывать весь функционал удаляемого модуля. Т.е. Модули идентифицируются через атрибут AssemblyGuidAttribute, добавляемый автоматом при создании проекта. Поэтому 2 модуля с одним идентификатором не загрузятся
Каждый модуль должен быть легковесным, чтобы базовые интерфейсы не нуждались в постоянном обновлении, а при необходимости, модуль можно изъять из системы и встроить как обычную сборку в приложение через ссылку (Reference). Благо, CLR загружает зависящие сборки через ленивую загрузку (LazyLoad), так что нужда в сборках модульной инфраструктуры отпадает.
И последнее условие, система должна предоставлять поэтапное расширение функционала для разработчика, чтобы уровень вхождения был на достаточно низком уровне.
При этом, система должна автоматизировать рутинные задачи, которые повторяются от приложения к приложению. А именно:
В результате накопившихся решений и отдельных компонентов, работающих по единому принципу, было составлено общее видение всей инфраструктуры:
Для предоставления независимости разработки как от конкретного приложения, так и самими программами, появились следующие ключевые компоненты:
В результате этих требований сформировались следующие базовые сборки:
Для минимального функционирования системы, достаточно добавить ссылку на SAL.Core, а при необходимости реализовать или использовать расширения, добавить ссылку на соответствующий набор расширений интерфейсов. Либо самостоятельно расширить минимальный набор интерфейсов нужной абстракцией.
Во время запуска хоста, первым делом инициализируются встроенные в хост базовые модули, для загрузки настроек и внешних плагинов (LoaderProvider и SettingsProvider).
Сначала инициализируется провайдер плагинов, а затем провайдер настроек. Встроенный в хост загрузчик ищет все плагины в папке приложения и подписывается на событие поиска зависимых сборок. Затем, встроенный в хост провайдер настроек, подгружает настройки из XML файла, находящегося в профиле пользователя. Оба провайдера поддерживают иерархическую инфраструктуру наследования и при обнаружении очередного провайдера становятся родителями нового провайдера. Если провайдер не находит требуемые ресурсы, то запрос ресурсов адресуется родительскому провайдеру.
После завершения процесса инициализации всех провайдеров, происходит инициализация всех Kernel, а затем и оставшихся плагинов. В отличие от остальных модулей, Kernel плагины инициализируются в первую очередь, получая возможность подписаться на события загрузки остальных плагинов с возможностью отмены загрузки лишних плагинов.
Данное поведение может быть переписано в хостах, если необходимо соблюсти иерархию загрузки других типов плагинов. Сейчас думаю о выносе последовательности загрузки модулей в Kernel.
Стандартные LoaderProvider через рефлексию ищут все public классы, которые реализуют IPlugin и это не правильный подход. Дело в том, что если в коде идёт вызов конкретного класса или через рефлексию идёт обращение к конкретному классу, и этот класс не ссылается ни на какие сторонние сборки, то события AssemblyResolve [6] не произойдёт. Т.е., сборку можно изъять из модульной инфраструктуры и использовать как обычную сборку добавив на неё ссылку и необходимость в SAL.dll отпадёт. Но базовые провайдеры модулей, реализованы по принципу сканирования текущей папки и всех объектов сборки, поэтому событие AssemblyResolve на все ссылающиеся сборки произойдёт на момент загрузки модуля.
Для решения этой проблемы, я написал несколько вариантов простых загрузчиков [7], но с разным поведением. В некоторых требуется указать список сборок заранее, некоторые сканируют папки самостоятельно.
В дальнейшем, как один из вариантов решения данной задачи, можно использовать сборку PEReader [8], которая описана ниже.
Базовые интерфейсы и небольшие куски кода, реализуемые в абстрактных классах для упрощения разработки. В качестве самой минимальной версии фреймворка для основы, была выбрана версия .NET Framework v2.0. Выбор минимальной необходимой версии позволяет использовать базу на любых платформах поддерживающих эту версию фреймворка, а обратная совместимость (выбор рантайма при запуске) позволяет использовать основу до .NET Core (пока исключая).
В теории, базовые классы должны представлять из себя фундаментальную основу, позволяющие использовать их в любой ситуации. На практике же наверняка найдутся условия, для которых придётся их расширить. В этом случае весь код абстрактных классов можно переписать, а интерфейсы расширить собственной реализацией. Поэтому в этой сборке и находится самый минимум возможного кода.
На момент написания статьи единственным хостом, наследующим базовые интерфейсы, является хост для WinService приложений.
Этот набор базовых классов, который предоставляет основу для написания приложений на основе WinForms и WPF. В составе идут интерфейсы для работы с абстрактным меню, тулбаром и окнами.
С точки зрения расширения, хост как Add-In для Visual Studio расширяет интерфейсы SAL.Windows и дополняет специфичным для VS функционалом. Если зависимый плагин не находит ядра, взаимодействующего с Visual Studio, то он может продолжать работать с ограниченным функционалом.
Все написанные хосты, поддерживающие интерфейсы SAL.Core, автоматизируют следующий функционал:
На этих интерфейсах реализованы следующие хосты:
Логирование событий реализовано через стандартный System.Diagnostics.Trace. В хостах MDI, Dialog и WinService, listener прописанный в app.config'е, пытается отдать полученные события обратно в само приложение через Singleton, которые затем отображаются в окнах логов (Output или EventList) в зависимости от события. Для devenv.exe тоже присутствует возможность прописать trace listener в app.config'е, но в данном случае мы получим загрузку сборки хоста до загрузки его в качестве Add-In'а. Поэтому trace listener добавляется программно в коде (Отображает в VS Output ToolBar или модальным окном).
Написанная инфраструктура позволяет развиваться в направлении HTTP приложений, но для этого необходимо реализовать часть модулей, обеспечивающих как минимум аутентификацию, авторизацию и кеширования. Для приложения TTManager, которое описано ниже, был реализован свой собственный хост для WEB сервисов, который реализовал в себе весь необходимый функционал, но, увы, он сделан под конкретную задачу, а не как универсальное приложение.
Такой подход логирования и разбивания на отдельные модули, позволяет с лёгкостью выявить узкие моменты при запуске в новом окружении. Для примера, при разворачивании массива модулей на Windows 10, обнаружил, что загрузка, занимает времени намного больше, чем на других версиях ОС. Даже на моей старенькой машине с WinXP, загрузка 35 модулей выполняется максимум за 5 сек. Но на Win10 процесс загрузки одного единственного модуля занимал куда больше времени.
Благодаря независимой архитектуре, локализовать проблемный модуль удалось мгновенно. (В данном случае проблема была в использовании рантайма v2.0 под Windows 10).
Первая версия инфраструктуры появилась в 2009 году. Как для тестирования, так и для ускорения выполнения тривиальных задач по работе, накопилось большое количество разнообразных и независимых модулей, автоматизирующих разные задачи (Все картинки кликабельны, модули можно скачать на страницах проекта).
В основе этого приложения лежит приложение, идущее совместно с Visual Studio — WCF test client. На мой взгляд, в первоисточнике масса неудобных моментов. К моменту перехода на WCF у меня уже было написано много приложений на обычных WebService'ах. Изучив принципы работы самой программы через ILSpy, я решил расширить функциональность не только WCF, но и WS клиентов. В итоге, разобрав основную программу, я написал плагин со следующим расширенным функционалом:
Опять же, первоисточником программы стали программисты из M$. В основе программы лежит программа RDCMan [14], но, в отличие от основной программы, я решил встроить окно подключённого сервера в диалоговый интерфейс. А удалённое хранилище настроек, помогло держать список серверов у всех причастных коллег в актуальном состоянии.
В первоисточнике этого приложения лежит новая идея по автоматизации, которую я не смог найти в других приложениях. Цели написания такого приложения было 3:
Несколько раз получалось, что исполняемые файлы реализовывали некий функционал, но этот функционал либо устаревал, либо никем не использовался. Чтобы не искать по исходным кодам приложений на разных языках использование тех или иных объектов и написано это приложение. Для примера, у меня есть сборка в общем репозитории и я решил удалить из этой сборки один метод. Как узнать, используется ли этот метод в текущих зависимых сборка других проектов написанными коллегами? Можно попросить проверить всех исходный код, можно посмотреть поискать в Source Control, а можно просто поискать одноимённый метод внутри скомпилированных сборок. Оно состоит из 2х компонентов:
Для поиска по иерархии PE, DEX, ELF и ByteCode файлов, был написан отдельный модуль, который замечательно вписался в инфраструктуру: ReflectionSearch [17]. В данный модуль была вынесена вся логика поиска по объектам через рефлексию и благодаря нескольким публичным методам в модулях чтения исполняемых программ, удалось добиться многоразовости кода.
Чтобы не описывать каждым отдельным пунктом весь список готовых модулей, я опишу оставшиеся модули одним списком:
Остальные тут [40] (Всего выложено около 30 модулей). Изображения всех модулей тут [41].
Для наглядного демонстрирования удобств построения всего комплекса на модульной архитектуре, я приведу пару готовых решений построенных на разных принципах:
Приложение для системы задач, которое в основе использовало систему динамического расширения с возможностью использования разных источников задач. В итоге получился унифицированный интерфейс, который способен создавать, экспортировать/импортировать, просматривать задачи из разных источников. На текущий момент поддерживает в качестве источника MSSQL, WebService и частично REST API задач Мегаплана (не реклама). WebService написан по аналогичному принципу, с использованием базовых классов SAL.Web. Так что сам WebService также могут использовать в качестве источника MSSQL, Мегаплан или опять WebService.
Kernel плагин приложения, ленивой загрузкой ищет все плагины источников задач (DAL). Если найдено несколько плагинов доступа к данным, то клиенту предлагается выбрать тот плагин, который он хочет использовать (Только в SAL.Windows, в хостах без пользовательского интерфейса — вылетит с ошибкой). Зависимые плагины получают доступ к выбранному DAL плагину через Kernel модуль.
В данном примере Kernel плагин абстрагирован интерфейсами от остальных зависимых плагинов. В таком случае, можно написать ещё один Kernel модуль (или переписать текущий). Или переписать вообще любой плагин) для возможности работать с несколькими источниками задач одновременно.
Для решения проблемы со статусами задач, внутри некоторых DAL плагинов зашита матрица статусов (Или берутся из источника задач, если есть). В таком случае не возникает проблем с переносом данных из одного источника в другой.
Приложение позволяет, используя готовые плагины, парсить сайты через Trident или WebRequest. Для парсинга доступно несколько уровней абстракции. Самый низкий уровень позволяет написать дополнительный плагин, который будет заниматься открытием и парсингом ответа, используя DOM или ответ от сервера. Уровень выше предлагает написать .NET код в рантайме, который через плагин “.NET Compiler” будет скомпилирован и применён к результату страницы, отображаемой в Trident'е в рантайме. Самый высокий уровень предполагает указание, через UI, элементов на странице сайта отображаемой в Trident’e. И после применения xpath (самописный вариант) шаблона, передать на обработку в универсальный плагин или выполнить .NET код из плагина ".NET Compiler".
Модулю, зависимому от Kernel плагина, предлагается выбрать один из готовых интерфейсов вывода и базовый пользовательский интерфейс скачивания данных. Либо Trident, либо WebRequest с возможностью логирования. Kernel предлагает не только интерфейс, но и таймер опрашивания каждого отдельного модуля.
Интерфейс вывода предлагает стандартный GridView с контейнером вывода данных, с возможностью сохранения последней открытой позиции в таблице. По умолчанию контейнер поддерживает отображение изображения или текстовых данных.
В данном случае я не стал абстрагироваться от Kernel плагина интерфейсами и все зависимые плагины ожидают найти в массиве подгруженных плагинов конкретный Kernel плагин.
Приложение писалось в 3 итерации (Только под SAL.Windows):
Хост EnvDTE проверен только на английских студиях. Могут возникнуть проблемы на локализованных версиях (Один раз испытал на VS11 с русской локализацией).
Хост EnvDTE закрывает студию, если подгружен плагин Winlogon (SENS) и пользователь решил выгрузить хост через Add-in Manager. (Встретил на Windows 10).
Т.к. Хост написан как Add-In, а не как полноценное расширение, то совместимости с другими продуктами на основе EnvDTE — нет.
При желании использовать функции кеширования, в довесок к встроенным классам System.Web.Caching.Cache и System.Runtime.Caching.MemoryCache, доступны удалённые кеши. Для примера, AppFabric. Написав базовый интерфейс клиента для кеширования, можно разработать массив модулей для каждого вида кеша и выбирать нужный модуль по необходимости (На момент публикации уже написаны, но не выложены).
Модули на момент написания могут подгружаться с файловой системы, с файловой системы в память и обновляться по сети, используя в качестве TOC XML файл. Дальнейшее развитие позволяет использовать в качестве хранилища не только с файловой системы, но и использовать nuget как хранилище или реализовать хост, который позволяет запускать модули удалённо.
Персонализация пользователя возможна как Roles, так и Claims. Но при использовании OpenId, OAuth, OpenId Connect, провайдеров существует огромное множество, при этом от каждого провайдера требуется получить System.Security.Principal.IIdentity (При использовании Roles based auth) или System.Security.Claims.ClaimsIdentity (При использовании Claims аутентификации). Соответственно, один раз написав клиента для LinedIn'а, можно его использовать в любом приложении без перекомпиляции.
При использовании очередей сообщений можно написать модуль и набор интерфейсов, который будет выполнять функции ServiceBus, а модули реализации конкретной очереди уже будут отвечать за получение и отправку сообщений.
Можно написать UI интерфейс динамического связывания публичных методов модулей, по аналогии с SSIS или BizTalk сервисами.
Автор: ZetaTetra
Источник [49]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/195279
Ссылки в тексте:
[1] Dialog: https://dkorablin.ru/project/Default.aspx?File=80
[2] MDI: https://dkorablin.ru/project/Default.aspx?File=79
[3] EnvDTE (Visual Studio Add-In): https://dkorablin.ru/project/Default.aspx?File=77
[4] Windows Service: https://dkorablin.ru/project/Default.aspx?File=101
[5] Готовые базовые сборки: https://dkorablin.ru/project/Default.aspx?File=76
[6] AssemblyResolve: https://msdn.microsoft.com/en-us/library/system.appdomain.assemblyresolve(v=vs.110).aspx
[7] несколько вариантов простых загрузчиков: https://dkorablin.ru/project/Default.aspx?tag=16
[8] PEReader: https://github.com/DKorablin/PEReader
[9] DockPanel Suite: http://dockpanelsuite.com
[10] Web Service/Windows Communication Foundation Test Client: https://dkorablin.ru/project/Default.aspx?File=96
[11] Image: https://habrastorage.org/files/127/532/ede/127532edef554b6099e21560c74c53e0.png
[12] RDP Client: https://dkorablin.ru/project/Default.aspx?File=90
[13] Image: https://habrastorage.org/files/c97/eb8/105/c97eb8105b6e445ea4bbf4b19310c71b.png
[14] RDCMan: https://www.microsoft.com/en-us/download/details.aspx?id=44989
[15] PE Info: https://dkorablin.ru/project/Default.aspx?File=89
[16] Image: https://habrastorage.org/files/a93/8ed/8c6/a938ed8c6b19458db370836ba2cffc95.png
[17] ReflectionSearch: https://dkorablin.ru/project/Default.aspx?File=110
[18] ELF Image Info: https://dkorablin.ru/project/Default.aspx?File=113
[19] ElfReader на GitHub: https://github.com/DKorablin/ElfReader
[20] ByteCode (.class) Info: https://dkorablin.ru/project/Default.aspx?File=112
[21] ByteCode Reader на GitHub: https://github.com/DKorablin/ByteCodeReader
[22] DEX (Davlik) Info: https://dkorablin.ru/project/Default.aspx?File=111
[23] DexReader на GitHub: https://github.com/DKorablin/DexReader
[24] .NET Compiler: https://dkorablin.ru/project/Default.aspx?File=102
[25] хостинга: https://www.reg.ru/?rlink=reflink-717
[26] Browser: https://dkorablin.ru/project/Default.aspx?File=86
[27] HtmlAgilityPack: https://htmlagilitypack.codeplex.com/
[28] Configuration: https://dkorablin.ru/project/Default.aspx?File=83
[29] Members: https://dkorablin.ru/project/Default.aspx?File=98
[30] DeviceInfo: https://dkorablin.ru/project/Default.aspx?File=97
[31] DeviceIOControl: https://msdn.microsoft.com/en-us/library/windows/desktop/aa363216(v=vs.85).aspx
[32] GitHub: https://github.com/DKorablin/DeviceIoControl
[33] Single Instance: https://dkorablin.ru/project/Default.aspx?File=84
[34] SQL Settings Provider: https://dkorablin.ru/project/Default.aspx?File=103
[35] SQL Assembly scripter: https://dkorablin.ru/project/Default.aspx?File=92
[36] Winlogon: https://dkorablin.ru/project/Default.aspx?File=93
[37] SENS: https://msdn.microsoft.com/en-us/library/windows/desktop/cc185680(v=vs.85).aspx
[38] EnvDTE.PublishCmd: https://dkorablin.ru/project/Default.aspx?File=91
[39] детально описал тут: https://habrahabr.ru/post/169529/
[40] тут: https://dkorablin.ru/project/Default.aspx?tag=14
[41] тут: https://dkorablin.ru/project/Gallery.aspx?Gallery=13
[42] Image: https://habrastorage.org/files/2a2/d51/037/2a2d51037e514f039c4d41f6cbe5b609.png
[43] Image: https://habrastorage.org/files/805/dae/618/805dae618b0a40fb9fb59d34f04b3a5f.png
[44] Image: https://habrastorage.org/files/f56/500/245/f56500245cd3457cb13cae68310d3fb3.png
[45] Image: https://habrastorage.org/files/82d/c59/677/82dc59677ab04fa6ba7d3eb1d755b4ee.png
[46] Image: https://habrastorage.org/files/00a/967/317/00a967317cc04dc49756eb8eacc402ea.png
[47] Image: https://habrastorage.org/files/d1f/b5a/118/d1fb5a118c7b426892b7909b2f3856a3.png
[48] HtmlAgilityPack: https://htmlagilitypack.codeplex.com
[49] Источник: https://habrahabr.ru/post/303032/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.