Pull to refresh

Comments 30

Имхо, местами с функциональщиной перебор:

public static Weight ofKilograms(int kilograms) {
    var inGrams = BigDecimal.valueOf(kilograms)
        .multiply(BigDecimal.valueOf(1000));

    var rounded = inGrams.setScale(0, RoundingMode.HALF_UP);

    return new Weight(rounded.longValueExact());
}

return new Weight (kilograms * 1000L); - вполне достаточно.

Да, вы правы, окрулять не надо если у нас и так целые значения и храним в long.

Мне хотелось этим показать, что можно упаковать в метод правила округления.

Думаю вариант с аргументов double лучше демонстрирует идею:

public static DiscountRate ofFraction(double raw) {
    var bd = BigDecimal.valueOf(raw);              
    var normalized = bd.setScale(2, RoundingMode.HALF_UP);
    return new DiscountRate(normalized);
}

Хотя если копать дальше, я бы предпочёл округлять и нормализовать scale именно в конструкторе в итоге, тогда точно будет одна точка подготовки данных.

В C# double не даёт нужной точности значения. Всегда использую decimal.

Коммент для треда "ваша джава творит дичь" :)

В Java тоже обычно используют BigDecimal для точности. Но есть бесячая проблемка. Если в C# при сравнении decimal "созданной с разной точностью" сравнения происходят без проблем:

decimal a = 1.1m;
decimal b = 1.10m;

Console.WriteLine(a == b);  // true
Console.WriteLine(a.Equals(b));  // true

То в Java разная точность и надо сравнивать через compare, так как через equals не будет true. Поэтому связываться с этим в бизнес логики совсем неинтересно:

var a = new BigDecimal("1.10"); //scale = 2
var b = new BigDecimal("1.1"); //scale = 1

System.out.println(a.equals(b)); //false
System.out.println(a.compareTo(b) == 0); //true
System.out.println(a.setScale(2).equals(b.setScale(2))); //true

Строки взяты для быстрой настройки scale и для точности.

Разве в джаве нет оттестированных библиотек для работы с физическими величинами?

Конечно есть, в Java есть стандарт JSR 385 (Units of Measurement API 2.0) и популярная реализация это библиотека Indriya. Мы его используем, особенно востребована библиотека в "международных" сервисах, когда величины разные и зависят от страны.

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

Для денег тоже есть устоявшаяся библиотека JSR 354 RI Moneta. И я бы ее рекомендовал для работы с деньгами по умолчанию.

Weight(700)

700 от куда-то взялся, а откуда, и в чем он? 700 грамм? 700 тон?

Хорошее замечание. Было бы разумнее назвать класс "Grams", чтобы существенно повысить читаемость, и избежать подобных вопросов.

Это псевдокод) там нет new если мы говорим о Java синтаксисе. И подразумевает, что объекты созданы на основе одинаковых значений.

Но мне нравится это замечание и я верну синтаксис Java, чтобы не было вопросов и затыков при чтении)

Всем value объекты прекрасны. Кроме как наличием отсутствия перегрузки операторов в Java, чем в общем и BigDecimal характерен. Более-менее сложное вычисление - и хоть в LLM скармливай, чтобы объяснило, что там написано. Особенно когда встречается пара таких объектов, каждый из которых - со своим неповторимым API.

Это совсем не проблема VO, вы можете писать обычные сервисы и дто имея разные API. Так что это вопрос может касаться конкретного языка или договоренностей в команде. VO не делает это лучше или хуже, это делают люди. Ну с перегрузкой нельзя не согласиться.

Мы больше не бросаем исключения. Вместо этого явно возвращаем либо ошибку (Left), либо успешно созданный VO (Right). Больше примеров использования на все случаи жизни вы найдете в документации к библиотекам.

Вот такое я по возможности блокирую на review. Потому что:
где-то должно быть написано, что Left - это ошибка, а Right - это значение. И соблюдаться неукоснительно. В данном случае накосячить с правой стороной сложно, а с левой - "а давайте для случаев, когда нужно возвращать VIP статус, будем там возвращать строчку "VIP", а если не "VIP" - то сообщение об ошибке. А если цифры - то это идентификатор сотрудника, который оформил заказ. И через пару - тройку развивателей продукта, каждому из которых нужно добавить функциональность и по возможности ничего не сломать - получаем псевдо-DSL в поле Left. В более лёгком случае - набор public static final String, которые сравниваются хитрыми if-else в объёме на полноценную state machine.

