Всем привет. ( Поправил статью, постарался учесть комментарии.)
Хочу рассказать о своем опыте импортозамещения UI.
Не предлагаю ничего нового, просто известный подход, приложенный к конкретной ситуации.
Совсем кратко задачу можно описать так:
Есть: Исходный проект с множеством таблиц и форм.
Задача: Нужно в целевом проекте создать аналогичные таблицы и формы по другой технологии. По сути много однотипных классов и файлов, отличающихся заранее известными частями.
Решение:
Автоматически собрать необходимую информацию из исходного проекта в файл
Дополнить этот файл в той части которую не получилось собрать автоматически
Cоздавать нужные файлы по шаблону заменяя подстановочные символы в шаблоне значениями из файла. Полученные файлы могут быть если необходимо доработаны
Первое что приходит на ум при решении такой задачи — это использовать встроенный в VisualStudio кодогенератор, он реализует похожую задачу. Я изначально рассматривал использование его. Но передумал. Получается много сложнее.
Очевидные для меня плюсы такого подхода:
На порядок быстрее
Код максимально однообразен
Код может быть покрыт автоматически созданными тестами
В случае внесения в шаблон изменений, а это обязательно случится и не раз, можно быстро пересоздать код.(если шаблон доработан, то используя систему контроля версий можно быстро поправить как надо).
Теперь подробнее.
Я несколько лет участвовал в разработке Web приложения на net framework 4.7 слой UI которого построен на Devexpress. В один прекрасный момент поступила команда импортозаместить приложение. Собственно, импортозамещение можно разделить на 3 части.
Смена базы (отдельная задача, сейчас не о ней)
Смена framework 4.7 на .NET (выполняется достаточно просто с помощью upgrade-assistant)
-
Смена UI (нужно уйти от Devexpress, так как текущая версия не поддерживает Core, а новую увы нам не продадут)
UI в новом приложении решено было сделать на MVC, в для того что бы уменьшить различия между новым и старым кодом. По крайней мере контроллеры и расположение файлов cshtml остается прежним. Это значительно упростило мою задачу. В качестве нового слоя UI было принято решение использовать чистый Vue + библиотека для отображения таблиц. В целях упрощения отказались от идеи использовать SPA. Каждая странница должна загружаться отдельно. Так же в целях упрощения было принято решение отказаться от сборщиков и загружать js библиотеки через cdn. (о плюсах и минусах такого решения рад буду услышать в комментариях.)
Значительная часть нашего исходного Web приложения — это табличные формы и относительно простые карточки к ним. Их много, более 70, написаны в разное время и разными людьми, и естественно по-разному, что в них точно одинаково так это использование таблиц Devexpress.
Что бы сделать новое приложение максимально однообразным мне было поручено разработать образцы кода, которые будет использоваться в новом приложении. Эти образцы были согласованы со всеми разработчиками.
При работе над образцами собственно и родилась идея создавать код автоматически. Код для табличной части в 90% случаев может быть оставлен без изменений, изменения если и требуются, то это добавление кнопок для каких то специфичных действий и полей в дополнительный фильтр. Форму редактирования практически всегда придётся дорабатывать. Для формы создается заготовка с полями, которые найдены в модели, если в таблице найдены аналогичные поля, то называю их так же.
Стенд (прототип) на котором я обкатывал это решение я опубликовал на GitHub:https://github.com/SergiyShest/DevExpressToDevExtremeMigrate .
На всякий случай скажу, что весь этот код написан в личное время по личной инициативе и не является ни чьей собственностью кроме меня. Шаблоны, по которым сделано новое приложение так же отличаются. (К примеру, пришлось использовать Vue 2 так, как в компании могут использоваться браузеры, не поддерживающие JavaScript module).
В прототипе 3 приложения:
Source: Файлы исходного приложения на net framework 4.7 + MVC и Devexpress.Приложение не собирается потому, что я удалил все лишнее и оставил только необходимые файлы. Когда-то я нашел это приложение как пример для изучения Devexpress.
Target: Целевое приложение на .NET MVC. Изначально создано по шаблону в VisualStudio. В него добавлен проект Core в котором в папке Entity помещены те же файлы Entity что и в исходном приложении. Что бы в не заморачиваться с базой данных там же расположен класс TestDataHelper который возвращает случайным образом заполненный IQueryable<T> имитируя работу с базой. В проект UI добавлены базовые Generic контроллеры и необходимые js файлы.
CodeGenerator: Генератор который на основании Devexpress таблиц в исходном проекте создает аналогичные таблицы в целевом проекте, но уже по другой технологии (Devextreme). Шаблоны так же находятся в этом проекте.
Для ускорения разработки и гибкости генератор управляется запуском тестов. Это удобно для программ, которые выполняются только разработчиком.
Тесты для управления отделены от тестов, которые я использовал при разработке и вынесены в отдельный класс GeneratorCommand:
CollectInfo(): Изначальный сбор информации в файл
GenerateAll(): Генерация всего кода.
GeneratePart(): Генерация кода для отдельной таблицы.
Предусмотрены так же есть флаги, которые позволяют пропускать создание любого из файлов.
Предполагаемый алгоритм работы:
Разместить нужное исходное приложение на месте проекта Source или/и поправить пути в файлах CodeGeneration.cs и InfoCollector.cs.
Разместить целевое приложение на месте проекта Target или/и поправить пути в файлах CodeGeneration.cs и InfoCollector.cs.
В целевом приложении должны быть все Entity из исходного приложения по которым предполагается создавать формы.
Собрать данные запустив тест GeneratorCommand.CollectInfo() логика работы приблизительно следующая:
a) В исходном проекте UI ищутся все файлы по шаблону _*grid*.cshtml. В этих файлах содержится описание колонок таблицы и обычно есть модель, по которой можно понять какой класс Entity биндится с вьюхой или таблицей в базе данных. (иногда эту информацию приходится вытаскивать из контроллера).
b) Ищу класс Entity на для того что бы взять описания полей для формы редактирования (что бы поиск был быстрее необходимо обязательно установить свойство EntityPath в классе InfoCollector)
c) Сохраняю часть собранной информации в файл (collector.json). Счел более удобным не сохранять информацию о колонках и полях формы, расположении файлов и. т. п. Эту информацию редактировать мне не возникла необходимость, без нее collector.json получается более кратким.
5. Дополнить или отредактировать собранную информацию.
a) Так как из-за разнородности исходного кода не для всех значений можно получить правильные названия модели и т.п., то файл нужно дополнить руками. К примеру CardEntityName всегда соответствует Entity табличной формы что всегда не так. Не найдены заголовки форм (карточек в нашей терминологии)
b) Объекты в файле json для которых нет необходимости в автогенерации нужно удалить(закомментировать) или установить признак AlwaysSkip.
c) Отредактированный файл нужно разместить в СodeGenerator\templates\collector.json (Я специально создаю его в другом месте, что бы исключить возможность перезатереть при запуске тестов
6. Получить результирующий код выполнив тест GeneratorCommand.GenerateAll() логика работы приблизительно следующая:
a) Читается файл collector.json.
b) По каждому объекту в файле собирается дополнительная информация (состав колонок и поля формы)
c) Код нужных классов создается по шаблону. На каждый объект следующие классы:
i) Контроллер табличной формы
ii) Вью табличной формы
iii) Контроллер формы
iv) Вью формы
v) Js тест
vi) Для удобства отладки формируется так же меню.
e) В случае необходимости предусмотрена возможность отключить генерацию любого из классов в списке выше.
Файлы контроллеров сохраняются в том же месте и с тем же именем как в исходном приложении, Файлы представлений всегда Index.cshtml. (спасибо, что мы выбрали MVC)
7. Отладка приложения. Здесь все стандартно.
8. И наконец моя любимая тема, так сказать вишенка на торте ТЕСТЫ.
Для тестирования я решил использовать Cypress (чем мне нравится cypress так тем, что дает стабильные результаты).
В демонстрационном примере приведены простейшие тесты на табличную форму своего рода Smoke Tests. Тесты формируется по шаблону.
describe('Тест журнала $MainHeader$', () => {
it('Проверка наличия заголовка', () => {
cy.visit('https://localhost:7210/$HtmlRequestPath$')
cy.contains('H1', '$MainHeader$')
})
it('проверка фильра на наличие записей', () => {
cy.visit('https://localhost:7210/$HtmlRequestPath$')
cy.get('#d1From').type('1023-12-20').blur();
cy.get('#d1To').type('2024-12-31').blur();
cy.get('#findButton').click();
cy.wait(1000);
cy.get('.dx-info').invoke('text').then((text) => {
// регулярное выражение для извлечения числа из текста Page 1 of 1 (0 items)
const matches = text.match(/\((\d+)/);
// Проверяем, что удалось извлечь число и оно больше 0
const extractedNumber = matches && parseInt(matches[1], 10);
cy.wrap(extractedNumber).should('be.gt', 0);
})
})
it('Проверка фильтра на отсутствие записей', () => {
cy.visit('https://localhost:7210/$HtmlRequestPath$')
cy.get('#d1From').type('2000-12-31').blur();//unreal date tnere no records
cy.get('#d1To').type('2000-12-31').blur();
cy.get('#findButton').click();
cy.wait(1000);
cy.get('.dx-info').invoke('text').then((text) => {
//регулярное выражение для извлечения числа из текста Page 1 of 1 (0 items)
const matches = text.match(/\((\d+)/);
// Проверяем, что удалось извлечь число и оно 0
const extractedNumber = matches && parseInt(matches[1], 10);
cy.wrap(extractedNumber).should('eq', 0);
})
})
})
· В логике тестов используется особенность нашего приложения, что большинство таблиц имеет дополнительный фильтр по датам. Это позволяет задать значения фильтра таким образом, чтобы получить пустую таблицу. Для тех таблиц для которых даты нет, текст теста нужно поправить в процессе тестирования (по-хорошему нужно конечно автоматом создавать чуть другой тест для таких случаев, но руки не дошли).
Тесты можно запустить из Visual Studio через Test Explorer. Выглядит это так.
В процессе отладки я запускал тесты непосредственно через Cypress. На каком-то этапе это необходимо, но мне не нравится необходимость менять контекст (инструмент) в процессе работы. Кроме того, запуск тестов из Visual Studio через механизм тестов теоретически дает возможность провести какие-то манипуляции с базой, или что значительно красивее запустить приложение с специальным конфигом что бы загрузился мокнутый адаптер работы с базой.
Для обеспечения возможности использовать NUnit для запуска js тестов пришлось написать не большой адаптер. Возможно здесь я изобрел велосипед, но велосипед простой, работает надежно.
В реальном приложении тесты написаны так же на формы Там пришлось учесть еще необходимость авторизации. Для авторизации я выбран максимально простой подход, то есть перед каждым тестом поднимется форма авторизации и вводятся данные тестового пользователя. С этой задачей Cypress справляется легко.
Еще раз напоминаю что весь код доступен по адресу https://github.com/SergiyShest/DevExpressToDevExtremeMigrate
P.S. Хотел бы в комментариях услышать критику решения, а не только стиля статьи.( Одна из целей написания статьи для меня в том, что бы оценить правильность предложенного решения. Любая критика приветствуется, но к сожалению критики по существу статьи я так и не увидел).
Комментарии (21)
Breathe_the_pressure
15.03.2024 04:25+11Что-то я логику не понимаю, Devexpress - вражеский продукт. Ну ок, хозяин-барин.
А .NET, Visual studio, Github, я даже боюсь произносить слово Windows. Это уже всё наше стало?
Vladmk
15.03.2024 04:25+1Использовать .NET, Github и Visual Studio Code можно. Продукты пока еще доступные в РФ. Хотя техническую поддержку можно получить только от сообщества. Что касается ОС Windows, то перечисленные решения к ней не привязаны. Но в перспективе возможно стоит присмотреться к java стеку. Там есть Axiom.
Breathe_the_pressure
15.03.2024 04:25+3Т.е. пока какие-то продукты можно использовать, они не являются вражескими?
AgentFire
15.03.2024 04:25Конечно, если .NET обозвать вражеским и отменить, то у нас всё в каменный век откатится :)
SergejSh Автор
15.03.2024 04:25Решение об импортозамещении и стратегия этого действа как наверное догадались не мое. Про Windows вы угадали на 100%. Однозначно замена на Linux. К счастью на верху хватило ума не отказаться от С#.
Atreides07
15.03.2024 04:25+9Искренне Вам сочувствую. Если вы воспринимаете замену DevExpress не как замену ради снижения рисков (что может быть вполне объективно оправдано), а как вражеский продукт от которого надо избавиться, то представляю какого Вам, когда пользуетесь компьютером, мобильником, интернетом, стиралкой, машиной и даже телевизором и т.д. (С учетом того что используете термин "Вражеский" наверняка Вы потребляете много контента из зомбоящика). Очень Вам сочувствую, тяжело так жить наверное. С другой стороны, обычно, люди потребляющие телевизор, не сильно обременены знаниями из каких компонентов состоят даже отечественные продукты с электроникой
SergejSh Автор
15.03.2024 04:25+1Вы как то близко к сердцу восприняли эту фразу. Я писал её с иронией. Использовать Devexpress далее мы не можем потому что у нас не закуплена версия под Core. И её нам не продадут.
Soapheart
15.03.2024 04:25+1Мне кажется, если бы обрамили данный термин в кавычки, то воспринималось бы это легче.
Удачи в переписывании "вражеского" кода, кек
Vitimbo
15.03.2024 04:25Вы слишком категоричны. У нас с автором похожая ситуация, раньше мы могли спокойно использовать любые продукты для реализации наших проектов. Теперь, сверху спустили список разрешенных к использованию БД, ОС, брокеров и прочего. Между собой мы тоже иногда используем термин "вражеский", но, лично для меня, это просто удобнее, чем использовать полную формулировку про реестр разрешенного ПО и т.д. и т.п.
Ну, и мы, соответственно, вынуждены опираться на этот список, а не на личные предпочтения. Иначе наш продукт не будут даже рассматривать многие потенциальные клиенты, а у текущих могут возникнуть проблемы при использовании.
Так что, рамки сужаются и приходится как-то с этим жить.
Atreides07
15.03.2024 04:25+1Отнюдь. Я, как и многие в нашей стране, был вынужден пересмотреть выбор продуктов после событий с 2014 года. И это объективная вынужденная замена ради снижения рисков. Использование многих продуктов стало "рисокваным". Но термин "вражеский" очень сильно режет слух своим непрофессионализмом и оголтелой пропагандой, так как по работе ОЧЕНЬ много общаюсь с коллегами других стран и никакой вражды с их сторны не чувствю. И эта реальность совершенно не соответствует реальности из телевизора, которые охотно впитывают из телевизора многие наши сограждане оторванные от реального окружающего мира.
SergejSh Автор
15.03.2024 04:25Поверь я так же не считаю их врагами. И в голову не приходило что кого то это может задеть.
AlexDevFx
15.03.2024 04:25+1Можно было бы сделать статью гораздо интересней технически, если:
- Убрать пункт об импортозамещении
- Избавиться от сленга "разрабы", "вражеский" и пр.
- Добавить код, отражающий суть решения
Идея-то сама по себе интересная, но вот реализация...
Vladmk
Перед Вами ставили задачу отказаться от Devexpress (Смена UI, нужно уйти от Devexpress как вражеского продукта), но компоненты Devextreme это тот же Devexpress. Выходит задача не выполнена!?
SergejSh Автор
А кто сказал что результирующий шаблон в компании на Devextrime? Я использовал Devextreme как бесплатный пример. В реальном коде другая платная библиотека.
Vladmk
Можно уточнить какая именно?
SergejSh Автор
Не нужно. Я не могу влезть в голову нашим безопасникам. Вдруг это покажется им разглашением It структуры компании. Здесь демонстрация принципа, а не реальный код.
SergejSh Автор
На самом деле результат должен состоять в первую очередь в том, что сервер где публикуется приложение должен быть Linux. То есть замена 4.7 на .Net. Даже Oracle и Devextreme допустим.
Во вторую очередь все используемое ПО должно быть в реестре Российского ПО.