Как построить систему управления налогами с помощью Rulette: часть первая

Это глубокое погружение в использование Рулетки для моделирования бизнес-правил. Это упрощенная (слегка) версия реального варианта использования, для которого я использовал Rulette в производстве. Мы попробуем смоделировать налог, который необходимо платить при продаже определенных видов товаров производителями, находящимися в разных штатах.

В начале…

Все начинается с создания великих законов о налогах, где правительство предписывает, чтобы налог, который должен платить производитель при продаже, зависел от штата, в котором находится его предприятие (Западная Бенгалия, Пенджаб…), типа товар (футболка, обувь, сумки и т. д.), материал (шелк, кожа, золото…) и цена товара. Менеджер по продукту совещается с финансовой и юридической командой и собирает все комбинации этих атрибутов в таблице Excel. Все это представляет собой многоуровневую структуру, построенную на правилах по умолчанию с более конкретными правилами, применяемыми поверх них (Карнатака взимает 5% с продаж обуви, если только она не сделана из кожи и не стоит дороже 5000 рупий, в этом случае взимается 10%). . Что-то вроде этого.

Нам, команде разработчиков, теперь нужно создать систему для хранения этих правил и определить, какими должны быть применимые налоги на продажу. Правил много, поэтому система должна с этим справиться, но ожидается, что они будут меняться нечасто, но читаться будут очень регулярно. Конечно, мы хотели бы сделать управление всеми этими правилами как можно проще, а их оценку — как можно быстрее. Допустим, у нас уже есть базовая налоговая система Java, и мы хотим усовершенствовать ее, чтобы она соответствовала новому налоговому режиму. С этим, давайте перейдем к решению(ям).

Жестко запрограммированное если-иначе

Любой набор правил может быть выражен как комбинация условий if-else — это то, что представляет собой все управление правилами. Таким образом, наивным способом было бы жестко закодировать все эти правила и вернуть соответствующие налоговые значения в качестве вывода. Если в правиле есть какое-то конкретное измерение, которое нас не интересует (например, Любой MRP), мы будем представлять его строкой «Любой».

public String getTax(
	String sourceSate,
	String itemType,
	String material,
	Double mrp) {
	
	if (sourceState == "KAR" && itemType = "bag" && material="Any" && mrp ="Any") {
		return "5";
	} else if (sourceState == "KAR" && itemType == "bag" && material=="Leather" && mrp > 5000) {
		return "10";
	} else if........//About 100000 more time
	} else {
		return null;
	}
}

Это сработает, но кодовая база превратится в идеальный ад для человека и зверя. Это сложно понять, изменить и проверить на правильность. Если кто-то спросит о налоговых правилах в нашей системе, нет другого пути, кроме как открыть код, что очень затрудняет общение с менеджерами по продукту и бизнес-командами. Они не видят технического представления правил. Кроме того, с вычислительной точки зрения, это поиск грубой силы при попытке оценить любой результат. Мы перебираем все комбинации всех входных данных. Структура проблемы указывает на то, что ее можно было бы улучшить, если бы мы использовали какое-либо решение на основе дерева/графа.

Настраиваемый если-иначе

Огромным шагом вперед по сравнению с этой ситуацией было бы захватить входные данные правила и операторы (равно, больше и т. д.) в какой-то конфигурации или DSL, которые мы можем проанализировать при запуске, чтобы построить весь набор комбинаций if-else. Это упростит управление и совместное использование правил вне кода, потому что мы можем преобразовать таблицу Excel для менеджеров продуктов в конфигурацию независимо, а затем перезагрузить их в налоговую систему. Чтобы код выглядел проще, тяжелая работа переместилась с реализации кода на реализацию преобразования листа Excel в DSL. Это не устраняет вычислительную сложность — мы по-прежнему оцениваем все комбинации, чтобы выяснить, какие правила применяются, комбинации просто генерируется с помощью конфига.

public String getTax(
	String sourceSate,
	String itemType,
	String material,
	Double mrp) {
	List<Config> allConfigs = loadCOnfigs(file);//Assuming things are stored in a file
	for (Config config : allConfigs) {
		if (config.matches(sourceState, itemType, material, mrp)) {
			return config.output;
		}
	}
	return null;
}

Использование Rulette для моделирования правил

Рулетка включает в себя знания о предыдущих двух попытках и представляет их в одном компактном решении. Мы видим, что наиболее интуитивно понятной конфигурацией/DSL для моделирования правил является тот же формат, в котором они были разделены — двумерная матрица, в которой каждый столбец представляет собой «ввод правила» (состояние источника, тип элемента, материал, цена), а каждая строка представляет «правило», т. е. комбинация этих входных данных, сопоставленных с выходным значением.

