Domain-Driven Design: Продажа идеи

12 марта 2012 г.

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

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

Одним из серьезных препятствий оказалось то, что 90% бизнес-логики было написано на хранимых процедурах, остальная часть написана на .NET/C#. Ситуация осложнялась тем, что первая версия проекта была реализована и внедрена на Oracle, а второй заказчик настаивал на MSSQL.

Подход к продаже идеи

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

  1. Выявление проблемы (потребности)
  2. Выявление последствий проблемы
  3. Предложить решение, обсудить его выгоды
  4. Рассмотреть опасения
  5. Установить безопасное окружение для пилотирования
  6. Profit

Шаг 1. Выявление проблемы

У разработчиков были мысли, что бизнес-логику нужно выносить из БД, но куда и как они не знали. Плюс нужно было как-то обосновать для начальства время на рефакторинг и создание нового прототипа. Кроме того, в компании работал очень сильный администратор БД. Он оппонировал всем, кто хотел убрать бизнес-логику из БД и ни в какую не соглашался на смену архитектуры. Основным аргументом был вопрос о потере производительности.

В системе можно было выделить 3 компонента переплетенных между собой:

  1. Работа с пользовательским интерфейсом - UI
  2. Обработка данных - DB
  3. Набор множества объектов типа *Helper, *Manager, *Wrapper и т.п.

Основной вопрос, ответ на который я хотел услышать от разработчиков - Где описана бизнес-логика системы? Им было действительно трудно ответить и вот почему.

Посмотрите на один из самых простых примеров кода из блока UI:

 public EditGoodForm(DataRow dr)
 {
            InitializeComponent();
            connection = new SqlConnection(MainSQLConnection.Connection.ConnectionString);
            connection.Open();

            comboBoxType.DataSource = Populate("SELECT distinct id, Name FROM dbo.HB_GoodsType");
            comboBoxType.ValueMember = "ID";
            comboBoxType.DisplayMember = "Name";

            textBoxName.Text = _dr.Field<string>("Name");
            textBoxTolerance.Text = dr.Field<decimal?>("Tolerance")!=null?dr.Field<decimal?>("Tolerance").ToString():string.Format("{0:F}",0);
            comboBoxType.SelectedValue = _dr.Field<int>("HB_GoodsType_ID");
            comboBoxUnit.SelectedValue=_dr.Field<int>("HB_Unit_ID");
            comboBoxRouteMap.SelectedValue = _dr.Field<int>("HB_RouteMap_ID");
            checkBoxSkladUchet.Checked = _dr.Field<bool>("IsStoreMove");
 }
Вот вырезка из типичной хранимой процедуры:
--Получаем инфу по рецепту
SELECT @IsProduction=IsProduction,@Workcenter_ID = MF_WorkCenter_ID,@Good_ID = HB_Goods_ID,@RecipeOutcome=ProductionOutcome,@RecipeDateTimeShift=MinimalTime,@SingleObject = SingleObject,@MinimalObjectWeight = MinimalObjectWeightPercent * ProductionOutcome / 100,@NotPerformTechnicalReglament=NotPerformTechnicalReglament FROM MF_Recipe WHERE id = @IterationRecipeID

IF @SingleObject IS NULL SET @SingleObject = 1

SET @ObjectTime = DATEADD(hh,-@RecipeDateTimeShift,@TaskDateTime) --@TaskDateTime - @RecipeDateTimeShift

IF @RecipeOutcome = 0
SELECT @RecipeOutcome = SUM(Income) FROM MF_RecipeRow WHERE MF_Recipe_ID = @IterationRecipeID
--SET @Rule = 0
DECLARE @ObjectsOutput AS TABLE
(
ID INT,
ObjectOutcome DECIMAL(30,10),
RecipeMultipler DECIMAL(30,10),
AlredyExists BIT
)

-- --Фикитивная запись, для нумерации
INSERT @ObjectsOutput (ID, ObjectOutcome ,RecipeMultipler)
VALUES (0 ,0,0)

IF (dbo.F_MF_GetAuthorWorkcenter(@Workcenter_ID)=-1)
BEGIN
SET @Error = 'Для рабочего центра с ИД '+CAST(@Workcenter_ID AS VARCHAR(10))+' не найден родительский РЦ'
RAISERROR(@Error,12,1)
RETURN
END

Про Helper'ы и Wrapper'ы нечего говорить, потому что они представляют собой просто кучу статических объектов переплетённых между собой.

Ну и где же описана бизнес-логика? От ответа на этот вопрос зависит куда мы пойдем, когда требования заказчика изменятся.

