Модульна архітектура і багаторазовий код



Мене завжди цікавила розробка багаторазового і цілісного коду. Але проблема багаторазового коду зупиняється на етапі перенесення в іншу інфраструктуру. Якщо програма розширюється плагінами, то плагіни пишуться під конкретну програму. А що якщо винести логіку програми в плагін (далі — модуль), а інтерфейс програми з керуючої ланки перетворити на керований модулем компонент. На мій погляд, головна задача в подібному сценарії, спростити базові інтерфейси до мінімуму і дати можливість переписати або розширити будь-який фрагмент всієї інфраструктури окремо. Якщо цікаво, що вийшло з ідеї модульного коду, то ласкаво просимо під кат.

Ідея
Перша умова до майбутньої системи — можливість динамічно розширювати систему без необхідності перекомпіляції окремих модулів. Це відноситься як до хосту, так і до модулів.
Будь-ланка рішення (крім базових інтерфейсів) може бути переписано і динамічно інтегровано. На додаток до можливості розширення модулів інтерфейсами, хотілося мати можливість отримувати динамічний доступ до публічних методів, властивостей і подій, які доступні в будь-якому модулі. Відповідно, всі елементи класу реалізує базовий інтерфейс IPlugin, які позначені доступністю як public, повинні бути видимі ззовні іншими модулями.
Будь-модуль, може вилучатися і додаватися в інфраструктуру, але при цьому, при вирішенні замінити один модуль іншим модулем, доведеться реалізувати всю функціональність видаляється модуля. Тобто Модулі ідентифікуються через атрибут AssemblyGuidAttribute, що додається автоматом при створенні проекту. Тому 2 модуля з одним ідентифікатором не завантажаться
Кожен модуль повинен бути легковажним, щоб базові інтерфейси не мали потребу в постійному оновленні, а при необхідності, модуль можна вилучити з системи і вбудувати як звичайну збірку в додаток через посилання (Reference). Благо, CLR завантажує залежать складання через ліниву завантаження (LazyLoad), так що потреба в збірках модульної інфраструктури відпадає.
І остання умова, система повинна надавати поетапне раширение функціональності для розробника, щоб рівень входження був на досить низькому рівні.
При цьому, система повинна автоматизувати рутинні завдання, які повторюються від програми до програми. А саме:
  • Збереження/завантаження користувацьких налаштувань або загальне сховище налаштувань,
  • Збереження стану або інших параметрів, у залежності від застосування,
  • Перенесення раніше написаних компонентів,
  • Обмеження у використанні програмного забезпечення без достатнього рівня прав (Завантажувати компоненти від рівня доступу, а не приховувати елементи інтерфейсу),
  • Взаємодія з хмарної інфраструктурою без необхідності змінити логіку (Message Queue, REST, SOAP сервіси, Web sockets, Caching, OAuth/OpenId/OpenId Connect...)
