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

Вурдалак

Продвинутый новичок
Простейший пример: человек вводит имя файла, но опечатался, программа пытается открыть файл и возникает исключение ( ! ), хотя для программы это вполне штатная ситуация -- нужно просто переспросить имя файла, завершать программу не нужно.
Это в случае примитивного дизайна, когда представление напрямую работает с моделью. Если речь о нормальном дизайне, то с точки зрения модели это именно исключение, решение о том переспросить ли пользователя будет делаться вне domain. Для domain это не штатная, а не имеющая смысла ситуация. Исключение — это выход из текущего контекста, а не завершение программы. Исключения можно ловить, если чо.

Нет. Вы продвигаете объект, Вы его создали и вызвали на нём метод.
Так устроена Scala. By design. Каждый объект формируется и редуцируется к ответу.
В C++ можно написать такой объект, который никуда не редуцируется, вообще пустой, но не компилируется... Из-за несовместимости типов при инициализации. В Scala так нельзя. By design. Любой объект создаётся успешно
Что ты имеешь в виду? Я чо, могу написать
Код:
try {
    val foo = new Foo(-1)
} catch {
    case e: Exception => println("Invalid object: " + foo)
}
И получить невалидный объект foo? Может я неверно понимаю фразу «Любой объект создаётся успешно»?
 
Последнее редактирование:

Вурдалак

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

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

Что здесь происходит?
 

Lionishy

Новичок
Но в Scala конструктор может содержать логику.
Да, может.
Я совершенно не прав. Глупость. Конструктор есть и код есть.
Код конструктора явно выражает преобразование входных данных в тип класса, как, собственно, и любой другой.

Что до AvailableMana, то там происходит тоже самое: мы принимаем целое число и преобразуем его в AvailableMana. Естественно, если мы точно знаем, что операционная семантика не позволяет нам достичь результата, когда передано целое число не из промежутка 0..10, то выделять это в отдельное преобразование выглядит избыточно.

P.S. Где-то есть явный прокол. Всегда точно знал, что выделять нужно предусловия. Надо подумать.
 

fixxxer

К.О.
Партнер клуба
Какая еще нафиг компиляция, когда значение приходит из рантайма.
С другой стороны, PHP нам услужливо сделает int из строкового значения, услужливо же распарсенного из http-запроса, и вообще у нас HTTP-протокол.
А если у меня строгая типизация и какой-нибудь сериализатор в какой-нибудь протобуф, тут становится менее очевидно, на уровне контрактов протокола и DTO, наверное, тоже ведь уместно?
 

fixxxer

К.О.
Партнер клуба
Еще немножко подумал. :)

Раз уж мы вышли за рамки конкретных языков и рассуждаем вообще...

Инварианты по своей сути статические. Вот тут ты немного лукавишь:
PHP:
        Assertion::inArray($this->getDomain($value), ['corp.mail.ru', 'corp.list.ru', 'corp.bk.ru']);
Строго говоря, self::getDomain. $this тут не имеет никакого смысла, т.к. никакого состояния еще нет, да и делать assertions с помощью внешних источников (таких как база данных или конфигурация) VO не должен.

Если же:
1) все условия статические и не зависят от состояния;
2) типизация исключительно строгая, и явное преобразование из строки или ByteArray будет требоваться в любом случае;
3) все типы равноправны - нет никаких различий между "встроенными" и "пользовательскими" типами

- то в принципе, ничего не мешает описывать VO в виде типа и делать проверку в compile time, если язык программирования такое умеет (а почему нет? Те же темплейты в С++ тьюринг-полные). А уж в рантайме может быть какой-нибудь TypeCastException.
 

Lionishy

Новичок
Ты всего-навсего предлагаешь декомопозировать один VO на несколько более мелких VO без какой-либо разумной аргументации.
Если в run-time приходила бы строка, Вы бы стали конструировать AvailableMana от строки? Или при наличии типа PositiveInteger стали бы Вы принимать SignedInteger?
Скорее всего нет! Почему? Потому что такой код легче переиспользовать! Не проси больше, чем тебе нужно.

