Pull to refresh

Шаблон проектирования “минисценарий с проверкой противоречий”

Reading time 11 min
Views 6K
В данной статье я расскажу о собственных наработках, которые я опробовал на практике, чтобы внести больше ясности в разработку и эксплуатацию кода.

Последние пару лет я работаю в крупном проекте и наткнулся на некую закономерность, которая приводит к тотальному запутыванию кода. Код, который развивался лет двадцать командой около сотни человек трудно назвать кодом без эпитетов. Скорее это гора на десяток миллионов строк из всяких правильных и неправильных техник, умных идей, ухищрений, заплаток, копипастов на скорую руку и тд тп…

image

В организации, где я работаю, автоматизируют бизнес процессы, и обычно это связано с ведением базы данных. У нас принято работать по канонам, — сначала проводить бизнес анализ, составлять умное ТЗ, писать код, проводить тестирование и много всякой деятельности при дефиците времени. Первичная мотивация, вроде разумная, — “давайте будем разделять обязанности”, “давайте будем делать так, чтобы было безопасно” и тд и тп. Все эти приемы менеджмента с восторгом преподают на различных курсах, обещая много хайпа и охмурянта. Надеюсь, что читатель уже знаком с некоторыми модными словами, которые зачастую ни потрогать, ни налить нельзя. Но вопрос не об них, а о том, как программисту жить с ними.

Далее постараюсь объяснить, в чем разница между “бизнес логикой” и “строгой логикой”, на которую почему-то многие не обращают достаточного внимания. В результате оголтелого использования бизнес логики страдает просто логика, за которую боролись и математики и философы сотни лет. А когда страдает настоящая логика, то вместе с этим страдают сначала исполнители-технари, которые от безысходности могут начать лепить несуразное, чтобы лишь бы начальники отвязались. А потом бумерангом страдания возвращаются на источник “ярких супер бизнес идей”, заставляя их придумывать другие еще более “яркие супер бизнес идеи” или в конце концов надевать накладную бороду и темные очки, чтобы больше никто не узнавал на улице и не показывал пальцем.

С какими спецэффектами я постоянно встречаюсь по работе:

  1. Предметную область досконально знают немногие и еще меньше могут внятно формулировать для программиста.
  2. Заказчик может выдвигать противоречивые требования, и вывод его на чистую воду приводит к потере терпения и конфронтации.
  3. Исполнитель стремится выполнить ТЗ, пройти тесты и отправить код в бой при дефиците времени и давлении со стороны бизнеса. Красота решения через некоторое время мало кого волнует, так как разбираться голову сломаешь.

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

Перейдем к конкретике и разберем упрощенный пример. Давайте включим воображение и предположим, мы ведем базу с договорами. Пусть в базе ведутся договоры типа1 и типа2 для физ. лиц и для юр.лиц. Сверху на программиста сваливаются требования из ТЗ 1 со словами “для всех договоров типа1 нужно выполнять действие1” в момент вступления договора в силу.

Не беда, программист делает такой код номер 1:

if( doctype==1 ){ do_something1; }

Через некоторое время спускается другое требование из ТЗ 2: “для договоров юр.лиц выполнять действие2” тоже в момент вступления его в силу. Но вот беда, — старый программист уже перекинулся на другое направление и ТЗ 2 уже поручили другому программисту, который не успел вникнуть во все нюансы, поэтому он тоже незатейливо пишет код номер 2:

if( clienttype==ORGANIZATION ){ do_something2; }

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

Таблица 1
юр. лица
физ. лица
тип договора 1 do_something1
do_something2
do_something1
тип договора 2 do_something2

Получаем 4 клетки и видим, что в ¾ случаев программа будет работать без вопросов. И лишь в одном случае возникает коллизия, — выполняются оба действия. Хорошо это или плохо, зависит от условий. Иногда может быть и хорошо, а иногда плохо, но хотелось бы проконтролировать.