Мы также видим, что отношение между столбцами листа (в одной строке) относится к типу «И» (также видно в приведенном выше коде) и что любые отношения «ИЛИ» моделируются как несколько строк (как показано остальными). если условия в коде выше).

Rulette переносит эти идеи моделирования в самое сердце приложения, так что команды разработчиков и бизнес-команды говорят на одном языке, когда дело доходит до моделирования правил.

Настройка рулетки

Предположим, что мы берем лист Excel и помещаем его в таблицу MySQL с именем 'tax_rule_system' в 'tax схема. Вам не обязательно делать это (у Rulette есть расширяемая модель загрузки данных, которая позволяет вам подключать любой источник правил), но MySQL официально поддерживается из коробки, поэтому мы будем использовать это в этом случае. учиться. Эта таблица почти идентична листу Excel и теперь содержит все наши налоговые правила.

CREATE TABLE `tax_rule_system` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `source_state` varchar(100) NULL,
  `item_type` int NULL,
  `material` varchar(100) NULL,
  `min_mrp` DECIMAL(12,3) NULL,
  `max_mrp` DECIMAL(12,3) NULL,
  `rule_output_id` VARCHAR(256) NOT NULL,  
  PRIMARY KEY (`id`)
);

Давайте создадим две таблицы метаданных в схеме «налог» (или любой другой схеме, к которой налоговое приложение имеет доступ для чтения). Эти таблицы помогают Rulette разобраться в налоговых правилах, которые мы сохранили ранее.

CREATE TABLE rule_system (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `table_name` varchar(100) NOT NULL,
  `output_column_name` varchar(256) DEFAULT NULL,
  `unique_id_column_name` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`id`)
);
CREATE TABLE `rule_input` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `rule_system_id` int(11) NOT NULL,
  `priority` int(11) NOT NULL,
  `rule_type` varchar(45) NOT NULL,
  `data_type` varchar(45) NOT NULL,
  `range_lower_bound_field_name` varchar(256) NULL,
  `range_upper_bound_field_name` varchar(256) NULL,
  PRIMARY KEY (`id`)
);

Первая таблица моделирует систему правил как сущность и позволяет нам определить, где хранятся правила системы правил. В этом примере мы можем выполнить следующее сопоставление, чтобы сопоставить имя системы правил «tax_rule_system» с данными, хранящимися в нашей таблице «tax_rule_system». Он также определяет, что этот уникальный идентификатор для правил в этой таблице называется «id» (первичный ключ) и что значения исходящего налога находятся в столбце с именем «rule_output_id».

INSERT INTO rule_system 
  (`name`, `table_name`, `output_column_name`, `unique_id_column_name`)
VALUES 
  ('tax_rule_system', 'tax.tax_rule_system', 'rule_output_id', 'id');

Вторая таблица интерпретирует столбцы нашей системы правил. Каждая строка в этой таблице определяет имя ввода (состояние источника, материал, тип элемента, ППМ), его тип (ЗНАЧЕНИЕ или ДИАПАЗОН), его типы данных (строка/число/дата), его приоритет (порядок, в котором входы сопоставляются во время оценки). Вы можете узнать больше о типах и типах данных здесь и о том, как происходит вычисление правил в Rulette здесь. На данный момент просто сосредоточьтесь на приоритетах, установленных для каждого входа, и, поскольку значение MRP является входом RANGE, оно физически хранится в виде двух столбцов (min_mrp, max_mrp) в таблице tax_rule_system, которые отображаются как столбцы `range_lower_bound_field_name` и `range_upper_bound_field_name` в последнем операторе INSERT.

INSERT INTO rule_input 
  (`name`, `rule_system_id`, `priority`, `rule_type`, `data_type`, `range_lower_bound_field_name`, `range_upper_bound_field_name`)
VALUES 
  ('source_state', 1, 1, 'VALUE', 'STRING', NULL, NULL);
  
INSERT INTO rule_input 
  (`name`, `rule_system_id`, `priority`, `rule_type`, `data_type`, `range_lower_bound_field_name`, `range_upper_bound_field_name`)
VALUES 
  ('item_type', 1, 2, 'VALUE', 'NUMBER', NULL, NULL);
  
INSERT INTO rule_input 
  (`name`, `rule_system_id`, `priority`, `rule_type`, `data_type`, `range_lower_bound_field_name`, `range_upper_bound_field_name`)
VALUES 
  ('material', 1, 3, 'VALUE', 'STRING', NULL, NULL);
  
INSERT INTO rule_input 
  (`name`, `rule_system_id`, `priority`, `rule_type`, `data_type`, `range_lower_bound_field_name`, `range_upper_bound_field_name`)
VALUES 
  ('mrp_threshold', 1, 4, 'RANGE', 'NUMBER', 'min_mrp', 'max_mrp');