Другой вопрос, это сам конструктор. Он, конечно, выражает преобразование типа, но он не является объектом первого рода! Его нельзя вернуть, его нельзя передать. Именно по этой причине нам нужен метаобъект-фабрика, штампующий наши объекты. Такой объект-преобразование, мы уже можем продвигать по коду. Да и само преобразование будет максимально отложено (может нам и не понадобится вовсе).

Кроме того, все эти constrained-types можно переиспользовать как по своему проекту, так и в других проектах! И выражения станут яснее. По интерфейсу конструктора AvailableMana мы не видим, что там должно быть число от 0, 10. Нам нужно залезть в документацию.

P.S. В PHP, конечно, это не вопрос, потому что мы всегда видим код конструктора, но код может оказаться сложным, из целого набора проверок, а указание типов в параметрах сразу снизит нагрузку на код конструктор, он не будет засорён относительно простыми проверками.

И главная, всё же, на мой взгляд причина: функция не должна проверять свои предусловия, потому что она не компетентна в том, что делать в случае провала.

Функция может быть даже и не может проверить предусловия.
Ваш пример, скажем, с e-mail корпоративной почтой. Получается, что ValueObject должен не только строку получить, но и ресурс с доменами: файл или базу данных, или поток. Такой объект будет очень тяжело переиспользовать даже в разных частях программы. Проще передать строку и набор доменных имён. Тогда ещё проще использовать constrained-type для строки, который проверит, что у строки суффикс из набора. И этот объект можно переиспользовать уже где угодно, даже в других проектах.
 

Вурдалак

Продвинутый новичок
Основная претензия по поводу
PHP:
final class CorpEmail
{
    public function __construct(EmailStringWithCorpDomain $email)
    {
        // ...     
    }
}
Это выглядит абсурдно, потому что фактически сам EmailStringWithCorpDomain и есть нужный нам VO, зачем тогда нужен CorpEmail?

По интерфейсу конструктора AvailableMana мы не видим, что там должно быть число от 0, 10. Нам нужно залезть в документацию.
Для этого больше подходят предусловия как в контрактом программировании, мне кажется. Будет int, но с проверкой.

Функция может быть даже и не может проверить предусловия.
Ваш пример, скажем, с e-mail корпоративной почтой. Получается, что ValueObject должен не только строку получить, но и ресурс с доменами: файл или базу данных, или поток. Такой объект будет очень тяжело переиспользовать даже в разных частях программы. Проще передать строку и набор доменных имён. Тогда ещё проще использовать constrained-type для строки, который проверит, что у строки суффикс из набора. И этот объект можно переиспользовать уже где угодно, даже в других проектах.
Это как-то ортогонально нашему обсуждению. Хочешь — проверяй суффиксы, чо. Если это допустимо с точки зрения бизнес-логики.
 

Lionishy

Новичок
Это выглядит абсурдно
Естественно абсурдным выглядит создание ValueObject, когда я пытаюсь ввести механизм его заменяющий.
CorpEmail у Вас как реализации даже нет. CorpEmail -- это псевдоним типа проверки на Email и суффикс из списка.
В PHP так сделать не получится. Да, PHP недостаточно мощный c точки зрения типизации. Потому в PHP вообще не нужно заниматься типизацией.

Для этого больше подходят предусловия как в контрактом программировании
Любое программирование контрактное, в принципе... Так как невозможно составить алгоритм, который бы принимал постусловия и код (произвольные) и выдавал бы набор предусловий, при которых код возвращает результат, удовлетворяющий этим постусловиям.
Так что перед передачей данных в функцию, мы всегда будем проводить проверку входных данных, если у нас нет механизма доказательства, что они корректные.
А раз мы всегда проверяем данные перед входом в функцию, то зачем проверять их внутри?