В более сложном случае, когда типов больше (см таблицу 2), то пресловутые обобщения “для всех типов договоров“ или “для всех типов клиентов” выглядят как строки или колонки в таблице сочетаний. А коллизии образуются на пересечении. В таблице 2 коллизия возникает в одном случае из девяти. Чем больше проект, тем больше эти списки, и тем труднее найти иголку в стоге сена. Вспоминается анекдот про суслика в поле: “Видишь коллизию? Нет не вижу. А она есть!”.

Таблица 2
тип клиента 1 тип клиента 2 тип клиента 3
тип договора 1 do_something1
do_something2
do_something1 do_something1
тип договора 2 do_something2
тип договора 3 do_something2

Чем сложнее проект, тем больше эта матрица, и со временем она растет и растет. Поэтому для наглядности и честности анализа стоит добавить еще один тип — неизвестный в данный момент “новый тип”. С ним точно столкнется будущее поколение, и что оно будет означать, — скорее всего ни один телепат не подскажет.

Таблица 3
тип клиента 1 тип клиента 2 тип клиента 3 новый тип
клиента,
возможный
в будущем
тип договора 1 do_something1
do_something2
do_something1 do_something1 ?
тип договора 2 do_something2 ?
тип договора 3 do_something2 ?
новый тип
договора,
возможный
в будущем
? ? ? ?

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

В наивном коде нет ответов следующие вопросы:

  1. Что делать для новых типов? Какова гарантия, что они впишутся в старые алгоритмы? Как программа должна реагировать, на незнакомую ситуацию ?
  2. Что делать в случае, если действие1 и действие2 взаимоисключающие ?
  3. Как вести знания о предметной области и как формулировать ТЗ, чтобы не было противоречий?

Первый вопрос я уже пытался осветить в статье [ссылка]. Кратко — я предлагал внедрять в код списки типов, который он имеет право обрабатывать и сигнализировать о появлении нового типа, если вдруг новый тип попадет в подпрограмму. Кстати этот прием я тоже уже опробовал на проекте, и он дает свои плоды, добавляя ясности в процесс эксплуатации при неопределенных ситуациях. (update: прием присутствует в защитном программировании)

Сейчас хочу осветить второй и третий вопросы, — как определять своевременно противоречия в автоматическом режиме. Ведь когда проект переваливает за несколько миллионов строк, то сложно добавить новый код, чтобы он чего-нибудь не попортил старого. По сути я предлагаю прикрепить к бизнес-объекту некий умный контейнер, куда вписывать все требования. А этот контейнер уже сам будет проверять, — насколько новые пункты ТЗ уживаются со старыми. То есть когда приходит новый пункт ТЗ, то программист тупо перекладывает его “как пришло” на язык программирования, сохраняя заложенную бизнес-логику. Должен получиться такой эффект, что если смотреть на получившийся код и на ТЗ, то должно быть понятно без особый усилий, — как они связаны. Затем он помещает код в умный контейнер, который является связующим звеном между бизнес логикой и строгой логикой. Насколько это возможно программно — выношу на суд читателей. Далее описываю свое решение.

Для этого во первых я предлагаю отделить вычисление условий и сами действия. То есть разнести if-ы отдельно от do_something. При вычислении условий запрещается выполнять какие-либо изменения в базу данных или в глобальные переменные. Это ближе к функциональному программированию и чистой логике. Ничего не изменяется во внешнем контексте, а только вычисляются логические выражения. Внедряются более мягкие связи между условиями из ТЗ и действиями программы. На первом этапе контейнер формирует текст — легко интерпретируемый как человеком, так и любой программой.

На выходе на первом этапе контейнер выдает:

  1. Основной минисценарий в виде небольшого текста или простой структуры данных.
  2. Дополнительные предупреждения и рекомендации в виде текста.
  3. Ошибку в виде текста или возбуждает исключения в зависимости от пожеланий.

Схема формирования минисценария выглядит так:

image