Исходя из принципов, приведённых в статье: делаем VO ErrorMessage, который инкапсулирует строку с сообщением. Или вообще имеет где-то там enum с этими строками. Either.left им и типизирован, при чтении понятна семантика, да и IDE автоматически сгенерирует переменную нужного типа где-нибудь там потом.

Вариант чуть посложнее, если упороться в рефакторинг: делаем VO для возвращаемого результата, с явными полями Error error и PickupNumber pickupNumber. Пару методов создания - ofError(, ofSuccess( можно ещё ofAlmost( - это когда и создать удалось, но не без сопутствующих ошибок. Кстати, когда попросят собрать все ошибки, warning-и и умолчания, которые случились внутри вызова, этот список можно будет упаковать внутрь Error и не переписывать весь окружающий код.

Таким образом появляется некоторый шанс, что API без присмотра просуществует некоторое время примерно так, как задумывал автор.


где-то должно быть написано, что Left - это ошибка, а Right - это значение

В доке библиотеки написано) By convention the success case is Right and the failure is Left. Это как с assertEquals(a , b) в JUnit, что первое, а что второе. Благо AssertJ упрощает и уже на надо запомнить это соглашение о порядке элементов. И использование внешней библиотеки или подхода должно быть не внезапным событием для ревьювера, а заранее спланированных событием.

Чтобы это не создавало когнитивную нагрузку, то всегда можно сузить возможности для разработчика и отдать это компилятору:

Either<DomainError, T>

В данном случае накосячить с правой стороной сложно, а с левой - "а давайте для случаев, когда нужно возвращать VIP статус, будем там возвращать строчку "VIP", а если не "VIP" - то сообщение об ошибке.

Ок, если рассматривать текст статьи как бездумный копипаст и использовать String, то да. Полностью прав с вашим комментом, но у нас есть же объекты. Вы можете возвращать, то что вам удобно. Можно сделать sealed класс с исключениями DomainError, а там используйте что удобно и готовый текст или код ошибки для дальнейшей локализации ответа. По сути вы это и описали во второй части комментария – делаем систему возвратов строго типизированной. И я разделяю этот подход.

А возможно я тут и сам попал в Primitive Obsession силки 😁 кто знает)

мне кажется или проделана большая работа по натягиванию совы на глобус....

Вам не кажется. Подано как очередная серебряная пуля, а по сути — пшик. Собственно поэтому мой комментарий выше остался без ответа.

Не судите строго) Костя хороший разработчик, просто чтобы апнуться в сдэк нужно показать перед hr-ами активность на конференции или ляпнуть жидкого на хабре

Спасибо за поддержку! Я бы сказал, что это очень смело на Хабре высказываться за автора! Жму руку)

Не-а, не кажется 🙂
Я не предлагаю всем срочно делать VO и что без этого у нас всё развалится. Нет, это просто подход, который может решить некоторые проблемы.

P.S. на коммент ответил :)

То же самое на расте (Result для обработки ошибок и стирание типов при компиляции, в рантайме работает так же быстро, как если бы использовался обычный usize):

use std::ops::{Add, Mul, Sub};

#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct Weight(usize);

impl Weight {
    #[allow(clippy::unwrap_used)]
    pub fn from_grams(grams: i32) -> Result<Self, String> {
        if grams < 0 {
            Err(format!("Weight cannot be negative: {} g", grams))
        } else {
            Ok(Self(grams.try_into().unwrap()))
        }
    }

    pub fn from_kilograms(kilograms: i32) -> Result<Self, String> {
        Self::from_grams(kilograms * 1000)
    }

    /// Получить вес в граммах
    #[must_use]
    pub fn inner(self) -> usize {
        self.0
    }

    #[must_use]
    pub fn is_between(self, first: Self, second: Self) -> bool {
        let min = first.min(second);
        let max = first.max(second);
        min <= self && self <= max
    }

