Структурные и Именованные типы. Поведение или Представление?

Вурдалак

Продвинутый новичок
А, кстати, еще такой момент: получается, что тип Int<0, 10> нужно будет протаскивать по всему коду, раз это на уровне компиляции, т.е. передать обычный Int в аргумент, который требует Int<0, 10> будет нельзя. Получается, мы бизнес-инварианты нужного нам VO фактически выставляем наружу.
 

Вурдалак

Продвинутый новичок
Enum. В терминологии scala он назвал - sealed trait.
В ООП задача решается добавлением уровня абстракции - например, абстрактной фабрикой.
Сохранение инвариантов === enum? Прости, я не понимаю о чем ты.

Что касается фабрики, то она никак не решает проблему сохранения инвариантов того объекта, который «выпускает» (что мне мешает создать объект вне фабрики?). Если речь, конечно, не про named constructor.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
@Вурдалак, enum - это конечный набор инвариантов. Обычно скаляров, но почему бы не использовать пользовательские типы?

Фабрика отлично решает проблему сохранения инвариантов того объекта, который «выпускает» - именно потому что она контролирует инициализацию, аналогично named constructor. Она не мешает создать объект вне фабрики, аналогично никто не мешает в int присвоить char или weight сложить с length. Ограничивать это или нет - depends.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
@Вурдалак, enum - это конечный набор инвариантов.
Enum — это конечный набор значений.

Краткий ликбез. Инвариант — это утверждение, которое будет для объекта справедливо всегда. И в данном случае инвариант звучит как «значение enum всегда будет принадлежать фиксированному множеству E».

PHP:
final class CorpMail
{
    private $value;

    public function __construct(string $value)
    {
        Assertion::email($value);
        Assertion::inArray($this->getDomain($value), ['corp.mail.ru', 'corp.list.ru', 'corp.bk.ru']);

        $this->value = $value;
    }

    // ...
}
— тут 2 инварианта:
1. Это похоже на email.
2. Домен один из корпоративных.

Фабрика отлично решает проблему сохранения инвариантов того объекта, который «выпускает» - именно потому что она контролирует инициализацию
Это не имеет отношения к ООП. Объект должен контролировать собственные инварианты сам. Иначе ты придешь к той же anemic domain model, когда «модель» вообще ничего о себе не контролирует, а всё делегирует каким-то «сервисам». Это процедурное программирование.

Это примерно как считать, что вместо DateTime надо иметь анемичный DateTime + DateTimeFactory. А ты попробуй написать new DateTime('trash'), будет ли поведение молчаливым, каким его хочешь видеть ты.
 
Последнее редактирование:

Lionishy

Новичок
предлагаешь декомопозировать один VO на несколько более мелких VO без какой-либо разумной аргументации
i) Я согласен, что IntRange<0,10> может быть реализовано как ValueObject, но может быть реализовано и по-другому: через везде определённое или частично определённое преобразование типов, посредством конечного числа отдельных конструкторов, перечислением или псевдонимом элемента списка [0..10]. IntRange<0,10> -- это абстракция.
ii) IntRange<0,10> может обеспечить проверку на этапе компиляции, если в коде где-то была логическая ошибка.
iii) IntRange<0,10> явно декларирует в аргументах предусловия -- самодокументируемый код.

Есть ещё и "теоретический" момент. Функция не должна контролировать свои предусловия. Это для неё бессмысленно. Вы пытаетесь написать функцию, в операционную семантику которой входит обработка недопустимых данных.
Вместо
Код:
if (<bool expression>)
{
//on true
}
else
{
//on false
}
Вы предлагаете писать
Код:
try {
    if ( to_bool(<any_expression>) )
    {
    }
    else
    {
    }
} catch(IncompatibleType)
{
}
Так, вероятно, нужно делать, если операционная семантика макроязыка не определена, то есть, если Вы не знаете какие условия на конструирование AvailableMana накладываются. Это фактически означает, что про AvailableMana не известно является ли она вычислимой.
 

Lionishy

Новичок
Получается, мы бизнес-инварианты нужного нам VO фактически выставляем наружу
Не совсем так.
Мы наружу "выставляем" предусловия. Инвариант всегда следует из предусловий. Также как и следует, что вариант не верен (или наоборот верен). Но какой инвариант -- мы не знаем. В частности: можно затребовать два числа IntRange<0,10> и IntRange<0,10>, но инвариантом будут не два числа, а их сумма IntRange<0,20>. Мы этого не знаем, это знает только сам процесс.
 

Вурдалак

Продвинутый новичок
может обеспечить проверку на этапе компиляции, если в коде где-то была логическая ошибка
Еще раз бегло пройдясь по постам, мне кажется, что основной проблемой в нашем диалоге является то, что ты делаешь упор на проверки на уровне компиляции, я же говорю, что это не требуется. Если ты конструируешь AvailableMana в 10-ти разных кейсах, и значение почти всегда приходит извне (от пользователя, например), то тебе всегда придется принимать решение о том как скастить обычный int в int<0, 10>. А учитывая, что для бизнеса каст не будет являться чем-то осмысленным, то тебе придется как-то завершать работу программы (исключение, return, etc.), т.е. везде будет один и тот же какой-то мусорный код с if (Is_castable_to(...)).

Да и потом, AvailableMana — примитивный кейс. Вот как твой подход будет выглядеть для CorpMail? Ты введешь тип EmailStringWithCorpDomain?
 

Вурдалак

Продвинутый новичок
В частности: можно затребовать два числа IntRange<0,10> и IntRange<0,10>, но инвариантом будут не два числа, а их сумма IntRange<0,20>
Ага, или инвариантом будет утверждение «числа a и b не должны совпадать». Ты введешь тип NotFuckingEqualIntCouple<0,10>?
 