Рішення
В результаті накопичених рішень і окремих компонентів, що працюють за єдиним принципом, було складено спільне бачення всієї інфраструктури:
  1. Мінімальні вимоги до основних інтерфейсів,
  2. Модульна інфраструктура з незалежним джерелом завантаження модулів,
  3. Загальне сховище налаштувань,
  4. Незалежність рішення від реалізації додатків (UI, Services):
    1. Які хости є на момент написання:
      • Dialog

      • MDI

      • EnvDTE (Visual Studio Add-In). [Не працює в Visual Studio 2015],
      • ASP.NET Компонент (Потребує доопрацювання → IHttpHandler, OwinMiddleware),
      • //dkorablin.ru/project/Default.aspx?File=76">Готові базові складання
      В результаті цих вимог сформувалися такі базові складання:
      • SAL.Core   Набір мінімальних необхідних інтерфейсів для хостів і модулів,
      • SAL.Windows   Залежить від SAL.Core. Набір інтерфейсів для хостів і модулів, які підтримують стандартний функціонал WinForms, WPF (Form, MenuBar, StatusBar, ToolBar...) додатків,
      • SAL.Web   Залежить від SAL.Core. Набір інтерфейсів для хоста і модулів, які підтримують програми, написані з використанням ASP.NET (Потребує кардинальної доопрацювання).
      • SAL.EnvDTE   Залежить від SAL.Windows. Надає розширення для плагінів, які можуть взаємодіяти з оболонкою, на якій написана Visual Studio.
      Для мінімального функціонування системи, досить додати посилання на БІЗ.Core, а при необхідності реалізувати або використовувати розширення, додати посилання на відповідний набір розширень інтерфейсів. Або самостійно розширити мінімальний набір інтерфейсів потрібної абстракцією.
      Під час запуску хоста, насамперед ініціалізуються вбудовані в хост базові модулі, для завантаження налаштувань і зовнішніх плагінів (LoaderProvider і SettingsProvider).
      Спочатку ініціалізується провайдер плагінів, а потім провайдер налаштувань. Вбудований в хост завантажувач шукає всі плагіни в папці програми і підписується на подію пошуку залежних збірок. Потім, вбудований в хост провайдер налаштувань, довантажує налаштування XML файлу, що знаходиться в профілі користувача. Обидва провайдера підтримують ієрархічну інфраструктуру спадкування і при виявленні чергового провайдера стають батьками нового провайдера. Якщо провайдер не знаходить необхідні ресурси, то запит ресурсів адресується батьківського провайдера.
      Після завершення процесу ініціалізації всіх провайдерів, відбувається ініціалізація всіх Kernel, а потім і решти плагінів. На відміну від інших модулів, Kernel плагіни ініціалізуються в першу чергу, отримуючи можливість підписатися на події завантаження інших плагінів з можливістю скасування завантаження зайвих плагінів.
      Дана поведінка може бути переписано у хостах, якщо необхідно дотримати ієрархію завантаження інших типів плагінів. Зараз думаю про винесення послідовності завантаження модулів Ядра.

      Завантаження збірок

      Стандартні LoaderProvider через рефлексію шукають все public класи, які реалізують IPlugin і це не правильний підхід. Справа в тому, що якщо в коді йде виклик конкретного класу або через рефлексію йде звернення до конкретного класу, і цей клас не посилається ні на які сторонні збірки, то події виконання в assemblyresolve не відбудеться. Тобто, збірку можна вилучити з модульної інфраструктури і використовувати як звичайну збірку додавши посилання на неї і необхідність в SAL.dll відпаде. Але базові провайдери модулів, реалізовані за принципом сканування поточної папки та всіх об'єктів збірки, тому подія виконання в assemblyresolve на всі посилаються складання відбудеться на момент завантаження модуля.
      Для вирішення цієї проблеми, я написав кілька варіантів простих завантажувачів, але з різним поведінкою. У деяких потрібно вказати список збірок заздалегідь, деякі сканують папки самостійно.
      В подальшому, як один з варіантів вирішення даної задачі, можна використовувати збірку PEReader, яка описана нижче.
      SAL.Core
      Базові інтерфейси і невеликі шматки коду, що реалізуються в абстрактних класах для спрощення розробки. В якості самої мінімальної версії фреймворку для основи, була обрана версія .NET Framework v2.0. Вибір мінімальної необхідної версії дозволяє використовувати базу на будь-яких платформах підтримують цю версію фреймворку, а зворотна сумісність (вибір рантайма при запуску) дозволяє використовувати основу .NET Core (поки виключаючи).
      В теорії, базові класи повинні представляти із себе фундаментальну основу, що дозволяють використовувати їх в будь-якій ситуації. На практиці ж напевно знайдуться умови, в яких прийдеться їх розширити. В цьому випадку весь код абстрактних класів можна переписати, а інтерфейси розширити власною реалізацією. Тому в цій збірці і знаходиться самий мінімум можливого коду.
      На момент написання статті єдиним хостом, які наслідують базові інтерфейси, є хост для WinService додатків.
      SAL.Wndows
      Цей набір базових класів, який надає основу для написання додатків на основі WinForms і WPF. У складі йдуть інтерфейси для роботи з абстрактним меню, тулбаром і вікнами.

      SAL.EnvDTE
      З точки зору розширення, хост як Add-In для Visual Studio розширює інтерфейси SAL.Windows і доповнює специфічним для VS функціоналом. Якщо залежний плагін не знаходить ядра, взаємодіючого з Visual Studio, то він може продовжувати працювати з обмеженим функціоналом.
      Всі написані хости, що підтримують інтерфейси SAL.Core, автоматизують наступний функціонал:
      • Завантаження плагінів з поточної папки,
      • Збереження і завантаження налаштувань плагінів з XML файлів в профілі користувача,
      • Відновлення позицій і розміру всіх раніше закритих вікон при відкритті програми (SAL.Windows).


      На ці інтерфейси реалізовані наступні хости:
      • Host MDI   Multiple Document Interface, написаний з використанням компонента DockPanel Suite
      • Host Dialog   Діалогових інтерфейс з контрольним управлінням Windows ToolBar,
      • Host EnvDTE   Add-In для Visual Studio, перевірений на версіях EnvDTE: 8,9,10,12.
      • Host Windows Service   Хост в якості віндового сервісу, з можливістю установки, видалення і запуску через параметри командного рядка (PowerShell не підтримується).
      Логування подій реалізовано через стандартний 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 році. Як для тестування, так і для прискорення виконання тривіальних завдань по роботі, накопичилася велика кількість різноманітних і незалежних модулів, що автоматизують різні завдання (Всі картинки клікабельні, модулі можна скачати на сторінках проекту).

      Web Service/Windows Communication Foundation Test Client


      В основі цієї програми лежить програма, що йде разом з Visual Studio — WCF test client. На мій погляд, в першоджерелі маса незручних моментів. До моменту переходу на WCF у мене вже було написано багато додатків на звичайних WebService'ах. Вивчивши принципи роботи самої програми через ILSpy, я вирішив розширити функціональність не тільки WCF, але і WS клієнтів. У підсумку, розібравши основну програму, я написав плагін з наступним розширеним функціоналом:
      1. Підтримка WebService додатків (крім Soap Header),
      2. Можливість тестування сервісу зі старими binding'ами (при відкритті не оновлює проксі-клас автоматом, а тільки за запитом з UI),
      3. Незалежність від Visual Studio (об'єднав залежні складання через ILMerge),
      4. Вигляд всіх доданих сервісів у вигляді дерева, а не робота тільки з одним сервісом,
      5. Функція пошуку по всіх вузлах дерева,
      6. На форму запиту сервісу додано таймер, щоб відстежувати витрачений час на повне виконання запиту,
      7. Додано відновлення відправлених параметрів при закритті і відкритті форми тесту або всього додатки,
      8. Додана можливість збереження і завантаження параметрів у файл по кнопці формі тесту методу.
      9. Додана можливість автозбереження і завантаження параметрів методу (Знадобиться модуль Plugin.Configuration → Auto save input values [False])
      10. Зламана можливість редагування .config файл через програму SvcConfigEditor.exe


      RDP Client


      Знову ж таки, першоджерелом програми стали програмісти з M$. В основі програми лежить програма RDCMan, але, на відміну від головної програми, я вирішив вбудувати вікно підключеного сервера в діалоговий інтерфейс. А віддалене сховище налаштувань, допомогло тримати список серверів у всіх причетних колег в актуальному стані.

      PE Info


      В першоджерелі цього додатка лежить нова ідея автоматизації, яку я не зміг знайти в інших додатках. Цілі написання такої програми було 3:
      1. Надати інтерфейс для перегляду вмісту файлу PE, включаючи більшість директорій і таблиць метаданих (Хоча висновок ресурсів RT_DIALOG істотно відрізняється від оригіналу).
      2. Пошук по структурі PE/CLI файлів
      3. Дати можливість завантаження файлу PE не тільки з файлової системи, але і через WinAPI функцію дзвінки на loadlibrary. У разі завантаження через дзвінки на loadlibrary, є шанс прочитати розпакований PE файл і не треба вираховувати RVA.
      Кілька разів виходило, що виконувані файли реалізовували якийсь функціонал, але цей функціонал або устаревал, або ніким не використовувався. Щоб не шукати за вихідними кодами програм на різних мовах використання тих чи інших об'єктів і написано це додаток. Для прикладу, у мене є збірка в загальному репозиторії і я вирішив видалити з цієї збірки один метод. Як дізнатися, чи використовується цей метод в поточних залежних збірка інших проектів написаними колегами? Можна попросити перевірити всіх вихідний код, можна подивитися пошукати в Source Control, а можна просто пошукати однойменний метод всередині скомпільованих збірок. Воно складається з 2-х компонентів:
      1. Збірка PEReader (написана без unsafe маркера), основу якої доступні на GitHub'е,
      2. Клієнтської частини, яка являє собою плагін для SAL інфраструктури, використовуючи рівень абстракції SAL.Windows.
      Для пошуку по ієрархії PE, DEX, ELF і ByteCode файлів, був написаний окремий модуль, який чудово вписався в інфраструктуру: ReflectionSearch. У даний модуль була винесена вся логіка пошуку по об'єктах через рефлексію і завдяки кільком публічним методів в модулях читання виконуваних програм, вдалося домогтися многоразовости коду.

      Інші

      Щоб не описувати кожним окремим пунктом весь список готових модулів, я опишу залишилися модулі одним списком:
      1. ELF Image Info   Розбирання ELF файлу за аналогією з PE Info. ElfReader на GitHub.
      2. ByteCode (.class) Info Розбирання JVM .class файлу. ByteCode Reader на GitHub
      3. DEX (Davlik) Info   Розбирання DEX формату, який використовується в Андройд додатках. DexReader на GitHub
      4. Reflection Search   Складання для пошуку по об'єктах через рефлексію. Раніше була в складі модуля PE Info, але з появою інших модулів, була перенесена в окремий модуль, використовуючи публічні методи PE, ELF, DEX і ByteCode модулів.
      5. .NET Compiler   Компілятор .NET коду в реальному часі в поточному AppDomain. Надає можливість написання коду (TextBox), хостингу скомпільованого додатки, кешування скомпільованого коду і зберігання скомпільованого коду як у вигляді окремої збірки (Використовується у другій ітерації автоматизації додатка HTTP Harvester [Описаний нижче]).
      6. Browser   Хостинг для Trident'а з розширеним функціоналом отримання XPath (самописний, на подобі HtmlAgilityPack до DOM елементів. (Використовується на третій ітерації автоматизації додатка HTTP Harvester [Описаний нижче]).
      7. Configuration   Користувальницький інтерфейс для редагування налаштувань плагінів, бо не всі налаштування доступні через UI при використанні SAL.Windows.
      8. Members   Відображення в UI public елементів плагінів, які доступні для виклику ззовні.
      9. DeviceInfo   Складання, здатна прочитати S. M. A. R. T. атрибути з сумісних пристроїв і працює без unsafe маркера. Для отримання всіх даних використовується WinAPI функція DeviceIOControl, вихідний код самої збірки доступний на GitHub'е.
      10. Single Instance   Обмеження додатки єдиним екземпляром (Обмін ключами здійснюється через .NET Remoting),
      11. SQL Settings Provider   Провайдер збереження і завантаження налаштувань з MSSQL. (код писався на ADO.NET і збережених процедурах з розмахом на уніфікацію, тому для окремих СУБД доведеться писати свої реалізації хранимок),
      12. SQL Assembly scripter   Створення Microsoft SQL Server скрипта .NET збірки для встановлення керованого коду в MSSQL (не перевірено на unsafe збірках),
      13. Winlogon   Модуль надає публічні події для SENS інтерфейсів. Перша версія використовувала Winlogon, але він більше не підтримується.
      14. EnvDTE.PublishCmd   Цей модуль я детально описав тут.
      15. EnvDTE.PublishSql   Перед або після ручної публікації выполненяет довільний SQL запит через ADO.NET із зазначенням шаблонних значень.
      Інші тут (Всього викладено близько 30 модулів). Зображення всіх модулів тут.
      Готові рішення
      Для наочної демонстрації зручностей побудови всього комплексу на модульній архітектурі, я наведу кілька готових рішень побудованих на різних принципах:
      • Повна незалежність модулів між собою
      • Часткова залежність від Kernel модуля

      TTManager


      Додаток для системи завдань, яке в основі використовувало систему динамічного розширення з можливістю використання різних джерел завдань. В результаті вийшов уніфікований інтерфейс, який здатний створювати, експортувати/імпортувати, переглядати завдання з різних джерел. На поточний момент підтримує в якості джерела MSSQL, WebService і частково REST API завдань Мегаплан (не реклама). WebService написаний за аналогічним принципом, з використанням базових класів SAL.Web. Так що сам WebService також можуть використовувати в якості джерела MSSQL, Мегаплан або знову WebService.
      Як працює
      Kernel плагін програми, ледачою завантаженням шукає всі плагіни джерел завдань (DAL). Якщо знайдено кілька плагінів доступу до даних, то клієнту пропонується вибрати той плагін, який він хоче використовувати (Тільки в SAL.Windows, в хостах без користувальницького інтерфейсу — вилетить з помилкою). Залежні плагіни отримують доступ до вибраного DAL плагіну через Kernel модуль.
      Цікаві моменти
      У цьому прикладі Kernel плагін абстрагирован інтерфейсами від інших залежних плагінів. В такому випадку, можна написати ще один Kernel модуль (або переписати поточний). Або переписати взагалі будь-плагін) для можливості працювати з декількома джерелами завдань одночасно.

      Для вирішення проблеми зі статусами завдань, всередині деяких DAL плагінів зашита матриця статусів (Або беруться з джерела завдань, якщо є). В такому випадку не виникає проблем з перенесенням даних з одного джерела в інший.

      HTTP Harvester



      Додаток дозволяє, використовуючи готові плагіни, парсити сайти через Trident або WebRequest. Для парсингу доступно кілька рівнів абстракції. Найнижчий рівень дозволяє написати додатковий плагін, який буде займатися відкриттям і парсингом відповіді, використовуючи DOM або відповідь від сервера. Рівень вище пропонує написати .NET код в рантайме, який через плагін “.NET Compiler" буде скомпільовано і застосований до результату сторінки, яка відображається в Trident'е в рантайме. Найвищий рівень передбачає вказівку, через UI, елементів на сторінці сайту відображається в Trident'e. І після застосування xpath (самописний варіант шаблону, передати на обробку в універсальний плагін або виконати .NET код плагіна ".NET Компілятор".
      Як працює
      Модулю, залежному від Kernel плагіна, пропонується вибрати один з готових інтерфейсів виводу і базовий інтерфейс скачування даних. Або Trident, або WebRequest з можливістю логування. Kernel пропонує не тільки інтерфейс, але і таймер опитування кожного окремого модуля.
      Інтерфейс виводу пропонує стандартний GridView з контейнером виведення даних, з можливістю збереження останньої відкритої позиції в таблиці. За замовчуванням контейнер підтримує відображення зображення або текстових даних.
      Цікаві моменти
      В даному випадку я не став абстрагуватися від Kernel плагіна інтерфейсами і всі залежні плагіни очікують знайти в масиві подгруженных плагінів конкретний Kernel плагін.
      Додаток писалося в 3 ітерації (Тільки під SAL.Windows):
      1. Зроблена можливість написати плагін використовуючи базові елементи управління і масив методів роботи з Trident описані в Kernel плагіні
      2. З'явилася можливість замінювати код плагіна використовуючи рантайм код генерується і редагований в Plugin.Compiler
      3. З'явилася можливість вказувати в Trient шлях до вузлів HTML через UI. В результаті для рантайм або онлайн коду віддається масив Ключ/Значення, де значенням є шлях до HTML-елемента(ам) на подобі реалізації в HtmlAgilityPack


      Що вже застаріло і видалено
      1. Видалений Host для Office 2010. Він був написаний виключно для можливості створювати з контекстного меню завдання для TTManager, але з-за великої кількості милиць і обмеженості можливостей, подальша підтримка виявилася недоцільною.
      2. Видалена можливість створення вікон у EnvDTE через ATL. До VS 2007 можливість створення вікон у студії була реалізована тільки через ATL і COM. Потім з'явилася можливість все робити через .NET.
      3. Застарів хост для EnvDTE реалізований як Add-In


      Відомі помилки
      Хост 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 сервісами.
      Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.