    #[must_use]
    pub fn is_empty(self) -> bool {
        self.0 == 0
    }
}

impl Add for Weight {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Self(self.0 + rhs.0)
    }
}

impl Sub for Weight {
    type Output = Result<Self, String>;

    fn sub(self, rhs: Self) -> Self::Output {
        if rhs > self {
            Err(format!("Weight cannot be negative: -{} g", rhs.0 - self.0))
        } else {
            Ok(Self(self.0 - rhs.0))
        }
    }
}

impl Mul<usize> for Weight {
    type Output = Self;

    fn mul(self, rhs: usize) -> Self::Output {
        Self(self.0 * rhs)
    }
}

Так и думал, что речь шла просто о паттерне Newtype. Джависты всегда всё переусложняют.

Отличие есть, крато:

  • Newtype это только про типовую безопасность. Обернули String → получили другой тип. Не получили overhead, а компилятор не обманешь.

Думаю с учетом примера на Rust его дока и подходит. Rust Design Patterns – Newtype

  • ValueObject пошире, это можно сказать "усложненный" newtype, включающий инварианты и может содержать не одно поле при необходимости.

😁 И всё это придумали не джависты, не во всем они виноваты)

У меня была похожая проблема в графической системе - какие то функции возвращают координаты TPoint в абсолютных какие то в относительных, какие то в пикселях, какие то в сантиметрах. И все были нужны. Я сделал несколько по разному называющихся типов равнозначных TPoint и везде расставил. Стало понятно по коду в любом месте что функция возвращает.

Пока кто-то не наведет не сделает ревью всего и не приведет в порядок, его не будет.

А вот эти обертки в результате увеличат сложность, так как каждый ленивый разработчик будет приводить к нужному стандарту в своем конкретном месте и в цепочке прохождения все будут преобразовываться туда - сюда по 10 раз.

Вы сделали разные типы? И каждый выводит TPoint? Интересно узнать, что тут значит равнозначные.

Сделал разные алиасы для одного и того же типа

TFloatPoint = TPointF;

TPxPoint = TFloatPoint; { в пикселях }
TSxPoint = TFloatPoint; { в единицах сетки }

TFloatRect = TRectF;
TPxRect = TRectF; { в пикселях }
TSxRect = TRectF; { в единицах сетки }

И при объявлении метода видно в каких единицах он принимает и выдает результат

function GetBasePoint(const LPath: RSDERegion; SegN: integer; R: TPxRect): TPxPoint;

красиво и синтаксис понятный)

Это хорошо, когда есть newtype и прочие Units of Measure - в C# так просто промаркировать существующие value type нельзя, а, значит, нужен новый class/struct/record, а, значит, например, существующую арифметику придется переопределять. Скажем, Weight выше - хотим их сложить или умножить на количество единиц, ан фигу, давай заново пиши сложение и умножение. Семантически это правильно - новый тип определяет допустимые операции (включая равенство, сортировку и пр.), но при этом писанины - мама не горюй. Отдельный геморрой - любая сериализация бинарная/в строку/БД, которая про новые типы знает ничего. Опять или писать расширение или включать слой конвертации обратно в известные примитивы. В целом я совершенно за строгую типизацию (и к DDD она вообще никаким боком), но это совсем не бесплатно.

Супер лайк! Когда какой нибудь модный молодежный язык попробует это ввести прямо чтобы встроено было? Типа instant в Java

Уже есть поддержка, вот что пришло в первую очередь:

  • Kotlin имеет inline value classes — это лёгкие обёртки над примитивами с инвариантами, без overhead на runtime. Пример:value class Weight(private val grams: Long) , компилятор “инлайнит” его как long, но даёт типобезопасность и методы.

  • Rust использует newtype pattern:  pub struct Weight(pub u64);  — zero-cost абстракция, где компилятор оптимизирует до голого числа, но с отдельным типом для предотвращения ошибок.

Также примеры на C# тоже укладываются в «есть прямо сейчас», а Java ждёт проекта Valhalla. На Хабре писали про это Project Valhalla: эпичный квест Java за перфомансом)

Sign up to leave a comment.

Information

Website
rabota.cdek.ru
Registered
Employees
501–1,000 employees
Location
Россия
Representative
Марат Каримов