Изначально программирование выросло из математики, но со временем потеряло строгость, привычную для математики. Тем не менее связь с математикой продолжает существовать. Для решения некоторых проблем на помощь приходят разделы дискретной математики и логики, которые вносят ясность и однозначность. Дискретная математика хорошо подходит для формализации систем автоматизации бизнеса, так как большинство бизнес процессов имеет дискретные характеристики. Например, количество типов договоров исчисляется единицами, десятками, реже сотнями. Даты тоже имеют дискретную природу, их количество пропорционально количеству дней в году. Денежные операции, которые вроде бы описываются числами с плавающей точкой, на самом деле в большинстве случаев точно описываются целым числом копеек или центов. Поэтому количество копеек или центов, которое прошло через организацию за все время ее существования описывается вполне конкретным натуральным числом. И по меркам современной математики все эти числа совсем небольшие.
Для математика, постановка вопроса ясна. Имеется счетное ограниченное количество объектов, счетное ограниченное количество степеней свободы, состояний объектов и тд и тп. Все характеристики поддаются простому счету. Оценки и подсчеты для счетных конечных множеств не составят труда. Человек, прошедший курс комбинаторики или дискретной математики, сможет наметить план как можно формализовать все возможные состояния системы и как выделить корректные и некорректные состояния. Вот что может дать математика:
- подсчет количества возможных вариантов состояний объекта;
- доказательство полноты, учтены ли все варианты или есть неучтенные;
- доказательство наличия или отсутствия некорректных состояний;
- поиск примеров, которые приведут к некорректным или корректным состояниям;
- доказательства эквивалентности тех или иных действий;
- и многое другое.
Теперь постараемся разобраться с коварным врагом, который затрудняет этот процесс. Например, автор ТЗ передает некий список типов договоров, для которых будет выполнятся какое-то действие. Иногда в ТЗ может появиться фраза “все типы договоров” или “все остальные типы договоров, кроме...”. Программист стремится выполнить ТЗ правильно при недостатке времени и давлении со стороны бизнеса. Задумываться о том, насколько программа готова к новым изменениям зачастую не приходится, — лишь бы пройти тесты, исправить замечания, сдать, отчитаться, идти дальше.
Допустим, программист делает процедуру, которая подставляет номер счета в бухгалтерской проводке. Увидев в ТЗ список всех типов договоров которые присутствуют в системе или увидев фразу “все типы договоров”, программист делает первую ошибку — он обобщает, что может быть даже приятно для мозга, так как можно выразить мысль кратко. В результате его процедура, которая подставляет счет не анализирует тип договора. На первый взгляд никакого криминала нет.
Вот чем подвох — видите ли, понятие “все типы договоров” на момент написания ТЗ включало 10 типов. Потом это понятие изменяется, и через полгода добавляется еще пара типов. Такой случай не является проблемой в маленьких проектах, скажем до миллиона строк, где есть преемственность опыта и программисты быстро учатся друг у друга.
В больших же проектах, существует намного больше мест, где возможна вставка нового обработчика. Первый программист уже давно переключился на другие задачи и забыл где и как подставляется счет в зависимости от типа. Задачу обработки нового типа договоров могут поручить другому программисту, который изучив систему, нашел новое место, куда можно добавить обработчик. Например, второй допишет обработку в ином месте: на клиенте, в процедуре на сервере, или в триггере или где-то еще. Из-за таких блужданий логика со временем становится мало понятной. Обрабатывая то правила, то исключения из правил в самых неожиданных местах, и код запутывается.
Если повезёт, то новый тип договора будет иметь мало отличий от предыдущих. И скорее всего обработчики, которые допускали обобщения и не анализировали попадание незнакомого элемента будут работать корректно. Но каждый опытный программист, встречался с ситуацией, когда заказчик приводил новые и новые требования, ломающие тщательно выстроенные схемы. Трудно предугадать, как поведет себя программа после добавления нового типа договора, которая развивалась несколько лет коллективом в десятки или сотни сотрудников, которые то и дело допускали частичные или полные обобщения или разночтения. Старые универсальные процедуры и функции, начинают постепенно работать под всё новые и новые типы договоров, и от их работы зависят все больше и больше данных. Со временем становится все более рискованно влезать в эти процедуры и функции, так как любое исправление может повредить системе, и придется долго исправлять кривые данные, восстанавливаться из резервных копий, анализировать историю изменений. Из-за разночтений появляются люфты, там где их не должно быть и жесткие конструкции там, где они не к месту. Проект становится похожим на автомобиль, у которого заклинило коробку передач, а руль болтается туда-сюда.
Выход из такой ситуации напрашивается сам собой — надо фиксировать списки типов договоров на этапе технического задания или последующего технического дизайна, а в случае, если программа встречает незнакомый ей тип, то выдавать ошибку. Так убираются обобщения, вредные для формализации, а программа будет автоматически сигнализировать из всех мест, если вдруг ей встретится новый неучтенный тип. Разработчики же будут знать, где нужно учесть новые элементы, а не использовать каждый раз новый вариант вклинивания в старый код.
Сколько от этого пользы судите сами.
- Уменьшаются разногласия между составителем ТЗ и разработчиком. Общие слова подкреплены конкретикой.
- Уменьшается вероятность того, что в отлаженный алгоритм попадут параметры, на которых он не тестировался.
- Становятся видны места в коде, где надо добавлять обработку новых типов.
- Повышается уровень дисциплины в коде. Увеличивается вероятность, что логика будет концентрироваться в одном и том же месте, а не будет разбросана.
- Облегчается формализация и автоматический анализ кода.
Я не предлагаю хардкодить списки, когда их можно вести в базе. Просто привожу простейшие примеры для иллюстрации принципов на псевдокоде, похожем на pl/sql.
//Пример 1.
//Неверный код, нет проверки на попадание нового типа.
//Список изменяется со временем, появляются разночтения с начальным ТЗ.
//Со временем этот кусок кода влияет на все большее количество данных,
//поэтому изменения становится рискованным.
forall type_id in (all_type_list) loop
do_something;
end loop;
//пример 2
//тоже
if type_id not in (555,666) then
do_something;
end if;
//пример 3
//Неверный код, нет проверки на попадание нового типа.
//В случае нового типа выполнение продолжается без предупреждений
//Действие не выполняется, и на это могут не обратить внимания
if type_id in (333,444) then
do_something;
end if;
//////////////////////////////////////////////////////////////////////
//Верный код и его развитие. ////////////////////
//Пример 4.
if type_id in (1,2,3) then
do something;
else
//Попался неизвестный тип, сигнал добавить новую обработку, см ниже.
raise error;
end if;
//Развитие кода для новых типов 4,5,6.
//Вариант 1, новые типы обрабатываются стандартно по старому.
if type_id in (1,2,3,4,5,6) then
do_something;
else
raise error;
end if;
//Вариант 2, новые типы обрабатываются особо.
if type_id in (1,2,3) then
do_something;
elsif type_id in (4,5,6) then
do_something_2;
else
raise error;
end if;
Планирую написать продолжение на темы:
- как обрабатывать связки условий типов/под-типов
- как автоматически обнаруживать противоречивые требования в ТЗ
- как автоматизировать составление списков и позволить аналитику ими управлять
- как использовать математику для анализа системы
- развитие проекта через мета-системы
Д.А.Рыбаков, 2016
к.т.н.
Комментарии (46)
i360u
18.10.2016 13:16Перечисляемые типы вроде как для этого и созданы? И всякие велосипеды для их эмуляции там, где они "by design" не предусмотрены? И это, вроде как, для всех, кто хоть как-то работает со структурами данных обычная практика? Или я неправильно все понял?
dim2r
18.10.2016 14:00>И это, вроде как, для всех, кто хоть как-то работает со структурами данных обычная практика?
Выдавать ошибку, когда встретился незнакомый тип, «by design» не обеспечивает. По крайней мере на уровне языков такого не встречал.i360u
18.10.2016 14:25Так, а в чем проблема получить тип в месте использования, как возвращаемое значение какого-нибудь статического метода какого-нибудь абстрактного класса со встроенной обработкой ошибок в одном месте? А если поддерживаемые модулем типы (при слабой связанности) отправлять, при этом, в качестве аргумента, чтобы отлавливать любые нестыковки? Повторюсь: перечисляемые типы ведь для этого и созданы?
dim2r
18.10.2016 14:39Способов сделать много, я как раз планировал в продолжение написать как сделать, что бы программист не заботился о списках в коде. Что-то типа
if Is_in_list(type_id, "список типов, ТЗ от 01.01.2016") then do something; end if;
В функции is_in_list() выдывать исключение и мгновенно оповещать разработчика или аналитика, даже если исключение возникло у другого пользователя. Так даже пользователю не надо будет звонить в поддержку, что бы разобраться. Поддержке или программисту придет сообщение
«При обработке 'список типов, ТЗ от 01.01.2016' возникло исключение в таком-то куске кода. Требуется либо добавить новый тип в список, либо внести изменения в код для особой обработки этого типа»i360u
18.10.2016 14:55Во первых, список регламентированных типов и поддерживаемых в конкретном месте, может не совпадать. Но, при этом, в рамках конкретного места использования тип не может принимать значение отличное от заданного для данного логического контекста. И это далеко не обязательно ошибка, это может быть рабочей ситуацией в каком-нибудь модуле или являться частью общей эволюции модулей при развитии системы. Во вторых, как мы видим, это все легко разруливается различными методами и основаны все они на стандартизации типов, которую вы, собственно, и предлагаете. Для стандартизации существуют всякие встроенные возможности, типа "перечисляемых типов" и, как тут уже писали, чего-то типа assert. Т. е. вы хотите открыть людям глаза на какую-то довольно стандартную практику? В таком случае — согласен, стандартизация — это хорошая практика, но это же довольно банальное умозаключение. Вот я и интересуюсь, вы действительно имели в виду, что "трава — зеленая", или я что-то упустил?
dim2r
18.10.2016 15:06приведите пожалуйста пример, что бы легче понять Вашу мысль
Fortop
19.10.2016 11:42+1Список договоров в модуле операциониста по работе с юр. и такого же операциониста с физ.лицами отличаются.
Более того может существовать некий отдел снабжения, который работает с третьим непересекающимся списком.
При этом все они являются регламентированными
dim2r
18.10.2016 15:12>но это же довольно банальное умозаключение
да банально, не спорю,
Есть анекдот от Мамонова про старого гитариста.Выходит молодой гитарист и начинает играть и левой рукой и правой и вверх тормашками и за спиной и над головой. Выходит старый именитый гитарист, сел на стульчик и начинает на трех аккордах играть. Его спрашивают — «чего ж ты такой примитив гонишь? вон видишь молодой как скачет». «Вы понимаете», спокойно отвечает старый — «он еще ищет, а я уже нашел».
Scf
18.10.2016 23:32Идея правильная, но не является правильной на все случаи жизни. Подробные ТЗ на каждый "тип договора" и никаких обобщений — это рабочая и масштабирующаяся схема, но весьма дорогая в применении.
У меня на прошлой работе был как раз такой пример — когда на каждый тип договора аналитик писал ТЗ с табличкой что куда сохранять и как, тестировщик писал автотесты, а мы имплементили согласно ТЗ. В итоге у нас было очень мало багов… и очень длинный цикл разработки. В то время как заказчик хотел "сделайте как у типа 123, но с такими-то изменениями". И пусть даже там будут баги, зато эти десятки новых типов будут добавлены за разумное время.
dim2r
19.10.2016 08:32Было бы интересно узнать ваш опыт.
Возможно истина где-то посередине. Программа как-то сама должна фиксировать списки типов на момент её написания и выдавать ошибки, не всегда, а только когда, к примеру, тестер захочет протестировать в строгом режиме.Fortop
19.10.2016 11:55Вам уже предложили рабочее решение этого вопроса.
Контракты на каждый тип, который влияет на логику.
Можно поручить этот вопрос кодогегерации и миграциям.
Но чаще всего достаточно реализации их вручную.
Что касается ошибок, то debug mode это обычная практика.
Так же как и логи разных уровней.
Вы, похоже, действительно имеете очень мало практического опыта.dim2r
19.10.2016 12:29Вам уже предложили рабочее решение этого вопроса.
Извините, но предложенный шаблон не решает проблему, которую я поднял без кода, который я привел.
Что касается опыта, можете считать что его нет, но это видимость из-за того, что написал слишком просто для уровня студентов. Сначала надо с простым до конца разобраться.
Fortop
19.10.2016 12:35Она и не будет решаться без кода.
Поскольку в самой постановке задачи ваш некий новый тип имеет поведение отличное от других.
Вы можете это запрограммировать и сделать конфигурируемым… Но в сложных проектах это потребует прикладников внедренцев, как в тех же 1С, SAP.
А в более простых сложная система конфигурации нерентабельна и проще при очередной модификации дописать несколько конкретных моделей, которые реализуют специфичное поведение для конкретного типа.dim2r
19.10.2016 12:47дописать несколько конкретных моделей
Хорошо, когда вы знаете что дописывать. В моем проекте из-за большого объема заранее неизвестно что надо дописывать.Fortop
19.10.2016 12:58Это не соответствует действительности.
Если у вас поведение модели домена детерминировано согласно ТЗ, то вы уже знаете что дописывать.
Возможно вам потребуется ознакомиться с кодом или документацией к нему, чтобы найти подсистемы отвечающие за конкретные аспекты.
Но и не более того.
Во всех остальных случаях ваша модель должна наследовать и реализовывать конкретный общий интерфейсdim2r
19.10.2016 13:26Возможно вам потребуется ознакомиться с кодом или документацией к нему
Имеется 2 Гб кода, котрый находится в постоянном развитии. На некоторые модули по 10 человек очереди. Тут нужны навыки человека дождя. :):):)Fortop
19.10.2016 13:36Я вас огорчу.
Редко поведение вашей прикладной модели отличается от уже реализованных настолько, что требуется изменить все 2 Гб кода (возможно удивлю, но небольшой проект на node.js может подтянуть в себя 0.5 Гб зависимостей в коде).
Поэтому в рамках вашей задачи по наращиванию системы не новым, а модификацей уже существующего функционала вам редко потребуется взаимодействовать с кодом более 10-20 тысяч строк.
В противном случае вам лучше произвести декомпозицию задачи.
Что касается очереди…
То современные системы контроля версий позволяют вести параллельную разработку над одним модулем (git/mercurial).
Намного больше сложностей у вас возникает в случае монолитных систем.dim2r
19.10.2016 13:52Извините, я не огорчусь, ибо огорчаться нечему. Наоборот, даже интересно пообшаться со знающими людьми. Чем критичнее человек настроен, тем лучше воспринимает сказанное. К сожалению, не могу раскрывать детали происходящего на проекте, а то бы можно было бы обменяться более плодотворными идеями. Удачи
Fortop
19.10.2016 13:56В проекте на 100 человек, если он ещё не развалился, уже есть знающие люди.
Пользуйтесь внутренней экспертизой.
Минимум 3-5 человек адекватного уровня и опыта у вас должны бытьdim2r
21.10.2016 12:03Я в общем сам знающий и хорошо умею считать. Поэтому я не верю слепо всему, что говорят авторитеты и не боюсь, когда мне не верят. :)
samizdam
Если посмотреть с точки зрения объектно-ориентированной парадигмы, то озвученная проблема решается следующим образом:
Есть абстрактный тип данных «Договор» в виде интерфейса или абстрактного класса. Изначально, в первой версии ТЗ, система реализует N конкретных типов договора, каждый из которых представлен классом наследником абстрактного.
Весь код в системе, который должен обрабатывать различные конкретные типы знает только об общем интерфейсе.
Т.о. нет нужды плодить проверки if / else if / else по type_id, т.к. вся специфичная логика окажется локализованной в специальных классах. Это будет проще в поддержке, поскольку разработчикам не будет нужды беспокоиться о размазанной по программе специфике работы с разными договорами, можно будет мыслить на одном из уровней абстракций: либо конкретного подтипа, либо более общего кода.
dim2r
>Есть абстрактный тип данных «Договор» в виде интерфейса или абстрактного класса.
В ООП проблема остается, абстракция — это обобщение. Свойства договора на момент ТЗ фиксированы. Это очевидно. Вопрос — что будет в будущем? Появится новый тип договора и заказчик не гарантирует, что он будет попадать под старые свойства. Приведенный метод сразу будет сигнализировать во всех местах, где нужно обрабатывать новый тип. Как такое можно сделать на ООП — вопрос открытый? Какие есть идеи?
samizdam
Новый тип договора — новый тип в коде.
Меняются свойства — меняется код. Концепция Domain driven design. DDD.
Код должен быть покрыт тестами. Меняются требования — находим тесты их покрывающие, адаптируем к изменениям, убеждаемся, что тесты упали. Адаптируем код для прохождения тестов в условиях новых требований. TDD методология разработки.
dim2r
тесты и ООП — это разные вещи, друг друга не исключают
samizdam
А разве кто-то утверждает что исключают?
На мой взгляд, всё обсуждение умещается в одной фразе: «Любую проблему можно решить ещё одним уровнем абстракции. Кроме излишней абстракции.».
Находить удачные абстракции, которые мало «текут» со временем, не переусложнены и жизнеспособны в эксплуатации системы, это навык. В общем случае решения у этой проблемы нет. Есть лишь набор практик, популяризируемых признанными в области экспертами, и некоторые шаблонные решения проектирования, оторванные от конкретики.
Мне не совсем понятна мотивация для использования Вашего подхода. В многих языках есть конструкция утверждений (assert), по логике в приведённом псевдокоде стоило бы использовать её.
Только зачем, если проблема в сообществе DDD давно решена более легкими в поддержке и изящными способами?
dim2r
>В многих языках есть конструкция утверждений (assert), по логике в приведённом псевдокоде стоило бы использовать её
Главное, что бы принцип был понятен, а использовать assert или еще что-то другое — не важно. От перемены названия мало что меняется.
>Только зачем, если проблема в сообществе DDD давно решена более легкими в поддержке и изящными способами?
приведите пример
samizdam
Если на уровне кода мы должны отличать каждый из подтипов домена, то надо реализовывать их отдельными типами, реализующими общий для этого домена интерфейс.
Специфика каждого типа располагается в нём самом, изменения локализованы и легко тестируемы, а клиентский код работает с абстракцией.
dim2r
Я про другое — если Вы в базу вносите новый тип договора, то как будет реагировать старая программа? Будет тихо с ним работать или всё таки возмутится? Сможете ли вы сразу определить где вносить правки для обработки нового типа?
samizdam
Так вот вы о чём.
В описываемой Вами системе есть проблема разделения данных и логики их обработки. Вы предлагаете хардкодить в логике данные. Данные, как правило, более изменчивы чем логика. Такой путь к хорошему не приведёт, т.к. придётся менять каждый раз менять исходный код при изменении данных.
Если некоторые данные влияют на логику их обработки, значит это уже не просто данные, а часть кода. Помочь могут перечислимые типы, как писал ниже i360u.
Я вижу одно локализованное место для их использования: в коде инстанцирующем конкретные экземпляры объектов (репозиторий / фабрика), которые сопоставляют данные с подходящей реализацией.
Различия стоит локализовать в конкретных типах, а не размазывать по коду приложения. Иначе это будет напоминать ситуацию описанную в недавней статье: https://habrahabr.ru/post/312792/
dim2r
>Вы предлагаете хардкодить в логике данные
Нет не предлагаю хардкодить.
Я описал принцип и максимально примитивно его продемострировал.
Собирался написать продолжение кому и как вести эти списки.
dim2r
>Такой путь к хорошему не приведёт, т.к. придётся менять каждый раз менять исходный код при изменении данных.
Что-то вроде этого,… по моему мнению, код должен реагировать на незнакомые типы. Как именно он будет их определять — есть разные варианты. Хардкод, кончено, примитивный, но довольно наглядный.
dim2r
Я вижу одно локализованное место для их использования: в коде инстанцирующем конкретные экземпляры объектов (репозиторий / фабрика), которые сопоставляют данные с подходящей реализацией.
Хорошая фабрика, пожалуй, решит задачу, только в неё придется вставить код, похожий приведенный в статье.
samizdam
На самом деле с нормальной ORM и этого кода не надо, т.к. там скорее всего будет поддержка наследования сущностей исходя из данных.
dim2r
с простым случаем разобрались наконец
Fortop
Стойкое ощущение от вашего диалога, что вам нужно писать материал по теме, и вы таким образом его собираете…
Во всяком случае ваш собеседник выглядит и много профессиональнее. И предлагает реальное рабочее решение.
Странно что это элементарное решение не фигурирует в заметке.
Вместо ужасной идеи перечислимых списков.
dim2r
Не, мне не нужно писать материал, я делюсь своим опытом.
Дело в том, что я работаю на большом проекте и последнее время сталкиваюсь с данной проблемой в разных вариантах. Я даже в статье написал, что проблема возникает в большом проекте, когда один программист что-то делает, а потом другой подхватывают через длительное время.
Fortop
Это возникает в большинстве проектов занимающих больше нескольких человеко-лет.
И развивающихся в процессе своей жизни.
В общем предлагаю развить и закончить вашу мысль.
Поскольку в таком виде это скорее как антипаттерн для средних и больших проектов (а таковыми считаются, насколько я помню, проекты более 3-5 человеко-лет для средних и свыше 10-15 для больших)
dim2r
В общем предлагаю развить и закончить вашу мысль.
На момент написания статьи я даже не знал, что её кто-то прочитает, поэтому не вкладывался. Если найдет вдохновение, то подкреплю фактами.
Поскольку в таком виде это скорее как антипаттерн для средних и больших проектов
В моем проекте объем примерно 20 лет * 100 чел. Мне доступно примерно 2 Гб кода, примерно 20 млн строк кода. Сколько кода всего в системе сказать не могу. Такие дела
dim2r
«Любую проблему можно решить ещё одним уровнем абстракции. Кроме излишней абстракции.».
Я как раз против абстракций.
Надо Вам пару гигабайт кода кинуть, что бы Вы там в чужих абстракциях пару месяцев погуляли.
samizdam
Зато Вы, судя по предлагаемому решению сугубо за хардкод айдишников с дублированием.
Спасибо за предложение, мне вполне хватает исходников, и на работе и дома.
Вам со своей стороны тоже могу пожелать почаще писать, не только на псевдо языке, побольше коммерческих и OS проектов.
dim2r
>Зато Вы, судя по предлагаемому решению сугубо за хардкод айдишников с дублированием
Не я против, Вы придираетесь. Чем сильнее придираетесь, тем лучше поймете мысль. Хардкод исключительно для демонстрации
dim2r
>Концепция Domain driven design. DDD.
Кстати, не все так хорошо с DDD.
Цитатата из https://habrahabr.ru/post/313110/
«DDD Работает хорошо в устоявшихся бизнес-процессах»
Я-то поднял проблему изменяющихся бизнес-процессов. С устоявшимися все понятно и так.
potan
В ООП эта проблема не актуальна — общий метод легко переопределяется в новых классах, если это требуется. Проверить, что нужные методы переопределены для нового типа в соответствии ТЗ при ревью не сложно — достаточно посмотреть на сигнатуру класса. Инструменты по документированию создают удобные для этого отчеты.
В императивном подходе эта проблема есть..., но это не самая большая проблема императивного программирования.
dim2r
>общий метод легко переопределяется в новых классах, если это требуется
Если вы знаете что переделывать. А если вам досталось от предыдущих разработчиков сотни пакетов, большинство которых порядка 10 тыс строк кода, процедуры со 120ю параметрами и многое другое, то разобраться сразу затруднительно куда вставлять обработку нового типа.
В моем же варианте кода (пусть не самом лучшем) все места сразу «зазвонят», когда попадется новый тип.
potan
Диаграмы классив очень помогают разобраться в унаследованном коде. В ООП еще можно при начале работы с чужим кодом провести рефакторинг (если тесты есть), после которого становится ясно, куда новый класс пихать.
Вообще, программистам не следует делать то, что компилятор и инструмены сделают лучше.
dim2r
Специфика больших проектов в том, что есть зоопарк инструментов, используются старые технологии вперемешку с новыми, никто не видит полной картины и тд и тп. Зачастую непонятен стек вызова. Бывает такое: Нажимаешь на кнопку, она дает ошибку. Начинаешь разбираться, открываешь проект. В проекте кнопки нет. Через день понятно, что она рисуется через WinAPI. Далее она подгружает dll специальным загрузчиком с сервера. Там вызывается COM объект. COM открывает форму, в которой все компоненты получают данные через HTTPS от объектов на сервере. Объекты на сервере вызывают движок ActiveScript, который берет скрипты из базы данных, которые сами берут данные из хранимых процедур, которые возвращают refcursor. На третий день нашел где ошибка, поправил одну строчку.
Как это можно загрузить в диаграмму — непонятно.