Последние пару лет я работаю в крупном проекте и наткнулся на некую закономерность, которая приводит к тотальному запутыванию кода. Код, который развивался лет двадцать командой около сотни человек трудно назвать кодом без эпитетов. Скорее это гора на десяток миллионов строк из всяких правильных и неправильных техник, умных идей, ухищрений, заплаток, копипастов на скорую руку и тд тп…
В организации, где я работаю, автоматизируют бизнес процессы, и обычно это связано с ведением базы данных. У нас принято работать по канонам, — сначала проводить бизнес анализ, составлять умное ТЗ, писать код, проводить тестирование и много всякой деятельности при дефиците времени. Первичная мотивация, вроде разумная, — “давайте будем разделять обязанности”, “давайте будем делать так, чтобы было безопасно” и тд и тп. Все эти приемы менеджмента с восторгом преподают на различных курсах, обещая много хайпа и охмурянта. Надеюсь, что читатель уже знаком с некоторыми модными словами, которые зачастую ни потрогать, ни налить нельзя. Но вопрос не об них, а о том, как программисту жить с ними.
Далее постараюсь объяснить, в чем разница между “бизнес логикой” и “строгой логикой”, на которую почему-то многие не обращают достаточного внимания. В результате оголтелого использования бизнес логики страдает просто логика, за которую боролись и математики и философы сотни лет. А когда страдает настоящая логика, то вместе с этим страдают сначала исполнители-технари, которые от безысходности могут начать лепить несуразное, чтобы лишь бы начальники отвязались. А потом бумерангом страдания возвращаются на источник “ярких супер бизнес идей”, заставляя их придумывать другие еще более “яркие супер бизнес идеи” или в конце концов надевать накладную бороду и темные очки, чтобы больше никто не узнавал на улице и не показывал пальцем.
С какими спецэффектами я постоянно встречаюсь по работе:
- Предметную область досконально знают немногие и еще меньше могут внятно формулировать для программиста.
- Заказчик может выдвигать противоречивые требования, и вывод его на чистую воду приводит к потере терпения и конфронтации.
- Исполнитель стремится выполнить ТЗ, пройти тесты и отправить код в бой при дефиците времени и давлении со стороны бизнеса. Красота решения через некоторое время мало кого волнует, так как разбираться голову сломаешь.
Результатом же использования правильной логики является отсутствие противоречий или своевременное выявление оных. По крайней мере мой технический ум в это верит. Но трудно заставить заказчика мыслить и говорить так, как хотелось бы программисту, поэтому будем искать конструктивный компромисс на программном уровне.
Перейдем к конкретике и разберем упрощенный пример. Давайте включим воображение и предположим, мы ведем базу с договорами. Пусть в базе ведутся договора типа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 взаимоисключающие ?
- Как вести знания о предметной области и как формулировать ТЗ, чтобы не было противоречий?
Первый вопрос я уже пытался осветить в статье [ссылка]. Кратко — я предлагал внедрять в код списки типов, который он имеет право обрабатывать и сигнализировать о появлении нового типа, если вдруг новый тип попадет в подпрограмму. Кстати этот прием я тоже уже опробовал на проекте, и он дает свои плоды, добавляя ясности в процесс эксплуатации при неопределенных ситуациях. (update: прием присутствует в защитном программировании)
Сейчас хочу осветить второй и третий вопросы, — как определять своевременно противоречия в автоматическом режиме. Ведь когда проект переваливает за несколько миллионов строк, то сложно добавить новый код, чтобы он чего-нибудь не попортил старого. По сути я предлагаю прикрепить к бизнес-объекту некий умный контейнер, куда вписывать все требования. А этот контейнер уже сам будет проверять, — насколько новые пункты ТЗ уживаются со старыми. То есть когда приходит новый пункт ТЗ, то программист тупо перекладывает его “как пришло” на язык программирования, сохраняя заложенную бизнес-логику. Должен получиться такой эффект, что если смотреть на получившийся код и на ТЗ, то должно быть понятно без особый усилий, — как они связаны. Затем он помещает код в умный контейнер, который является связующим звеном между бизнес логикой и строгой логикой. Насколько это возможно программно — выношу на суд читателей. Далее описываю свое решение.
Для этого во первых я предлагаю отделить вычисление условий и сами действия. То есть разнести if-ы отдельно от do_something. При вычислении условий запрещается выполнять какие-либо изменения в базу данных или в глобальные переменные. Это ближе к функциональному программированию и чистой логике. Ничего не изменяется во внешнем контексте, а только вычисляются логические выражения. Внедряются более мягкие связи между условиями из ТЗ и действиями программы. На первом этапе контейнер формирует текст — легко интерпретируемый как человеком, так и любой программой.
На выходе на первом этапе контейнер выдает:
- Основной минисценарий в виде небольшого текста или простой структуры данных.
- Дополнительные предупреждения и рекомендации в виде текста.
- Ошибку в виде текста или возбуждает исключения в зависимости от пожеланий.
Схема формирования минисценария выглядит так:
Рисунок 1. Схема первого этапа
После того, как минисценарий сформирован, то получается, что большие обобщенные тексты ТЗ редуцируется в некий компактный читабельный текст, подходящий для конкретного бизнес-объекта (договора, клиента, документа и тп) в его текущем состоянии. Этот текст уже ближе к конкретным действиям в базе данных и элементарным операциям, но тем не менее он достаточно абстрактен и не зависит от базы или среды. Далее на следующих этапах с ним можно делать много полезных вещей до того, как по сценарию будут внесены изменения в базу данных.
Какие из этого ожидаются плюсы:
- Можно играть с этим контейнером как угодно, так как он не изменяет данные, а возвращает только текст. Можно гонять его по всей базе, не боясь чего-то изменить. Можно делать регресс-тестирование, сравнивая новые и старые тексты минисценариев. Можно делать прогноз, — что будет делать программа в том или ином случае.
- Реализуется полезный принцип слабой связи, когда чистая логика и чистые изменения разносятся в разные подпрограммы.
- Если дорисовать функционал, то через конфигурацию можно включать или выключать какие-то блоки. Особо актуально, когда нет доступа на бой, но надо отключить какие-то глючные куски.
- Текстовки можно делать на родном языке, что увеличивает понимание для специалистов других специальностей, — тестеров, бизнес аналитиков, пользователей и тп.
- Самое важное, о чем я бы хотел поведать — это автоматический контроль противоречий, который опишу ниже.
Какие минусы:
- Надо вычислять все условия, — и новые и старые, то есть со временем будет работать медленнее, но зато правильно. Уходим от преждевременной оптимизации и вспоминаем слова Дональда Кнута «Premature optimization is the root of all evil».
- В языках программирования еще нет кратких и красивых конструкций, которые бы принуждали программиста писать правильно по паттерну. Потенциально все еще можно пойти против паттерна и внедрить противоречивую логику туда, где её не должно быть. То есть требуется некоторая дисциплина от разработчика, чтобы он добавлял новые условия ТЗ по определенным правилам в определенное место.
Переходим к описанию реализации. Входными параметрами являются (рисунок 1):
- Данные, идентифицирующие бизнес-объект. В самом простом случае это может быть просто ссылка на java объект или ID объекта в базе данных. Но так как эта функция может вызываться из очень разных мест, — в том числе, от туда, где прямой ссылки на объект нет, то на практике я делал универсальный параметр, и в первом блоке (рис.1) производил поиск объекта.
- Второй параметр, — команда. Например, “установи номер бух. счета” или “установи следующий статус” и тп. Команда предназначена для оптимизации и повышения гибкости. Мне не практике она была не нужна, так как я всё время выдавал полный минискрипт по текущему объекту, а затем обработчик минискрипта решал, что ему нужно исполнить в зависимости от ситуации.
В следующих блоках происходит вычисление всех условий по всем ТЗ без оптимизации и логгирование. Логи я использовал только на сервере тестирования, чтобы быстро реагировать на замечания тестировщика. В логи вписывал присланные мне фразы из ТЗ, чтобы было на что сослаться при разборе полётов. То есть я использовал такой подход:
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. Установка значений для атрибута более одного раза, особенно если значения разные, — это и есть печально известный случай противоречий. В коде ниже больше подробностей.
Инфраструктура применения имеет такой вид:
Рисунок 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
Комментарии (14)
third112
14.11.2017 12:57ИМХО причина обсуждаемой ошибки: естественное желание сделать лаконичный код. Классическое решение согласно принципам защитного программирования: избыточность.
case doctype of 1: case clienttype of 1: do_something1; 2: do_something2; 3: do_something3; else error (unknownClienttype, clienttype); end; 2: case clienttype of 1: … 2: … 3: … else error (unknownClienttype, clienttype); end; 3: case clienttype of 1: … 2: … 3: … else error (unknownClienttype, clienttype); end; else error (unknownDoctype, doctype); end;
Фактически это та же матрица сочетаний, но развернутая в линию кода программы. Функция error выводит характер ошибки и значение переменной, ее вызвавшее. В случае необходимости нетрудно написать генератор, который сделает шаблон подобного кода.
Для лучшей ориентации в коде значения нужно заменить на константы с осмысленными именами:
const ORGANIZATION = 1; … case clienttype of ORGANIZATION: do_something1;
Вместо многоточий стоит поставить комментарии:
{ToDo}
При возможности лучше сразу писать целевой код, а не создавать внекодовую матрицу, т.к. при переносе из нее в код возможны опечатки. А для тестирования и документации этот код автоматически с помощью несложного конвертора можно превратить в матрицу, нпр., в Excel.dim2r Автор
14.11.2017 13:06Поддерживаю данный подход!
Только, если атрибута 3, то это уже трехмерная матрица. Если их будет штук 40, то полное сочетание в case операторе будет монстром. При добавлении нового атрибута надо будет еще во все места case оператора дописывать еще один уровень вложенности.
Плюс Вы не поняли еще одну фишку моего подхода — дополнительные действия с минисценариями. Их можно анализировать без внесения изменения в базу данных. Например, можно делать легкое регрес-тестирование или прогонять по всей базе не боясь чего-то изменить.third112
14.11.2017 13:17Ничего не мешает сделать подобный код и для 40 атрибутов. 40 уровней для вложенных case, конечно, будет очень объемным. И конечно его надо генерировать с помощью несложной программы или скрипта, а не писать руками, чтобы не ошибиться. На каждой вершине стоит перечислить значения атрибутов:
do_something1; // clienttype=ORGANIZATION doctype=...
Но ИМХО ничего страшного — главное уверенность, что это будет верно работать и легко модифицироваться!
third112
14.11.2017 13:24PS
еще одну фишку моего подхода — дополнительные действия с минисценариями
Ok. Я предложил альтернативное решение. Как часто бывает везде свои ++ / --.third112
14.11.2017 13:39PPS Прикинул, что 40 уровней может быть многовато для моего предложения…
dim2r Автор
14.11.2017 14:02Я тоже подход с case-ом когда то пытался проанализировать. Это похоже на промежуточный вариант между тем, что пишут в ТЗ и мат. логикой, которая основана математических конструкциях.
Моя идея — пиши код почти как написано в ТЗ, а умная оболочка разруливает нестыковки. Если использовать свой язык, как Вы предложили, то это будет ближе к моим идеям. Так на программисте меньше ответственности за ошибки.third112
14.11.2017 14:45Моя идея — пиши код почти как написано в ТЗ, а умная оболочка разруливает нестыковки. Если использовать свой язык, как Вы предложили, то это будет ближе к моим идеям. Так на программисте меньше ответственности за ошибки.
Да, только я использовал не свой язык, а Delphi. Вы отметили важный принципиальный момент: чем больше сходство кода с ТЗ — тем он убедительнее.
vasiliy404alfertev
14.11.2017 22:59Мне эта проблема напомнила про Drools. www.youtube.com/watch?v=GvN9W67Bscs&t=1s
Когда бизнес-логика усложняется, а ТЗ является просто коллекцией утверждений, требуется экспертная система для проверки противоречий.dim2r Автор
15.11.2017 09:08Спасибо за ссылку,
Надо будет попробовать реализовать пример из ролика на своем движке
sshmakov
Вы в начале привели таблицы, из которых явным образом следует, что в ТЗ есть противоречие. Почему этих таблиц нет в ТЗ?
dim2r Автор
даже если бы были, — ТЗ не программа, оно само не проверяет противоречия
sshmakov
Вы тоже, но противоречие нашли.
Хотя я понимаю, откуда ноги растут — у вас, скорее всего, составляются ТЗ на доработку, а документа, содержащего актуальное описание функциональности, с которым можно сверяться при подготовке ТЗ — нет. Так?
Если что, я отвечаю на ваш же вопрос в статье:
dim2r Автор
Так и есть.
Система мутирует много лет и меняются законы для бизнеса, поэтому на ТЗ, которому больше пары лет — уже никто не смотрит.