Рисунок 1. Схема первого этапа

После того, как минисценарий сформирован, то получается, что большие обобщенные тексты ТЗ редуцируется в некий компактный читабельный текст, подходящий для конкретного бизнес-объекта (договора, клиента, документа и тп) в его текущем состоянии. Этот текст уже ближе к конкретным действиям в базе данных и элементарным операциям, но тем не менее он достаточно абстрактен и не зависит от базы или среды. Далее на следующих этапах с ним можно делать много полезных вещей до того, как по сценарию будут внесены изменения в базу данных.

Какие из этого ожидаются плюсы:

  1. Можно играть с этим контейнером как угодно, так как он не изменяет данные, а возвращает только текст. Можно гонять его по всей базе, не боясь чего-то изменить. Можно делать регресс-тестирование, сравнивая новые и старые тексты минисценариев. Можно делать прогноз, — что будет делать программа в том или ином случае.
  2. Реализуется полезный принцип слабой связи, когда чистая логика и чистые изменения разносятся в разные подпрограммы.
  3. Если дорисовать функционал, то через конфигурацию можно включать или выключать какие-то блоки. Особо актуально, когда нет доступа на бой, но надо отключить какие-то глючные куски.
  4. Текстовки можно делать на родном языке, что увеличивает понимание для специалистов других специальностей, — тестеров, бизнес аналитиков, пользователей и тп.
  5. Самое важное, о чем я бы хотел поведать — это автоматический контроль противоречий, который опишу ниже.

Какие минусы:

  1. Надо вычислять все условия, — и новые и старые, то есть со временем будет работать медленнее, но зато правильно. Уходим от преждевременной оптимизации и вспоминаем слова Дональда Кнута «Premature optimization is the root of all evil».
  2. В языках программирования еще нет кратких и красивых конструкций, которые бы принуждали программиста писать правильно по паттерну. Потенциально все еще можно пойти против паттерна и внедрить противоречивую логику туда, где её не должно быть. То есть требуется некоторая дисциплина от разработчика, чтобы он добавлял новые условия ТЗ по определенным правилам в определенное место.

Переходим к описанию реализации. Входными параметрами являются (рисунок 1):

  1. Данные, идентифицирующие бизнес-объект. В самом простом случае это может быть просто ссылка на java объект или ID объекта в базе данных. Но так как эта функция может вызываться из очень разных мест, — в том числе, оттуда, где прямой ссылки на объект нет, то на практике я делал универсальный параметр, и в первом блоке (рис.1) производил поиск объекта.
  2. Второй параметр, — команда. Например, “установи номер бух. счета” или “установи следующий статус” и тп. Команда предназначена для оптимизации и повышения гибкости. Мне не практике она была не нужна, так как я всё время выдавал полный минискрипт по текущему объекту, а затем обработчик минискрипта решал, что ему нужно исполнить в зависимости от ситуации.

В следующих блоках происходит вычисление всех условий по всем ТЗ без оптимизации и логгирование. Логи я использовал только на сервере тестирования, чтобы быстро реагировать на замечания тестировщика. В логи вписывал присланные мне фразы из ТЗ, чтобы было на что сослаться при разборе полётов. То есть я использовал такой подход:

	bool1 = contract.type_id == 1;
	log(“согласно ТЗ 1 тип договора=1 ” + bool1.toString());
	if( bool1 ){ add_miniscript(“выставить счет”,“50”/*руб*/); } //key value
	bool2 = contract.client.type == ORGANIZATION;;
	log(“согласно ТЗ 2 тип клиента юрлицо ” + bool2.toString());
	if( bool2 ){ add_miniscript(“выставить счет”,“66”/*руб*/); }//key value

Как именно обнаруживать противоречия и коллизии, — наверное многие уже догадались. На помощь приходит ассоциативный массив key=value. Key — это команда, а value — её параметры. Установка значений для атрибута более одного раза, особенно если значения разные, — это и есть печально известный случай противоречий. В коде ниже больше подробностей.