Другой вопрос, что мы можем забыть написать эту проверку... Или допустить ошибку в доказательстве и число int m окажется отрицательным перед редукцией в AvailableMana. И чтобы себя обезопасить от нежелательного поведения, программисты придумали типы. Мы ограничим попадание в AvailableMana нежелательных параметров плотиной IntRange<0,10>. Компилятор уже не даст нам напрямую подсунуть int m, а потребует преобразования, той самой проверки. Теперь мы уже не забудем её написать. Если мы допустим ошибку в доказательстве, то где-то обязательно выстрелит отсутствие привидения типа или отсутствие обработки возможной ошибки. Вся теория типов -- это передача ответственности по проверкам на допустимое поведение.
 

Вурдалак

Продвинутый новичок
А раз мы всегда проверяем данные перед входом в функцию, то зачем проверять их внутри?
Зачем ты проверяешь в EmailStringWithCorpDomain, когда можно создать тип RealEmailStringWithCorpDomain и подавать его на вход EmailStringWithCorpDomain?
 

Lionishy

Новичок
@Вурдалак,
Не понял, в чём проблема...

Насколько я понял Ваш предыдущий тезис: есть какой-то бизнес-процесс, который может продвигаться только, если на вход поступает верный e-mail. Для ограничения непредвиденного поведения создаётся тип CorpEmail, который выражает предусловие для бизнес-процесса. Как CorpEmail реализован -- не важно.

Он может быть реализован объектом RealCorpEmail, конструктор, которого выражает преобразование типа. А может быть реализован и по-другому: записью с конструктором как объектом первого рода; записью, которая определяется своим конструктором и является объектом первого рода.

Если продвигаться от RealCorpEmail в направлении переиспользования кода макроязыка string-constrains, то RealCoprEmail просто превратится в фабрику записей с меткой CorpEmail.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
@Вурдалак,
Не понял, в чём проблема...
Напомню, что изначально мы говорили про int<0, 10>, ты в качестве преимуществ приводил это:
IntRange<0,10> явно декларирует в аргументах предусловия -- самодокументируемый код
Потом выясняется, что для CorpEmail ты бы тоже ввел свой тип, который бы фактически заменял собой CorpEmail. И куда же делась эта «самодокументируемая» составляющая? У нас опять есть некий тип CorpEmail, инварианты которого мы не знаем (конструктора, который бы описывал все правила, тут нет).

Т.е. у нас есть VO, являющийся оберткой над примитивным типом (string, int) с добавлением инвариантов. Тут приходишь ты и говоришь, что передать в конструктор этого VO нужно параметризованный тип, который уже в свою очередь проверяет эти же самые инварианты. Тогда я в недоумении: ну, а нахер тогда нужен сам VO, если этот параметризованный тип и является тем же самым? На что ты тут же отвечаешь:
Естественно абсурдным выглядит создание ValueObject, когда я пытаюсь ввести механизм его заменяющий.
OK, т.е. ты предлагаешь иметь не
PHP:
final AvailableMana
{
    public function __construct(int<0, 10> $mana)
    {
        // ...
    }
}
А сразу это:
PHP:
alias AvailableMana int<0, 10>
(по аналогии с Positive, Negative, etc.)

Таким образом, ты просто заменил VO на те же VO просто с проверкой на уровне компиляции. Зачем тогда было рассказывать про конструктор, про самодокументирующийся код? Выглядит, как манипуляция.
 

Lionishy

Новичок
У нас опять есть некий тип CorpEmail, инварианты которого мы не знаем
Если он выражен записью, то мы всё знаем, просто из объявления, из типов полей. Мы видим, что CorpEmail содержит поле, e-mail типа проверенная на e-mail строка, проверенная на суффикс.

От ситуации зависит. Если это действительно просто структурный псевдоним и процесс запускается от целого числа, то да.
А если нам важно, чтобы Mana не мешалась с Apples , то не следует так делать...
Мы сделаем AvailableMana записью, но полностью определённой конструктором:
PHP:
class AvailableMana {
    final public $mana;

    public function __construct(IntRange<0,10> $mana)
    {
        $this->mana = $mana;
    }
};
В Haskell это можно записать короче
Код:
data AvailableMana = AvailableMana (IntRange 0 10)
Например в Java для таких целей есть фреймворк с системой аннотаций, которые помогают такие проверки типов делать. Описывать checker на уровне фреймворка, а затем рассовывать аннотации к переменным. Вроде бы как тип. Чтобы при компиляции можно было узнать, а не подсунули ли мы где-то не тот int.