Lionishy

Новичок
@Вурдалак,
везде будет один и тот же какой-то мусорный код с if (Is_castable_to(...))
Я сделаю каст стрелочного типа
Код:
to_range (Int a) => a (Int b) => b :: Int -> IntRange<a,b> | Error
Что делать при неудачном касте, сам каст знать не может.
Зато мы знаем, что бизнес-процесс не допускает неудачного каста.
Мы введём политику для каста
Код:
panic_on_error a :: a | Error -> IO() -> IO() | ()
panic_on_error Error IO() = panic(IO(),Error)
painc_on_error a _ = ()
Как только я получил данные из файла, по сети или от пользователя я немедленно пишу каст. Если каст провалится -- сгорит предохранитель.
Почему это хорошо?

i) Я могу регулировать политику: сегодня программа просто падает, а завтра переспрашивает у пользователя число.
ii) Я получаю релевантную ошибку, т.е. ровно в том месте, где она произошла -- при получении данных. А не чёрт знает где.

Например, в Rust (если он когда-нибудь будет, конечно) так предполагается делать обработку ошибок: внедряя акторы on_fail.
 

Вурдалак

Продвинутый новичок
Код:
panic_on_error a :: a | Error -> IO() -> IO() | ()
panic_on_error Error IO() = panic(IO(),Error)
painc_on_error a _ = ()
А, т.е. логика при невозможности каста лежит отдельно. И panic() — это типа примерно как exit()? Но в реальности при ошибке будет показана либо страница ошибки, либо задача выпадет из очереди и т.д. Поскольку мы не знаем контекста (web, CLI, queue consumer, API, etc.), то мы выкинем исключение. И будет фактически то же самое.

i) Я могу регулировать политику: сегодня программа просто падает, а завтра переспрашивает у пользователя число.
Так с исключениями будет то же самое.

ii) Я получаю релевантную ошибку, т.е. ровно в том месте, где она произошла -- при получении данных. А не чёрт знает где.
Так с таким же успехом ты можешь напрямую юзать VO, чо. new CorpEmail('trash') тоже укажет на ошибку в том месте, где она произошла.
 
Последнее редактирование:

Lionishy

Новичок
И будет фактически то же самое.
Я не отрицаю, что можно использовать исключения, когда у Вас нет возможности продвигать программу далее.
Но... Не нужно допускать неверные данные к тем объектам, которые корректно их обработать не могут.
Для преобразования типов любое входное значение может быть успешно, без нарушения операционной семантики, обработано. Программа продвинется либо к типу конечному, либо к типу ошибки. Что делать в случае ошибки может решить только бизнес-процесс.

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

DateTime ты введешь тип DateTimeString
Если нужно преобразовать строку в DateTime, то будет стрелочный объект DateTimeFromString, в который мы будем внедрять актор on_fail.
Сам DateTime просто не может быть некорректно сформирован.

Хороший конструктор -- бесконечно тонкий, то есть просто список инициализации.
Scala, к примеру, не позволит Вам ничего проверить во время конструирования. По сути, в Scala конструктор -- это просто список private данных. Scala, в хорошем смысле, заставляет делать правильно.
 

Вурдалак

Продвинутый новичок
Scala, к примеру, не позволит Вам ничего проверить во время конструирования. По сути, в Scala конструктор -- это просто список private данных. Scala, в хорошем смысле, заставляет делать правильно.
http://stackoverflow.com/questions/9169691/how-to-check-constructor-arguments-and-throw-an-exception-or-make-an-assertion-i
 

Lionishy

Новичок
@Вурдалак,
Конечно, никто не запрещает Вам редуцировать объект к исключению. Но объект уже будет сформирован, а конструктор выполнен.

A better approach might be to provide a factory method.
Именно это и есть эквивалент ваших проверок.
 

Вурдалак

Продвинутый новичок
Scala, к примеру, не позволит Вам ничего проверить во время конструирования.
Конечно, никто не запрещает Вам редуцировать объект к исключению.
Взаимоисключающие параграфы?

A better approach might be to provide a factory method.
Именно это и есть эквивалент ваших проверок.
Что до цитаты, которую ты вырвал из контекста (там даже не точки после слова «method»), то там автор советует какую-то хрень. Result/Validation object почти всегда будет хуже, чем исключение.

А сам по себе factory method — это тот же named constructor. И чо?
 

Lionishy

Новичок
Взаимоисключающие параграфы?
Нет. Вы продвигаете объект, Вы его создали и вызвали на нём метод.
Так устроена Scala. By design. Каждый объект формируется и редуцируется к ответу.
В C++ можно написать такой объект, который никуда не редуцируется, вообще пустой, но не компилируется... Из-за несовместимости типов при инициализации. В Scala так нельзя. By design. Любой объект создаётся успешно.
Код:
class MyData {
};

class MyErrorClass {
public:
    MyErrorClass(int i): data(i) { };

private:
    MyData data;
};
Этот объект никуда не редуцируется но не проходит компиляцию.

это тот же named constructor
Да, объект компаньон для того и придуман. Придуман для написания явных стрелочных объектов (выражены в Scala подобно методам), которые, в том числе, могли бы заниматься преобразованием типов. Всё очень похоже... Это тёмная сторона Scala... Влияние Rapid Application Development. Скорость превыше качества.

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

Result/Validation object почти всегда будет хуже, чем исключение.
Смотря для каких целей.
Простейший пример: человек вводит имя файла, но опечатался, программа пытается открыть файл и возникает исключение ( ! ), хотя для программы это вполне штатная ситуация -- нужно просто переспросить имя файла, завершать программу не нужно.

P.S. Scala язык, который по структуре похож на ООП из Java или PHP, или C++, но только похож.
 
Последнее редактирование:
Сверху