Инфраструктура применения имеет такой вид:

image
Рисунок 2

Пока не раскрываю всех нюансов, с которыми удалось столкнуться. Самый первый вариант этого поведенческого паттерна я реализовал в Oracle PL/SQL без привлечения ООП. То есть паттерн можно считать универсальным и применимым как для ООП, так и для процедурного стиля. На первом же сложном ТЗ, который состоял из 15 пунктов я выявил 4 противоречия в первую неделю и еще пару через полгода, чем удивил аналитиков, которые даже не думали, что их слова могут так обернуться в определенных случаях.

Для тех, кто еще не устал читать — ниже описана реализация на Java, которую можно запускать, и она будет выдавать в консоль обнаруженную коллизию. Минисценарий реализован в виде упорядоченного списка, куда складываются элементы сценария — пары {key,value} и комментарии. Список выбран в связи с тем, что на практике надо разрешать иметь коллизии по некоторым ключам. Для этого в коде есть кусок, который может включать-выключать обработку коллизий. Подробные комментарии в коде. Должно работать, если скопировать в среду разработки и запустить.

file: testMiniscript.java

package test;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

// Интерфейс для бизнес-объектов, которым необходимо поддерживать минискрипты с проверкой противоречий.
interface IMiniScript{
    MiniScript GetMiniscript(String command) throws MiniScriptExcept; // отдельно получить 
    void ExecuteMiniScript(MiniScript ms) throws MiniScriptExcept;    // отдельно исполнить
}



// Элемент минискрипта
class MiniScriptItem{
    private final String key;       // ключ по которому будем искать конфликты
    private final String parameter; // необязательный доп. параметр
    private final String comment;   // необязательный коментарий, указывающий на пункт ТЗ
    
   public MiniScriptItem(String key, String parameter, String comment) {
        this.key = key;
        this.parameter = parameter;
        this.comment=comment;
    }
    
    public String getKey() {
        return key;
    }

    public String getParameter() {
        return parameter;
    }
    public String getComment() {
        return comment;
    }

    @Override
    public String toString() {
        return "MiniScriptItem{" + "key=" + key + ", parameter=" + parameter + ", comment=" + comment + '}';
    }
    
    
}// MiniScriptItem


class MiniScriptExcept extends Exception {
    MiniScriptExcept(String string) {
        super(string);
    }
}


// Базовый класс для минискрипта, с алгоритмом работы по умолчанию, когда любые ключи не допускают коллизий
class MiniScript { 
    
    // Делаю List а не HashMap, так как некоторые ключи могут допускать коллизии, см ниже allowDublicate(key)
    List<MiniScriptItem> miniScriptItemList = new ArrayList<MiniScriptItem>(); 

    
    // Собственно детектор коллизий. Блок 6 из рис. 1
    void add(String pkey, String pparameter, String pcomment) throws MiniScriptExcept{
       
        if(allowDublicate(pkey)){ // если дубли разрешены, то просто добавляем
           miniScriptItemList.add(new MiniScriptItem(pkey, pparameter,pcomment));
       }else{
         // дубли не разрешены, проверяем коллизии
         MiniScriptItem conflict=null ;
         for (MiniScriptItem miniScriptItem : miniScriptItemList) {
               if(miniScriptItem.getKey().equals(pkey)){
                   conflict= miniScriptItem;
               }
          }
          
          if(conflict!=null){ // обнаружился конфликт
              
              throw new MiniScriptExcept("Обнаружено противоречие требований. "
                      +System.getProperty("line.separator")
                      +"key="+conflict.getKey()+" parameter=" +conflict.getParameter()+" comment="+conflict.getComment()
                      +System.getProperty("line.separator")
                      +"key="+pkey+" parameter=" +pparameter+" comment="+pcomment
              );//throw
              
              
          }else{  //key не найден в списке, значи добавляем
            miniScriptItemList.add(new MiniScriptItem(pkey, pparameter,pcomment));
          }
       }
    }  
    
// Тут можно добавлять ключи, позволяющие иметь коллизии. 
// При адаптации к конкретному бинес-объекту надо делать override этой подпрограммы
  boolean allowDublicate(String key){ 
//Если этот кусок раскоментирован, то конфликта уже не будет, и выставится 2 счета. Попробуйте его раскоментировать и посмотреть что будет выдано в консоль
//        if(key.equals("выставить счет")){ 
//            return true;
//        }
        return false;
    }

