Comments 48
Есть абстрактный тип данных «Договор» в виде интерфейса или абстрактного класса. Изначально, в первой версии ТЗ, система реализует N конкретных типов договора, каждый из которых представлен классом наследником абстрактного.
Весь код в системе, который должен обрабатывать различные конкретные типы знает только об общем интерфейсе.
Т.о. нет нужды плодить проверки if / else if / else по type_id, т.к. вся специфичная логика окажется локализованной в специальных классах. Это будет проще в поддержке, поскольку разработчикам не будет нужды беспокоиться о размазанной по программе специфике работы с разными договорами, можно будет мыслить на одном из уровней абстракций: либо конкретного подтипа, либо более общего кода.
В ООП проблема остается, абстракция — это обобщение. Свойства договора на момент ТЗ фиксированы. Это очевидно. Вопрос — что будет в будущем? Появится новый тип договора и заказчик не гарантирует, что он будет попадать под старые свойства. Приведенный метод сразу будет сигнализировать во всех местах, где нужно обрабатывать новый тип. Как такое можно сделать на ООП — вопрос открытый? Какие есть идеи?
Меняются свойства — меняется код. Концепция Domain driven design. DDD.
Код должен быть покрыт тестами. Меняются требования — находим тесты их покрывающие, адаптируем к изменениям, убеждаемся, что тесты упали. Адаптируем код для прохождения тестов в условиях новых требований. TDD методология разработки.
На мой взгляд, всё обсуждение умещается в одной фразе: «Любую проблему можно решить ещё одним уровнем абстракции. Кроме излишней абстракции.».
Находить удачные абстракции, которые мало «текут» со временем, не переусложнены и жизнеспособны в эксплуатации системы, это навык. В общем случае решения у этой проблемы нет. Есть лишь набор практик, популяризируемых признанными в области экспертами, и некоторые шаблонные решения проектирования, оторванные от конкретики.
Мне не совсем понятна мотивация для использования Вашего подхода. В многих языках есть конструкция утверждений (assert), по логике в приведённом псевдокоде стоило бы использовать её.
Только зачем, если проблема в сообществе DDD давно решена более легкими в поддержке и изящными способами?
Главное, что бы принцип был понятен, а использовать assert или еще что-то другое — не важно. От перемены названия мало что меняется.
>Только зачем, если проблема в сообществе DDD давно решена более легкими в поддержке и изящными способами?
приведите пример
Специфика каждого типа располагается в нём самом, изменения локализованы и легко тестируемы, а клиентский код работает с абстракцией.
В описываемой Вами системе есть проблема разделения данных и логики их обработки. Вы предлагаете хардкодить в логике данные. Данные, как правило, более изменчивы чем логика. Такой путь к хорошему не приведёт, т.к. придётся менять каждый раз менять исходный код при изменении данных.
Если некоторые данные влияют на логику их обработки, значит это уже не просто данные, а часть кода. Помочь могут перечислимые типы, как писал ниже i360u.
Я вижу одно локализованное место для их использования: в коде инстанцирующем конкретные экземпляры объектов (репозиторий / фабрика), которые сопоставляют данные с подходящей реализацией.
Различия стоит локализовать в конкретных типах, а не размазывать по коду приложения. Иначе это будет напоминать ситуацию описанную в недавней статье: https://habrahabr.ru/post/312792/
Нет не предлагаю хардкодить.
Я описал принцип и максимально примитивно его продемострировал.
Собирался написать продолжение кому и как вести эти списки.
Что-то вроде этого,… по моему мнению, код должен реагировать на незнакомые типы. Как именно он будет их определять — есть разные варианты. Хардкод, кончено, примитивный, но довольно наглядный.
Хорошая фабрика, пожалуй, решит задачу, только в неё придется вставить код, похожий приведенный в статье.
if type_id in (1,2,3) then
return new class instance;
else
throw error;
end if;
interface Contract {}
class ContractA implements Contract{}
class ContractB implements Contract{}
class Contract_100500 implements Contract{}
class ContractRepository() {
enum ContractType {
A,
B,
_100500
}
public Contract getById(int id) {
// вот здесь по хорошему _единственное_ подходящее место
// для свитча по перечислению имеющихся типов
// алгоритм инстанцирования сводиться к
// 1. пойти в хранилище, взять строку по ID,
// 2. исходя из полученных данных инстанцировать нужную реализацию.
}
}
На самом деле с нормальной ORM и этого кода не надо, т.к. там скорее всего будет поддержка наследования сущностей исходя из данных.
Во всяком случае ваш собеседник выглядит и много профессиональнее. И предлагает реальное рабочее решение.
Странно что это элементарное решение не фигурирует в заметке.
Вместо ужасной идеи перечислимых списков.
Дело в том, что я работаю на большом проекте и последнее время сталкиваюсь с данной проблемой в разных вариантах. Я даже в статье написал, что проблема возникает в большом проекте, когда один программист что-то делает, а потом другой подхватывают через длительное время.
И развивающихся в процессе своей жизни.
В общем предлагаю развить и закончить вашу мысль.
Поскольку в таком виде это скорее как антипаттерн для средних и больших проектов (а таковыми считаются, насколько я помню, проекты более 3-5 человеко-лет для средних и свыше 10-15 для больших)
На момент написания статьи я даже не знал, что её кто-то прочитает, поэтому не вкладывался. Если найдет вдохновение, то подкреплю фактами.
Поскольку в таком виде это скорее как антипаттерн для средних и больших проектов
В моем проекте объем примерно 20 лет * 100 чел. Мне доступно примерно 2 Гб кода, примерно 20 млн строк кода. Сколько кода всего в системе сказать не могу. Такие дела
Я как раз против абстракций.
Надо Вам пару гигабайт кода кинуть, что бы Вы там в чужих абстракциях пару месяцев погуляли.
Спасибо за предложение, мне вполне хватает исходников, и на работе и дома.
Вам со своей стороны тоже могу пожелать почаще писать, не только на псевдо языке, побольше коммерческих и OS проектов.
Кстати, не все так хорошо с DDD.
Цитатата из https://habrahabr.ru/post/313110/
«DDD Работает хорошо в устоявшихся бизнес-процессах»
Я-то поднял проблему изменяющихся бизнес-процессов. С устоявшимися все понятно и так.
В императивном подходе эта проблема есть..., но это не самая большая проблема императивного программирования.
Если вы знаете что переделывать. А если вам досталось от предыдущих разработчиков сотни пакетов, большинство которых порядка 10 тыс строк кода, процедуры со 120ю параметрами и многое другое, то разобраться сразу затруднительно куда вставлять обработку нового типа.
В моем же варианте кода (пусть не самом лучшем) все места сразу «зазвонят», когда попадется новый тип.
Вообще, программистам не следует делать то, что компилятор и инструмены сделают лучше.
Как это можно загрузить в диаграмму — непонятно.
Перечисляемые типы вроде как для этого и созданы? И всякие велосипеды для их эмуляции там, где они "by design" не предусмотрены? И это, вроде как, для всех, кто хоть как-то работает со структурами данных обычная практика? Или я неправильно все понял?
Выдавать ошибку, когда встретился незнакомый тип, «by design» не обеспечивает. По крайней мере на уровне языков такого не встречал.
Так, а в чем проблема получить тип в месте использования, как возвращаемое значение какого-нибудь статического метода какого-нибудь абстрактного класса со встроенной обработкой ошибок в одном месте? А если поддерживаемые модулем типы (при слабой связанности) отправлять, при этом, в качестве аргумента, чтобы отлавливать любые нестыковки? Повторюсь: перечисляемые типы ведь для этого и созданы?
if Is_in_list(type_id, "список типов, ТЗ от 01.01.2016") then
do something;
end if;
В функции is_in_list() выдывать исключение и мгновенно оповещать разработчика или аналитика, даже если исключение возникло у другого пользователя. Так даже пользователю не надо будет звонить в поддержку, что бы разобраться. Поддержке или программисту придет сообщение
«При обработке 'список типов, ТЗ от 01.01.2016' возникло исключение в таком-то куске кода. Требуется либо добавить новый тип в список, либо внести изменения в код для особой обработки этого типа»
Во первых, список регламентированных типов и поддерживаемых в конкретном месте, может не совпадать. Но, при этом, в рамках конкретного места использования тип не может принимать значение отличное от заданного для данного логического контекста. И это далеко не обязательно ошибка, это может быть рабочей ситуацией в каком-нибудь модуле или являться частью общей эволюции модулей при развитии системы. Во вторых, как мы видим, это все легко разруливается различными методами и основаны все они на стандартизации типов, которую вы, собственно, и предлагаете. Для стандартизации существуют всякие встроенные возможности, типа "перечисляемых типов" и, как тут уже писали, чего-то типа assert. Т. е. вы хотите открыть людям глаза на какую-то довольно стандартную практику? В таком случае — согласен, стандартизация — это хорошая практика, но это же довольно банальное умозаключение. Вот я и интересуюсь, вы действительно имели в виду, что "трава — зеленая", или я что-то упустил?
да банально, не спорю,
Есть анекдот от Мамонова про старого гитариста.Выходит молодой гитарист и начинает играть и левой рукой и правой и вверх тормашками и за спиной и над головой. Выходит старый именитый гитарист, сел на стульчик и начинает на трех аккордах играть. Его спрашивают — «чего ж ты такой примитив гонишь? вон видишь молодой как скачет». «Вы понимаете», спокойно отвечает старый — «он еще ищет, а я уже нашел».
Выдавать ошибку, когда встретился незнакомый тип, «by design» не обеспечивает. По крайней мере на уровне языков такого не встречал.
Паттерн-матчинг в любом уважающем себя функциональном языке.
type Contract = Contract1 | Contract2 | Contract3
let f contract =
match contract with
| Contract1 -> printfn "Found contract 1"
| Contract2 -> printfn "Found contract 2"
Contract1 |> f //Found contract 1
Contract2 |> f //Found contract 2
Contract3 |> f //Run-time exception: The match cases were incomplete
(https://dotnetfiddle.net/Eup962)
Причем на самом деле, диагностика делается еще на этапе статического анализа.
И всякие велосипеды для их эмуляции там, где они «by design» не предусмотрены?
Мне тут подсказали, что есть так называемое контрактное программирование, где подобная семантика (else raise) встроена в предусловия и постусловия, которые срабатывают перед выполнением процедуры и после её выполнения.
Идея правильная, но не является правильной на все случаи жизни. Подробные ТЗ на каждый "тип договора" и никаких обобщений — это рабочая и масштабирующаяся схема, но весьма дорогая в применении.
У меня на прошлой работе был как раз такой пример — когда на каждый тип договора аналитик писал ТЗ с табличкой что куда сохранять и как, тестировщик писал автотесты, а мы имплементили согласно ТЗ. В итоге у нас было очень мало багов… и очень длинный цикл разработки. В то время как заказчик хотел "сделайте как у типа 123, но с такими-то изменениями". И пусть даже там будут баги, зато эти десятки новых типов будут добавлены за разумное время.
Возможно истина где-то посередине. Программа как-то сама должна фиксировать списки типов на момент её написания и выдавать ошибки, не всегда, а только когда, к примеру, тестер захочет протестировать в строгом режиме.
Контракты на каждый тип, который влияет на логику.
Можно поручить этот вопрос кодогегерации и миграциям.
Но чаще всего достаточно реализации их вручную.
Что касается ошибок, то debug mode это обычная практика.
Так же как и логи разных уровней.
Вы, похоже, действительно имеете очень мало практического опыта.
Извините, но предложенный шаблон не решает проблему, которую я поднял без кода, который я привел.
Что касается опыта, можете считать что его нет, но это видимость из-за того, что написал слишком просто для уровня студентов. Сначала надо с простым до конца разобраться.
Поскольку в самой постановке задачи ваш некий новый тип имеет поведение отличное от других.
Вы можете это запрограммировать и сделать конфигурируемым… Но в сложных проектах это потребует прикладников внедренцев, как в тех же 1С, SAP.
А в более простых сложная система конфигурации нерентабельна и проще при очередной модификации дописать несколько конкретных моделей, которые реализуют специфичное поведение для конкретного типа.
Хорошо, когда вы знаете что дописывать. В моем проекте из-за большого объема заранее неизвестно что надо дописывать.
Если у вас поведение модели домена детерминировано согласно ТЗ, то вы уже знаете что дописывать.
Возможно вам потребуется ознакомиться с кодом или документацией к нему, чтобы найти подсистемы отвечающие за конкретные аспекты.
Но и не более того.
Во всех остальных случаях ваша модель должна наследовать и реализовывать конкретный общий интерфейс
Имеется 2 Гб кода, котрый находится в постоянном развитии. На некоторые модули по 10 человек очереди. Тут нужны навыки человека дождя. :):):)
Редко поведение вашей прикладной модели отличается от уже реализованных настолько, что требуется изменить все 2 Гб кода (возможно удивлю, но небольшой проект на node.js может подтянуть в себя 0.5 Гб зависимостей в коде).
Поэтому в рамках вашей задачи по наращиванию системы не новым, а модификацей уже существующего функционала вам редко потребуется взаимодействовать с кодом более 10-20 тысяч строк.
В противном случае вам лучше произвести декомпозицию задачи.
Что касается очереди…
То современные системы контроля версий позволяют вести параллельную разработку над одним модулем (git/mercurial).
Намного больше сложностей у вас возникает в случае монолитных систем.
Пользуйтесь внутренней экспертизой.
Минимум 3-5 человек адекватного уровня и опыта у вас должны быть
О том, что затрудняет формализацию проекта и плодит скрытые ошибки