Эта статья будет в форме вопроса. Я опишу свою проблему, с которой столкнулся при написании сайта.
Начнем! Вы уже, возможно, знаете меня, а если нет, то советую прочитать про меня тут, и тут.
Как я написал свой интернет-магазин
До магазина, я писал программу для учёта зарплаты для сети магазинов мяса, которые работают с моим папой. Программка была самая простая, которая может быть. Писал на WinForms.
После этого я пытался переписать папину программу для автоматизации магазинов. Писал на WPF. Много чему научился, получил огромный опыт, но конечно же не написал и 1% того, что нужно было.
Потом тем же магазинам мяса захотелось интернет-магазин. Я и предложил написать. Потом захотел ещё один клиент, ещё один, и ещё.. Сейчас пишу магазин электротоваров.
Короче говоря, мой интернет-магазин написан на Blazor.
Когда я обещал написать его, я в душе не знал, что такое ASP.NET Core(Blazor). C# знал 2 месяца. Умел верстать сайты, немного знал CMS, немного умел работать с базами данных.
Также мне нужно было написать админку, где можно менять цены на продукты, добавлять категории и много чего еще. Ещё, на сайте автоматически заказ отправлялся на магазин по адресу в папину программу. Нужно было сделать возможность поменять магазин, который обслуживает заказ.
Решил для админки написать приложение на WPF, так как уже знал его, а на самом сайте мне тогда было сложно сделать.
Когда я писал для одного клиента, то было ещё все хорошо. Но уже скоро стали появляться проблемы.
Вот, например: для сети магазинов я делал справочник магазинов, где можно указывать название, загружать картинки, написать адрес и прочее. Это потом было отображено на старице на сайте. И также к этим магазинам нужно было привязать адреса, которые обслуживает точка.
Второй клиент был с одним магазином, и, естественно, зачем ему нужен справочник магазинов? Что делать?
Настройки - всему голова!
Да, я просто создавал настройку. Называл её, например, ShowShopsInMenuDesktop
. Хранил в виде key-value в таблице.
Такая же ситуация и на сайте. Для этого, кстати, и приставка Desktop к настройке.
Таких настроек у меня сейчас уже 111!
Я путаюсь в них, что-то, в них, поменять - достаточно проблематичная задача.
Почему так много? Очень много деталей, которые нужно настраивать, и их количество только увеличивается.
Как я пишу «под клиента»
Ну 200 настроек, и что с того? Сложность даже не в настройках, а в коде, где их нужно проверять.
Очень, очень много if-ов! Один if вложенный в if, который вложен ещё в один if!
Это сильно усложняет код. Писать и самое главное - понимать практически невозможно.
Как я это пытаюсь решить
Я нашёл решение, которым пользуюсь постоянно, и которое хоть как-то спасает меня.
Решение такое: Если много проверок и настроек - вынеси это в базу в нужном виде и генерируй в программе из базы.
Есть у меня меню. Можно задавать настройки для каждого пункта типа ShowItemInMenu
, а можно просто хранить пункты меню в таблице Menu и генерировать меню из базы.
Гибкости больше, все удобно и не засоряет настройки. Прекрасно!
Почему это не работает
Да, большинство настроек можно убрать и переместить в другую таблицу в нужном виде.
Но что делать, если это настройка, которая меняет алгоритм работы программы или настройка, которую нельзя никуда вынести.
У меня есть настройка CanUserBuyProducts
. Она отвечает за то, что человек может купить продукт, добавить его в корзину. Если она выключена, то пользователь сможет только смотреть продукты. Мое решение здесь не работает. И таких настроек тоже очень много, и даже если максимально вынести все правильно в базу, то проблема все равно останется.
И, вишенка на торте - разметка и стили. Менять разметку и стили компонента - обычное дело для меня. Каждый клиент хочет по-своему.
Поэтому стили у меня хранятся в css файлах, которые я могу менять. Никакого Sass, PostCSS и прочих предпроцессоров, только чистый CSS, только хардкор! Ладно, шутка)) Люблю чистый CSS.
Но хранение css в файлах лишает меня многих возможностей. Например, бандлинг.
А что с шаблоном компонента? А ничего... Добавляю десятки настроек, чтобы сделать элементарную вещь.
С приложением WPF еще сложнее. Настройка нужна на каждую мелочь. Нужно сделать фон отчета красным? Настройка! Убрать кнопку? Настройка!
Генерировать и хранить всю форму в базе со всеми параметрами и элементами на ней - это уже перебор, просто бред.
И что же делать?
Я провел месяцы в размышлениях и поисках решения. Но пока "стек" решений у меня остается прежним.
Писать отдельный сайт каждому клиенту каждый со своими багами и проблемами? Нет, спасибо.
Может сделать отдельную ветку каждому клиенту, а потом сливать их? Еще хуже!
Единственная статья на Хабре, которая поднимает похожий вопрос - это «Как мы дорабатываем продукт под конкретного клиента» от LsFusion. Я других не нашел.
В их статье они пришли к решению - модульность. Круто, они написали свой собственный язык программирования, а мне-то что делать?
Я думаю, что такая проблема возникает у многих. Как ее решить, вот в чем вопрос?
Ссылки на Github проект:
ShopWeb - сам сайт
ShopWebData - сущности
ShopWebApp - приложение
Комментарии (15)
euroUK
18.09.2021 21:26+4Тут есть два пути - SAAS решение, где делается максимально универсально и никаких доработок под клиента не делается (только фичи которые нужны всем). Да, никаких полей в отчете в красный цвет пока потребитель сам в конструкторе отчетов не сможет красить поля самостоятельно.
Либо делается инсталляция максимально заточенная под клиента. Но тогда тут нужно минимум абстракций. Если что-то можно сделать хардкодом - делаем хардкодом.
Можно конечно делать нечто среднее - универсальное ядро системы с базовым функционалом которое бранчуется под каждого клиента, но тут велика вероятность, что ветки разойдутся и поддерживать зоопарк версий будет очень больно.
Alexufo
19.09.2021 04:19+2согласен — взять любую облачную crm — не гибко? Закажи архитектуру у 16 летнего)
Milkweed
18.09.2021 23:06+1Много всего написали уже правильного, подскажу ещё как мы решали схожие проблемы.
Сталкивались с такой проблемой разработки под нескольких клиентов. Тоже изначально был путь отдельных настроек. Но тестировать и сопровождать при таком подходе очень больно. Пришли к решению через паттерн "стратегия". На этапе запуска приложения регистрируем необходимые реализации, при этом настройка только одна - "имя" клиента. Если инстанс приложения один под несколько клиентов разом, то можно реализации подхватывать во время запроса, в зависимости от того, от кого он прилетел.
По умолчанию можно делать дефолтную реализацию, которая подхватывается, если нет специфичной для клиента.
Таким образом все различающиеся части приложения можно выделять в отдельные кусочки и прятать за абстракцией. Отдельные реализации легко тестить. При тестировании внешних компонентов можно легко мокать или тестить целый контур с отдельной реализацией. Из минусов - абстракции усложняют код, но это гораздо удобнее кучи if'ов.
lair
18.09.2021 23:23+2Я думаю, что такая проблема возникает у многих. Как ее решить, вот в чем вопрос?
А нету общего решения.
Скажем, для внешнего вида решение — это скины и, когда их не хватает, шаблоны. А для поведения — стратегии. А для чего-то — "просто" настройки.
Или, если проекты сильно различаются, то общее ядро, которое протаскивается во все проекты как пакет, и дальше в каждом проекте свои доработки в точках расширения.
А общего универсального решения нет, надо на каждый конкретный случай смотреть.
Mox
19.09.2021 05:33Мне кажется что проблема возникает из-за смешения уровней работы с данными и представления
Тут правда несколько случаев, это может быть несколько "типовых" продуктов, с разным UI, потому что потребности ларька и потребности сети магазинов разные.
Дальше можно сделать общий core, может быть какие-то общие функции и кастомные варианты сайтов для каждого из типов клиентов. Тогда у вас получается полный контроль над UI каждого типа + хорошо протестированная база.
А UI лучше всего сегментировать на основе уже существующей базы клиентов. Может быть там 2 или 3 типа клиентов оказаться.
Вообще это достаточно частая ошибка начинающих - когда вместо разделения кода пытаются сделать один общий параметризованный.
И еще хорошо бы понять "какие клиенты точно не ваши", потому что универсальный комбайн всего, который бы подошел и ларьку у метро "ИП Смирнов" и сети Магнит - не сделать.
XaBoK
19.09.2021 11:05Если проект не большой, то решение довольно простое - разделение на базовое решение и клиента (платформа и расширения). Общий функционал становится базой продукта (вы переходите от проектальной разработки к продуктовой), а всё остальное пишется модулями под клиента (дополнениями). В вашем варианте создавать свой язык, конечно, не надо. Достаточно использовать DI. Инсталяция каждого клиента должна включать имплиментацию только этого клиента. Допустим с тем же Strategy, что рекомендовали выше.
mSnus
19.09.2021 12:11А может, сделать под каждого клиента отдельную ветку на GitHub? От if-ов избавляться везде, где можно, общий код вынести в мастер-ветку...
gameplayer55055
19.09.2021 13:25+5Пишешь на сисетке? Умей в ООП. Там все ООП.
Всякие фабрики, синглтоны, фасады, мосты, модули, миксины, декораторы
А вообще я советую для освоения паттерн PIDOR( Presenter Interactor Decorator Object Router), хороший паттерн, меня коллеги даже в его честь назвали
space2pacman
19.09.2021 20:36Начнем! Вы уже, возможно, знаете меня
Ого, да это же тот самый! Оставь автограф!
P.S. Надеюсь в следующей статье количество фотографий с вами будет больше.
SubarYan
21.09.2021 12:22+1Твоя проблема мне не кажется - проблемой. Чтобы построить грамотную архитектуру и проектировать сущности в БД, другими словами сделать дата-дизайн, нужен опыт.
Сам проходил похожие этапы, в итоге написал себе удобную CMS, и теперь проект любой сложности мне кажется простым. Вообщем обращайся, я помогу тебе разобраться со стеком технологий и архитектурой в твоих приложения.
ApeCoder
Откройте настройки в студии. Там их много но они разбиты на группы и ориентироваться проще. В коде их тоже можно разбить на классы
Попробуйте поискать в коде запахи и расправиться с ними по рецептам
Изучите c#. Например, null coalescing operators имели бы вам убрать один if из кода
Почитайте что-нибудь про дизайн. Ddd, mvc, mvvm
arthurlomakin Автор
Я думаю, что разбить настройки по группам и классам не сильно решает проблему, оно только ее оттягивает дальше.
За это спасибо, посмотрю.
В null coalescing operator выглядит он так себе. Он не отделяет блок {}, как if. Это еще хуже. Использую его только по назначению. А заменить if null coalescing операторами — это только вред
Сейчас как раз уже начинаю читать книги, статьи.
sshikov
>Сейчас как раз уже начинаю читать книги, статьи.
Ну, вообще говоря, чтение книг — это чуть ли не единственный реальный способ чему-то научиться (кроме способа набить все шишки самому на своем опыте, конечно). То факт, что вы уже умеете писать программы, совершенно не означает, что вы умеете проектировать хорошую архитектуру — тем более на все случаи жизни.
Как ниже уже резонно замечено, нет никакой серебряной пули, или универсального решения на все случаи жизни. Есть разные способы для решения разных проблем. То что вы описываете — по сути проблема «как построить универсальный софт, чтобы он решал разные задачи». Ну или хотя бы адаптировался легче. На эту тему написаны десятки книг. Куча аббревиатур, самая известная из которых наверное SOLID — они все про это же, как писать софт, чтобы не было больно потом его развивать. Разве вы не этого хотите?
Только лучше сразу понимать: то что описано в книгах на эту тему — это тоже по большей части не готовые решения. Это инструменты. Их тоже можно применять не по делу, или неверно. Ими тоже нужно уметь владеть, чтобы не завинчивать гвозди отверткой.