    @Override
    public String toString() {
        return "MiniScript{" + "miniScriptItemList=" + miniScriptItemList + '}';
    }
    
}//MiniScript



// Cобстенно пример бизнес объекта c адаптером для бизнес-логики
class Contract implements IMiniScript{    
   int type=1;
   String clienttype="юр.лицо";
   
   
   // См Рисунок 1 в тексте.
   // На картинке указан блок 1 "параметры, идентифицирующие объект",
   // в данном случае текущий объект является этим неявным параметром.
   // Второй параметр "команда" передается явно
   @Override
   public MiniScript GetMiniscript(String command) throws MiniScriptExcept{
       MiniScript ms = new MiniScript();

       // Именно сюда пишем всю бизнес-логику данного объекта.
       // Все остальное - умная обертка.
       if(command.equals("договор вступил в силу")){ 
           
           // Блок 2 из рис. 1 “формирование минисценария по ТЗ 1”
            if(type==1){
              ms.add("выставить счет", "50 руб", "согласно ТЗ 1");
            }
           
           // Блок 3 из рис. 1 “формирование минисценария по ТЗ 2”  
            if(clienttype.equals("юр.лицо")){
              ms.add("выставить счет", "66 руб","согласно ТЗ 2");
            }
            
            //Блок 5 из рис. 1 “ Место для новых ТЗ”
            //<<<< сюда вписываем новые пункты ТЗ по мере поступления >>>>
            //<<<< сюда вписываем новые пункты ТЗ по мере поступления >>>>
            
       }else{
           // элемент защитного программирования
          // программа должна реагировать на неизвестные ей команды
           throw new MiniScriptExcept("неизвестная команда "+command);
       }
       return ms;
    }

    @Override
    public void ExecuteMiniScript(MiniScript ms) throws MiniScriptExcept {
         List<MiniScriptItem> items = ms.miniScriptItemList;
         
         for (MiniScriptItem item : items) {
            if(item.getKey().equals("выставить счет")){
		// заглушка
                System.err.println("Выставляю счет в базе данных на "+item.getParameter()+" "+item.getComment());   
            }else{
                throw new MiniScriptExcept("неизвестная команда "+item.getKey());
            }
            }
        }
}// Contract

public class testMiniscript {

    public static void main(String[] args) throws MiniScriptExcept {
        
        // Рисунок 2 в тексте
        Contract c = new Contract();
        c.type=1;
        c.clienttype="юр.лицо";

        // получаем минисрипт
        MiniScript ms=c.GetMiniscript("договор вступил в силу");

        // тут еще можно его дополнительно проанализировать, 
        // например, можно сделать автоматический регресс-анализ со старой версией класса

        // собственно выполняем минискрипт
        c.ExecuteMiniScript(ms);
        
        System.err.println("Минискрипт " + ms.toString());
        
    }
    
    
}

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

P.S.: Некоторые читатели могут задаться вопросом, — почему я назвал паттерном, а не методикой, не методом, не шаблоном, не технологией или как-то еще? Думаю, что строгой разницы в названиях нет, поэтому надо на чем-то остановиться и как-то назвать. Давайте будем оценивать практическую пользу.

Дополнительная информация из википедии:
Bussiness Rule Management System
IoC

Рыбаков Д.А.
к.т.н., ноябрь 2017
Tags:
Hubs:
+3
Comments 21
Comments Comments 21

Articles