Вся эта настройка включена в пример сценария SQL в модуле примеров Rulette. Он также содержит набор примеров правил, которые вы можете вставить в таблицу tax_rule_system, чтобы поэкспериментировать с этим примером.

Вот и все! Теперь рулетка настроена для использования. Давайте теперь посмотрим на сторону кода.

Использование рулетки в коде

Первый шаг — добавить библиотеку Rulette в нашу налоговую систему. В Maven мы можем сделать это:

<dependency>
        <groupId>com.github.kislayverma.rulette</groupId>
        <artifactId>rulette-engine</artifactId>
        <version>1.3.2</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>com.github.kislayverma.rulette</groupId>
        <artifactId>rulette-mysql-provider</artifactId>
        <version>1.3.2</version>
        <scope>compile</scope>
    </dependency>

Первая — это зависимость для механизма оценки Rulette, а вторая — для плагина загрузки данных MySQL, который мы будем использовать для подключения к только что выполненной настройке базы данных. Теперь нам нужно сообщить Rulette, как подключиться к базе данных. Rulette использует пул соединений Hikari, который легко настраивается. Для этого создайте файл свойств, в котором его сможет найти и загрузить ваше приложение. Пример файла выглядит так. Теперь мы можем инициализировать систему налоговых правил следующим образом:

File f = new File("db properties file path");
    IDataProvider dataProvider = new MysqlDataProvider(f.getPath());
    RuleSystem rs = new RuleSystem('tax_rule_system', dataProvider);

Это сначала загрузит определения системы правил из наших таблиц метаданных, а затем загрузит все правила для «tax_rule_system» в памяти в формате trie, оптимизированном для оценки. Убедитесь, что вы создаете каждое правило только один раз, потому что каждый новый объект RuleSystem содержит все правила из базы данных в памяти. Если вы по ошибке начнете создавать экземпляр нового объекта для каждого запроса на оценку, у вас быстро закончится память! Теперь мы готовы приступить к оценке правил.

Допустим, нам нужно знать, какая ставка налога обычно применяется в штате Карнатака. Мы можем сделать следующее.

Map<String, String> inputMap = new HashMap<>();
inputMap.put("source_state", "KAR");
Rule applicableRule = rs.getRule(inputMap);
if (applicableRule != null) {
	System.out.println(applicableRule);	
} else {
	System.out.println("No rule found");	
}

Как насчет того, чтобы узнать, как облагаются налогом изделия из кожи в штате Карнатака?

Map<String, String> inputMap = new HashMap<>();
inputMap.put("source_state", "KAR");
inputMap.put("material", "leather");
Rule applicableRule = rs.getRule(inputMap);
if (applicableRule != null) {
	System.out.println(applicableRule);	
} else {
	System.out.println("No rule found");	}

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

Собрав все вместе, вот как может выглядеть новый код в налоговой системе:

public class RuletteBasedTaxManager {
	private final RuleSystem taxRuleSystem;
	public RuletteBasedTaxManager(String dpPropertiesFilePath, String ruleSystemName) {
	    File f = new File(dpPropertiesFilePath);
	    IDataProvider dataProvider = new MysqlDataProvider(f.getPath());
	    taxRuleSystem = new RuleSystem(ruleSystemName, dataProvider);
	}
	public Optional<String> getTax(Map<String, String> inputMap) {
		Rule applicableRule = rs.getRule(inputMap);
		if (applicableRule != null) {
			return Optional.ofNullable(applicableRule.getColumnData(rs.getOutputColumnName()));
		} else {
			return Optional.empty();
		}
	}
}

Обратите внимание на синтаксис доступа к выходным данным применимого правила. Как библиотека, Rulette не понимает, что означает «выходное значение» в любом случае использования — она просто возвращает правило наилучшего соответствия. Этот бизнес-агностицизм можно использовать для создания другого уровня косвенности, когда мы храним не фактическое значение налога внутри таблицы tax_rule_system, а скорее ссылку на внешний источник данных где-то еще. Это может быть полезно, если у нас уже есть некоторые данные в нашей системе, и нам нужна система правил для сопоставления с ними различных вариантов использования. Часть Rulette будет продолжать работать точно так же, в то время как использующее приложение теперь может использовать возвращенный вывод в качестве эталона в других системах.

Этот компактный фрагмент кода продемонстрировал, прежде всего, сложность моделирования бизнес-правил и невероятно быстрой оценки входных данных. Использование Rulette также означает, что вы можете общаться с бизнес-командами на их родном языке (каждый может понять таблицу tax_rule_system) и что вы можете добавлять дополнительные правила без изменения кода.

Дополнительные способы использования Rulette для управления правилами на основе MySQL описаны в модуле примеров.

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

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