Перечисляем проблемы

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

Итак, после обсуждения все вместе выявили ряд самых острых проблем:

  • Дублирование в C# коде
  • Дублирование в SQL-коде
  • Дублирование между C# и SQL-кодом
  • Бизнес-логика во всех слоях приложения
  • Нет разделения ответственности между классами и слоями приложения
  • В целом множество технических долгов

Как следствие из вышеперечисленного:

  • Трудно внести изменения и ничего не сломать
  • Трудно искать причины багов
  • Трудно гарантировать, что баг снова не выскочит
  • Рефакторинг значительно усложнен. Невозможно без опасения рефакторить SQL-код
  • Отсутствие модульных тестов: в C# коде сильная связанность и зависимость от внешних ресурсов, а на хранимые процедуры сложно писать модульные тесты

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

Шаг 2. Выявление последствий проблемы

Последствия были для всех очевидны, оставалось только выписать их по пунктам:

  • Низкая скорость разработки
  • Непредсказуемые баги
  • Отсутствие Definition of Done
  • Невозможность быстро реагировать на изменения требуемые заказчику
  • Незаменимость разработчиков
  • Сложность создания универсального решения
  • По факту нет коробочной версии, а это значит, что нет потока денег для компании

Такое положение вещей никак не устраивало руководство компании.

Шаг 3. Предложить решение

Мое предложение строилось на внедрении Domain Driven Design и создании прототипа с новой архитектурой. Мы подробно разобрали следующие темы:

  • Создание домена и обход основных ошибок при использовании DDD
  • Aggregation Root
  • Работа с данными: Repository, Unit of Work и Persistence Ignorance
  • Прямо во время обсуждение рождалось море идей по тому, как можно просто и красиво реализовать задачи проекта, как изменить архитектуру системы, как реализовать работу с БД. Вдохновение витало в воздухе, появлялись конструктивные споры.

    Шаг 4. Рассмотреть опасения

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

    • Если мы внесем изменения в систему, как это повлияет на уже работающую версию?
    • Не упадет ли скорость работы, когда бизнес-логика перейдет из БД в .NET?
    • В проекте есть работа с датчиками, датчики сидят на общей шине, которая настроена писать данные напрямую в БД. Можно ли будет перенастроить систему на отсылку сообщений в .NET?
    • После перехода к новой архитектуре смогут ли все разработчики в ней разобраться?
    • Начнется ли сопротивление со стороны администратора БД из-за потери своего влияния в компании? Если да, то как это можно будет смягчить?

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

    Шаг 5. Установить безопасное окружение для пилотирования

    Мы совместно с IT-директором компании выделили двух программистов, которые очень активно хотели уйти от бизнес-логики из БД. Они пообещали в течение месяца сделать прототип системы, исходя из новой для них парадигмы - Domain Driven Desing. Эти программисты пообещали, что больше никогда не предложат подобный рефакторинг, если идея с переходом на DDD окажется для их проекта не жизнеспособной.

    Шаг 6. Profit

    Меньше, чем за месяц программисты вынесли всю бизнес-логику из БД, написали на нее модульные тесты, сделали эмулятор железок и протестировали систему на быстродействие. Сейчас система готовится к выходу на новый объект для внедрения. К слову, первая версия системы делалась несколько лет целой командой.

    Выводы

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

    Какие я вижу предпосылки к появлению проблем в описанной компании:

    • Бизнес-логика исторически была в хранимых процедурах, вызывала проблемы, но никто не решался это изменить
    • Новые проекты писали по-старинке и бились об те же грабли снова и снова
    • Команда очень мало общалась, проблемы редко выносились на обсуждение
    • Авторитет в виде сильного администратора БД, с которым никто не хотел связываться
    • Программисты, которые были за хорошие идеи не знали как "продать" их без внешней помощи

    Почему удалось продать новые идеи:

    • Руководство компании было действительно заинтересовано в скорейших результатах
    • Мне удалось показать связь между хорошим кодом, тестами и прибылью для компании
    • Продажа идеи шла по шагам без спешки, без рассказа про серебряные пули
    • В компании мы нашли энтузиастов, которые взяли на себя ответственность по созданию прототипа

    Ссылки

    Видео с AgileDays 2010 - Как продать Agile заказчику, Асхат Уразбаев

    Книга СПИН-продаж, Нил Рекхэм

    Взаимоотношения или споры в команде, группа DotNetConf

    Моя книга «Антихрупкость в IT»

    Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...