По существу, это псевдоним. И компилятор после сверки типов может позволить себе всё конвертировать в int.

P.S. В мире PHP я сталкивался тоже с такими анализаторами, которые читают аннотации PHPDoc, по-моему, и предупреждают о несовместимости. Можно вообще типов не писать, а только аннотации -- контроль типов на уровне стороннего инструмента. И с псевдонимами, и с записями.
Изначально, речь шла о том, что если бы в Java были бы простые записи, то работа с данными существенно упростилась бы, потому что все ValueObjects можно было бы превратить в записи. А если там проверка, то выразить её типом. А если проверка типом не выражается как-то очевидно, видимо это и не ValueObject`а ответственность...
Объекты -- это хорошо, но скупость механизмов типизации -- плохо.


Кроме того, AvailableMana может содержать какие-то свои методы, то есть являться бизнес-процессом, и не выражаться записью. И мы инициируем этот процесс целым числом. Чтобы код самодокументировался, мы условие для инициализации процесса переносим явно в конструктор... А оттуда это условие перекочует в фабрику процессов, интерфейс которой мы явно видим и становится ясно, что нужно подавать не любой целое, а специальное целое.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
Мы видим, что CorpEmail содержит поле, e-mail типа проверенная на e-mail строка, проверенная на суффикс.
Это очередная манипуляция. Чтобы продать что-нибудь ненужное, нужно сначала купить что-нибудь ненужное. Чтобы передать в эту поле строку, нужно эту строку создать. Чтобы эту строку (EmailStringWithCorpDomain) создать, нужно осознать, что строка имеет некоторые правила: она должна быть похожа на email и домен быть быть один из трех корпоративных. Мы возвращаемся к тому же, откуда начали: у нас есть EmailStringWithCorpDomain, которая получается из string (string получается из поля ввода). Т.е. вместо получения CorpEmail из string мы теперь получаем EmailStringWithCorpDomain из string. Все стало намного проще, ога.

Изначально, речь шла о том, что если бы в Java были бы простые записи, то работа с данными существенно упростилась бы, потому что все ValueObjects можно было бы превратить в записи.
У тебя есть VO Money, который принимает на вход VO Amount и VO Currency (USD, EUR, etc.). У Money есть свои собственные методы и инварианты, каким образом ты хочешь Money заменить на record?
 

Lionishy

Новичок
Это очередная манипуляция.
Я не могу сейчас понять вопроса...
Вот была у меня функция F: A -> B , часть своей работы она делегировала в функцию G: A1 -> B1 , часть в функцию H: -> B1 -> B и часть в функцию I: A - > A1. Теперь F превратилась в композицию F = H G I
С таким кодом, несомненно, стало проще работать. Я каждую функцию отдельно отлаживаю, а F -- просто удобный псевдоним в коде.

CorpEmail замыкается не только EmailStringWithCorpDomain, но и стратегией on_fail. Поведение CorpEmail может быть разделено на: обработку ошибок, поведение, характерное для записи, и поведение преобразования типов. Обработку ошибок делает сам CorpEmail, например, завершая программу. Как запись, мы можем его копировать, присваивать, сохранять в файл. А поведение преобразования поддерживается элементарными объектами, такими как ValidString<Email> или ValidString<Suffix>.

Мне кажется, что Вы спрашиваете: "Зачем нам инверсия управления?" Или что-то подобное.


У Money есть свои собственные методы и инварианты
Так, как описано у Фаулера, Money никаким поведением не обладает. Исходя из проблемы можно только сказать, что Money -- это запись с полем типа Decimal, параметризованная типом Currency, чтобы USD не складывать с EUR. А между Money<USD> и Money<EUR> следует ввести преобразование.
 

Вурдалак

Продвинутый новичок
Обработку ошибок делает сам CorpEmail, например, завершая программу.
А кофе CorpEmail не варит случайно? CorpEmail может отослать письмо на email, который в нем записан, с предупреждением о том, что email попытались несанкционированно использовать в корпоративной сети?

Так, как описано у Фаулера, Money никаким поведением не обладает.
https://github.com/mathiasverraes/money/blob/master/lib/Money/Money.php
Ты предлагаешь это заменить на record, который не обладает ни поведением, ни immutability?

Вот была у меня функция F: A -> B , часть своей работы она делегировала в функцию G: A1 -> B1 , часть в функцию H: -> B1 -> B и часть в функцию I: A - > A1. Теперь F превратилась в композицию F = H G I
С таким кодом, несомненно, стало проще работать. Я каждую функцию отдельно отлаживаю, а F -- просто удобный псевдоним в коде.
А как это выглядит в ООП? Покажи код. С этим будет удобно работать, правда?
 

Lionishy

Новичок
CorpEmail может отослать письмо на email
CorpEmail выполняет ту обработку ошибок, которой его замыкают. Любую, которая не допускает инъекцию значения Error в бизнес-процесс, который требовал строго верной EmailWithCorpDomainString...

Класс, якобы обладающий поведением, ни одного интерфейса не реализует -- подозрительно.
Далее ещё страшнее... Процессы, такие как арифметические операции и сравнение, выражены методами... Сделайте меня развидеть это!

А как это выглядит в ООП? Покажи код.
На примере Money
Код:
interface Order {
    public bool gt();
    public bool lt();
}

interface Collection<T> {
    //some collection methods declared
}

interface FilterStrategy<T> {
    public bool filter_out(T t);
}

class Money<C extends Currency> {
    final public Decimal amount;
   
    public Money(Decimal amount_)
    {
        this.amount = amount_;
    }
}

class MoneyOrder<C extends Currency> implements Order{
    public bool gt()
    {
        return new DecimalOrder(lha.amount,rha.amount).gt();
    }

    public bool lt()
    {
        return new DecimalOrder(lha.amount,rha.amount).lt();
    }

    public MoneyOrder(Money<C> lha_, Money<C> rha_)
    {     
        this.lha = lha_;
        this.rha = rha_;
    }

    private Money<C> lha;
    private Money<C> rha;
}

class MoneyGtAmountFilter<C extends Currency> implements FilterStrategy<Money<C>>
{
    public filter_out(Money<C> mon)
    {
        return new MoneyOrder(mon,ref).lt();
    }

    public MoneyGtAmountFilter(Money<C> ref_)
    {
        this.ref = ref_
    }

    private Money<C> ref;
}

class FilteredCollection<T> extends Collection<T>
{
    // collection methods goes here
    public FilteredCollection(FilterStrategy<T> strategy_, Collection<T> coll_)
    {
        this.strategy = strategy_;
        this.coll        = coll_;
    }

   private FilterStrategy<T> strategy;
   private Collection<T> coll;
}

List<Money<USD>> usd_list = ArrayList<Money<USD>>; //create new list

Collection<Money<USD>> usd_filtered =
    new FilteredCollection<Money<USD>>(
       new MoneyGtAmountFilter(
           new Money<UDS>(
               new Decimal(10,3))),
       usd_list);

//some code with usd_filtered_colleciton goes here
 

Вурдалак

Продвинутый новичок
CorpEmail выполняет ту обработку ошибок, которой его замыкают. Любую, которая не допускает инъекцию значения Error в бизнес-процесс, который требовал строго верной EmailWithCorpDomainString...
А что будет дальше? Допустим, в случае невозможности каста мы попали в какой-то callback-обработчик. После выполнения этого callback'а куда мы вернемся? Завершение работы программы — это не представляющий интереса кейс.

Класс, якобы обладающий поведением, ни одного интерфейса не реализует -- подозрительно.
Интерфейсы у сущностей и value object'ов в большинстве случаев — это плохая практика. Особенно, если речь идет про модель. Если, например, я хочу измерять расстояние между двумя точками, то мне не нужно пытаться сделать абстракцию над функцией расстояния без ведомой на той причины, потому что скорее всего я измеряю расстояние на планете Земля, у которой в ближайшем будущем будет тот же радиус https://github.com/leopro/trip-planner/blob/master/src/Leopro/TripPlanner/Domain/ValueObject/Point.php#L47
Я не делаю абстрактную модель с обобщением для любой планеты, включая Марс.
Вряд ли бизнес интересует Марс.
Если ты тратишь время на написание подобных абстракций, то ты отнимаешь как своё время, так и своих коллег.
Не все абстракции хороши.

Далее ещё страшнее... Процессы, такие как арифметические операции и сравнение, выражены методами... Сделайте меня развидеть это!
У тебя какая-то профессиональная болезнь, связанная с функциональным программированием?

Ты видимо меня не так понял. Я говорил про CorpEmail. Ты начал рассуждать о функциях, об их суперпозиции... Я хотел, чтобы ты продолжил мысль. Я хочу понять, насколько далеко ты можешь зайти в попытке всё усложнить.
 

Lionishy

Новичок
Завершение работы программы — это не представляющий интереса кейс.
Давайте изначально определимся. Если Ваша программа может продвигаться, при неудачном касте, то у вас должен быть объект выбора, который либо вызовет бизнес-процесс для EmailWithCorpDomain, либо вызовет другой процесс. То есть, операционная семантика бизнес-логики допускает редукцию для String U Error.

А до этого, вроде бы, Вы говорили, что у Вас выскакивает исключение, то есть операционная семантика допускает редукцию только для String.

Если это не обрушение, а выбор, то бизнес-процесс будет требовать EmailWithCorpDomain. Сам же выбор будет замыкаться фабриками бизнес-процессов для удачного и неудачного кастов.

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

У тебя какая-то профессиональная болезнь, связанная с функциональным программированием?
У меня аллергия на процедурное программирование под ключевым словом class.

Я говорил про CorpEmail.
Я попытался показать, что происходит.
CorpEmail имеет набор ответственностей: аварийного завершения программы, кастовки типа, доступа к данным. Я предложил эту ответственность разделить: процесс аварийного завершения внедрить, преобразование типа передать полноценным объектам-процессам, образующим замкнутую категорию, а доступ к данным -- это просто запись.
 

fixxxer

К.О.
Партнер клуба
Ты сам для себя придумал процедурное программирование.

ValueObject - это нифига не чистое значение, несмотря на название. Значение - это частный случай простого, примитивного VO, но это не правило.

ValueObject это вполне себе модель, такая же как и Entity. Разница между ними в том, что
1) Entity.equals(Entity other) определяется через некое identity, вроде поля id, и Entity обычно мутабельны;
2) ValueObject.equals(ValueObject other) определяется через равенство всех полей (этим похоже на Record), и обычно ValueObject иммутабельны (могут в принципе и не быть, но это плохой дизайн).

А методы у VO могут быть ровно так же, как и у Entity. Вон с Point хороший пример.
 
Последнее редактирование:

Lionishy

Новичок
ValueObject - это нифига не чистое значение
Ты сам для себя придумал процедурное программирование
Чем отличается процедурное программирование, от функционального или объектного?

Значение - это частный случай простого, примитивного VO
Есть два варианта.
ValueObject предоставляет данные -- тогда это просто запись, значение, у него нет никаких направлений редукции.
ValueObject представляет абстрактный процесс получения данных, активные данные. Тогда сам ValueObject не более, чем интерфейс, у него нет ни конструктора, ни полей.
Промежуточный вариант разрушает замкнутость макроязыка.

хороший пример
Это плохой пример. Ужасный. Это не объектное программирование.
Вы предлагаете запихнуть, например, в класс целого числа операции с целыми числами.
Сначала мы запишем сложение и умножение. А через пару проектов нам понадобятся НОД и НОК. Нам нужно будет класс вскрывать и методы добавлять...
По-моему, даже с точки зрения практики -- это дико.

В классах же Money и Point предлагается делать именно это.
 
Сверху