Это статья является адаптацией моего выступления на конференции UfaDevConf 2025.
Представьте, что вы работаете в логистике. У вас десяток интеграций: маркетплейсы, партнёры, внутренние сервисы. Каждый из них присылает запросы на создание Отправления в пункт выдачи заказов (ПВЗ). У Отправления есть вес. Один партнёр шлёт вес в граммах. Второй — в килограммах.
В ПВЗ же весовые ограничения лежат в килограммах. В одном месте мы умножили на 1000, в другом — забыли. В третьем — округлили в одну сторону, в четвёртом — в другую. В итоге часть Отправлений нельзя сдать в нужный ПВЗ в результате запроса, хотя физически Отправления проходят лимиты без проблем.
Добавьте сюда деньги без валюты. Проценты, которые то 0.8, то 80. А может, и строку String color, которая иногда hex, иногда rgb, а иногда вообще "green".
Это и есть примитивная одержимость — когда сложные доменные понятия мы упрямо храним в String, int, BigDecimal, надеемся «не перепутать» и создаём отдельные методы для обработки этих примитивов. Спойлер: мы всё равно перепутаем и забудем, где правильная обработка.
В этой статье разберёмся, как вытащить бизнес‑смысл из примитивов в Value Object — и при этом не тащить за собой весь DDD: агрегаты, bounded context’ы и прочую тяжёлую артиллерию.
Содержание
История про ПВЗ, вес и один очень уставший BigDecimal
Начнём с упрощённой модели ПВЗ и Отправления с минимальным набором полей.
Наша задача — определить подходит ли для отправления выбранный ПВЗ. Наивный вариант:
class PickupPoint {
BigDecimal minWeight;
BigDecimal maxWeight;
}
class Parcel {
BigDecimal weight;
}
Но тут нет ясности, что в одном случае это килограммы, а в другом — граммы. Давайте исправим!
class PickupPoint {
// Минимальный возможный вес отправления в килограммах
BigDecimal minWeightKg;
// Максимальный возможный вес отправления в килограммах
BigDecimal maxWeightKg;
}
class Parcel {
// Вес отправления в граммах
BigDecimal weightGrams;
}
Комментариями мы честно подсказали, где килограммы, а где граммы. Кажется, жить можно. Любой разработчик может заглянуть в код класса, посмотреть на имя переменной, увидеть Kg и Grams и учесть это различие. Из минусов — это надо держать в своей голове: компилятор в этом уже не поможет.
Теперь пишем проверку: можно ли отправление сдать в выбранный ПВЗ. Вынесем проверку в отдельный метод, таким образом мы можем его переиспользовать. Будем передавать сразу ПВЗ и Отправление целиком:
public static boolean isParcelWeightFit(PickupPoint pickupPoint, Parcel parcel) {
BigDecimal gramsInKg = BigDecimal.valueOf(1000);
BigDecimal minWeightGrams = pickupPoint.getMinWeightKg().multiply(gramsInKg);
BigDecimal maxWeightGrams = pickupPoint.getMaxWeightKg().multiply(gramsInKg);
return minWeightGrams.compareTo(parcel.getWeightGrams()) <= 0
&& maxWeightGrams.compareTo(parcel.getWeightGrams()) >= 0;
}
А теперь ответьте на вопрос: «Что тут не так?»
Метод знает слишком много про детали хранения веса и ПВЗ, и отправления.
Конвертация килограммы ⇄ граммы уже размазана по коду. Если нам надо ещё где-то выполнять конвертацию, думаю, захочется создать отдельный метод, в котором мы будем передавать граммы или килограммы. И с этого момента надо следить, чтобы каждый разработчик не написал свою однострочную реализацию.
Всё грязное бельё внутрянки BigDecimal выставлено наружу, это неудобное сравнение через compareTo. Не очень читаемый код для бизнес-логики.
Теперь добавим реальность:
В одном месте вес хранится в
double, в другом — вBigDecimal.Кто-то вспомнил про округление и указал
HALF_UP, в другом —HALF_DOWNили вовсе не указали. В третьем расчёте указали точность вычислений, которой нет в остальных частях приложения.Всплывает Отправление с весом
-10грамм, так получилось.
То, что вес не может быть отрицательным, знает каждый человек. Но код об этом никак не узнаёт, если мы передаём голый BigDecimal. Вы можете сказать: «Ну, данные не берутся из ниоткуда, они получены из внешних источников». И мы честно и надёжно проверили, что в код не может просочиться ничего лишнего и невалидного. И это правда, если ваш код простой и не выполняет никаких вычислений, валидация может быть вашим помощником. Но как только вы обрабатываете, сравниваете значения — валидация уже тут не поможет.
Хорошо, признаюсь, вам в этом случае могут помочь тесты. Если вы каждый случай конвертации или обработки данных покроете юнит-тестами, вы сможете спать спокойно. Только придётся написать большое количество тестов и учитывать конвертации даже в случаях, когда цель тестов — совсем другая бизнес-логика.
Primitive obsession: когда числа и строки захватили ваш домен
Primitive obsession или Одержимость примитивами — это использование базовых типов (строк, чисел) для представления сложных доменных понятий.
Частые случаи, когда мы, не думая, используем примитивы (ладно, это примеры из моей жизни):
String color, который иногда#ffffff, иногда255,255,255. Это один и тот же цвет, но в разных форматах.BigDecimal moneyAmount, но без валюты. Это уже встречается редко, но в моновалютных приложениях всё ещё есть фиксированная валюта, и кажется, можно просто использовать одно фиксированное значение. А потом появляются внешние данные в новой валюте → перепишите каждое место где используетсяmoneyAmount.double progress, который в одних местах 0.8 (80%), а в других — 80 (тоже 80%, но уже руками). Так сотрудники выполняли план на 8000%.String barcode, но это EAN-13? Code128? QR? Как его читать, как его печатать?Несколько
Stringаргументов в методе: номер ПВЗ, номер договора, ID магазина — перепутать легко, компилятор не поможет.
Почему это создает проблемы:
Вы теряете контекст значения. Число 25 само по себе ничего не значит: это килограммы, сантиметры или проценты? Это может быть что угодно. У вас остаётся только название переменной, которое обязано в полной мере передать контекст значения числа.
У вас нет истории валидности. Вы проверили данные на входе, но после трёх слоёв вызовов уже никто не помнит, валидны ли они. Это создаёт ситуации, когда входные аргументы метода многократно проверяются.
Отсутствует контроль инвариантов. Вес может уйти в минус, процент — стать 1200%, а moneyAmount — внезапно оказаться в тенге. Про дублирование логики уже упоминал.
Конечно, Примитивы хороши своей простотой: не надо ничего придумывать, просто пишем BigDecimal и бежим дальше. Но эта простота мстит вам через полгода, когда это поле уже расползлось по коду и его невозможно контролировать.
Инварианты: правило, которое всегда должно быть истинным
Второе ключевое слово статьи — инвариант. Инвариант — это правило над данными, которое должно быть истинно всегда. В любой момент работы приложения. Объект не может быть создан, если нарушен инвариант.
Простые примеры инвариантов: вес >= 0; процент выполнения плана от 0 до 100 включительно; у магазина должен быть хотя бы один юридический документ: договор или доп. соглашение.
Важно отличать инварианты от валидации:
Инвариант — жёсткое правило, которое почти никогда не меняется. Его можно смело зашить в код.
Валидация — правило, которое может меняться в конфигурации или базе (например, минимальная сумма заказа или ограничение по региону).
Инварианты — идеальное место для Value Object.
Что такое Value Object
Value Object (VO) — это тип, который представляет доменное понятие, не имеет собственной идентичности, сравнивается по значению, неизменяем и всегда хранит свои инварианты.
Разложим по пунктам:
Представляет доменное понятие. И называется теми же словами, которыми говорит бизнес, аналитики, менеджеры. Всем понятно, что значит имя этого объекта.
Weight,Length,Money,Email,PickupPointId,Barcode,Percent,ShopLegalDocs— всё это кандидаты на Value Object.Не имеет идентичности. Два объекта
Weight(1000)иWeight(1000)считаются равными, даже если созданы в разное время.Равенство по содержимому. Просто сравниваем поля напрямую. Поэтому если вес хранить как значение и единицу измерения, то
1 Кги1 000 гнеравны. Если хранить всегда в граммах, то сравнение будет простым.Неизменяемый. Создали
Weight(500)— он таким и останется. Любая мутирующая операция возвращает новый объект. Добавили 200 к весу — получили новый объектWeight(700), а старыйWeight(500)остался неизменным.Всегда хранит инвариант. Если мы видим в коде
Weight, можем быть уверены: внутри нет отрицательного числа и сломанных единиц измерения.Может иметь мини‑API. Удобные методы, которые делают бизнес‑логику читаемой:
isBetween,toKilograms,plus,minus,isZeroи так далее.
И главное, вам не нужен DDD, чтобы использовать Value Object. Вы можете использовать любую парадигму: внедрение VO не разрушает принципы написания вашего кода.
«Они уже среди нас»
До Java 8 у нас была стандартная боль: java.util.Date, Calendar, часовые пояса, UTC и переводы времени. А ещё можно было легко создать нерабочий вариант даты или изменить дату в другом объекте, ведь объекты были изменяемыми.
java.time эту боль заметно утихомирил. Instant, LocalDate, Duration, Period — типичные Value Object. Они неизменяемы, проверяют границы (нет 13‑го месяца и 31 февраля), прячут внутри массу специфичной логики: високосные годы, количество дней, сдвиги по часовым поясам. Имеют удобный API для движения по временной шкале. В исходниках Instant, Period, Duration, LocalTime больше 1 000 строк в каждом. Это тяжёлые универсальные VO для любого приложения.
Не предлагаю вам делать каждый свой VO универсальным. Нужны маленькие, локальные Value Object под наш бизнес с минимальным необходимым набором инвариантов.
Проектируем Weight
Вернёмся к задаче с ПВЗ и посылками.
Шаг 1. Заводим простой Value Object
Начнём с минимальной реализации на Java:
public record Weight(long grams) implements Comparable<Weight> {
public Weight {
if (grams < 0) {
throw new IllegalArgumentException(
"Weight cannot be negative: %s g".formatted(grams)
);
}
}
@Override
public int compareTo(Weight other) {
return Long.compare(this.grams, other.grams);
}
}
Пробежимся по чек-листу VO:
✅ Инвариант: всегда
grams >= 0✅ Неизменяемость (за нас это сделал
recordиз Java 16)✅ Равенство по значению
✅ Естественное сравнение по другому весу.
Теперь в любом месте системы, увидев Weight, мы можем доверять этому типу: отрицательных значений нет, значение всегда задано и неизменно.
Шаг 2. Фабричный метод и конвертация из килограммов
Добавим фабрику, чтобы не размазывать BigDecimal по коду:
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());
}
Теперь вся конвертация килограммов → граммы живёт в одном месте. Хотите еще и фунты/унции/караты/пуды — добавляете
Enum WeightUnit и методы of(int value, WeightUnit unit) и to(WeightUnit unit).
Шаг 3. Делаем код читаемым: isBetween
Вместо того, чтобы сравнивать веса вручную, спрячем логику в метод:
public boolean isBetween(Weight first, Weight second) {
long min = Math.min(first.grams, second.grams);
long max = Math.max(first.grams, second.grams);
return this.grams >= min && this.grams <= max;
}
Теперь проверка «подходит ли вес посылки под ПВЗ» превращается в простой и понятный код:
public static boolean isParcelWeightFit(PickupPoint pickupPoint, Parcel parcel) {
return parcel.getWeight().isBetween(
pickupPoint.getMinWeight(),
pickupPoint.getMaxWeight()
);
}
Логика не изменилась, но код стал читаться как текст: «вес посылки находится между минимальным и максимальным весом ПВЗ». Мы ещё сделали удобство: не важно, в каком порядке передавать min и max — метод isBetween взял всё на себя.
Шаг 4. Добавляем мини‑API
Когда вы начинаете активно использовать Weight, очень быстро хочется ещё удобств:
Складывать веса:
public Weight plus(Weight other) {
return new Weight(this.grams + other.grams);
}
Вычитать:
public Weight minus(Weight other) {
return new Weight(this.grams - other.grams);
}
Умножать:
public Weight times(long factor) {
return new Weight(this.grams * factor);
}
Проверяем, что вес нулевой:
public boolean isZero() {
return this.grams == 0;
}
Получается микробиблиотека работы с весом, заточенная ровно под ваш проект. Вы можете использовать уже готовые библиотеки для физических величин, но бывает, они громоздкие и с лишним функционалом. С ними ещё надо разбираться, а у нас получилось очень просто и понятно.
Тестируем Value Object: просто, быстро, надёжно
Ещё один наиприятнейший бонус у Value Object — их невероятно приятно и просто тестировать.
Один из вариантов параметризованного теста:
@ParameterizedTest(name = "{3} -> {2}kg between {0}g and {1}g")
@CsvSource({
"0, 10000, 2, true",
"0, 10000, 20, false"
})
@DisplayName("Weight is between two other weights.")
void isBetween_worksCorrectly(int grams1, int grams2, int kg, boolean expected) {
var wToCheck = Weight.ofKilograms(kg);
var w1 = new Weight(grams1);
var w2 = new Weight(grams2);
assertThat(wToCheck.isBetween(w1, w2)).isEqualTo(expected);
}
Пара десятков таких тестов — и вы с высокой уверенностью знаете, что всё, что касается веса, ведёт себя корректно. А главное — дальше вы просто забываете, как это устроено внутри, и пользуетесь Weight как чёрным ящиком.
Value Object: когда несколько примитивов — одно понятие
Value Object это не только про вес, температуру, длину и прочие физические и знакомые каждому величины. Это ещё могут быть ваши базовые бизнес-условия и понятия.
Пример: юридические документы магазина
Бизнес‑правило: у интернет‑магазина должен быть хотя бы один юридический документ — договор или доп. соглашение.
Можем выразить это с помощью VO так:
public record ShopLegalDocs(
Contract contract,
AdditionalAgreement agreement
) {
public ShopLegalDocs {
if (contract == null && agreement == null) {
throw new IllegalArgumentException(
"Магазин должен иметь хотя бы один юридический документ"
);
}
}
}
Теперь любой код, который получает ShopLegalDocs, может не беспокоиться о кейсе «у магазина нет ни одного документа» — такой объект просто невозможно создать.
Пример: ID как типы, а не строки
Вернёмся к ПВЗ:
public record PickupPointId(String value) {}
public record ContractNumber(String value) {}
public class PickupPoint {
PickupPointId id;
ContractNumber contract;
Weight minWeight;
Weight maxWeight;
}
После этого невозможно вызвать метод, принимающий номер ПВЗ и номер договора, так, чтобы местами перепутать номер ПВЗ и номер договора. Компилятор вам не позволит совершить такую ошибку.
Исключения, Either, Result: что возвращать из VO
Простейший способ защищать инвариант — бросать исключения в конструкторе. Так и было показано в примерах выше. Хотя, думаю, многих учили, что нельзя ни в коем случае бросать исключение в конструкторе. Даже если придерживаться этого правила, всегда можно сделать фабричный метод, который всё проверит, а уже потом вызовет приватный конструктор. За примерами далеко ходить не надо: вот вам класс Instant — его нельзя создать через new Instant(). Используйте статические фабричные методы, например:
public static Instant ofEpochMilli(long epochMilli) {
long secs = Math.floorDiv(epochMilli, 1000);
int mos = Math.floorMod(epochMilli, 1000);
return create(secs, mos * 1000_000);
}
private static Instant create(long seconds, int nanoOfSecond) {
if ((seconds | nanoOfSecond) == 0) {
return EPOCH;
}
if (seconds < MIN_SECOND || seconds > MAX_SECOND) {
throw new DateTimeException("Instant exceeds minimum or maximum instant");
}
return new Instant(seconds, nanoOfSecond);
}
Этот подход вы можете использовать и в своих VO. От исключений это вас не оградит. Да и меня самого раздражают эти исключения, вот почему:
Исключение может быть не связано с самим процессом напрямую. Это значит, вы формируете ПВЗ, а получили ошибку «Вес не может быть меньше 0». Хотя вы просто хотели вывести список ПВЗ.
Сложнее строить аккуратные ответы клиенту, легко можно «Вес не может быть меньше 0» отдать в ответ клиенту. Он ничего не поймёт — причём тут вес.
Добавляется некрасивый код для отлова исключений и реагирования на них. Общий отлов исключений не очень помогает, ведь как реагировать «Вес не может быть меньше 0». Только отлавливать при формировании объекта ПВЗ и уже в этом случае отвечать понятным текстом.
Альтернатива — функциональный стиль: возвращать Either/Result.
На Java можно использовать библиотеку Vavr, на Kotlin — Arrow, на C# — Result библиотеки вроде CSharpFunctionalExtensions.
Пример на Java c Vavr:
public final class PickupPointNumber {
private static final Pattern REGEX = Pattern.compile("\\d{10}");
private final String value;
private PickupPointNumber(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public static Either<String, PickupPointNumber> of(String raw) {
if (!REGEX.matcher(raw).matches()) {
return Either.left("Номер ПВЗ должен состоять из 10 цифр");
}
return Either.right(new PickupPointNumber(raw));
}
}
Пример на Kotlin c Arrow:
data class PickupPointNumber private constructor(val value: String) {
companion object {
private val regex = Regex("\\d{10}")
fun of(raw: String): Either<String, PickupPointNumber> =
if (!regex.matches(raw))
Either.Left("Номер ПВЗ должен состоять из 10 цифр")
else
Either.Right(PickupPointNumber(raw))
}
}
Мы больше не бросаем исключения. Вместо этого явно возвращаем либо ошибку (Left), либо успешно созданный VO (Right). Больше примеров использования на все случаи жизни вы найдете в документации к библиотекам.
Конкретный выбор — дело вкуса и стека. Важно следовать правилу: инвариант не должен нарушаться никогда. Если VO создан — он валиден всегда.
Производительность и память: не раньше профилирования
Самый частый аргумент против VO, который я слышу:
«Мы же создаём кучу объектов! Всё уедет в хип, GC не справится, перекладывания и создание объектов нагружают CPU».
Технически, так оно и есть: новые объекты создаются, при модификации ещё раз создаются. Вместо сырого long или BigDecimal мы теперь таскаем объект с этими же полями внутри.
Но прежде чем вычищать или отказываться от VO ради мнимой производительности, необходимо это доказать. Доказать, что именно VO делает ваше приложение медленнее и нагруженнее. Тут самое время вспомнить Дональда Кнута, и не его расхожую цитату. А суть — оптимизация нужна там, где есть реальная проблема. Часто оказывается, что ваши проблемы связаны с производительностью в базе, сетевых вызовах или сериализации, а не в создании пары сотен крошечных объектов. Можно переиначить поговорку: семь раз отмерь, один раз оптимизируй.
Развитие языков или уже оптимизирует работу с мелкими объектами для VO или собирается. Тут пока Java в отстающих по сравнению с Go или C#. В Java развивают Project Valhalla, он двигает в сторону value‑типов, которые будут храниться компактнее и дешевле, чем обычные объекты.
В высоконагруженных системах, где каждая наносекунда важна, VO действительно могут оказаться заметным фактором. Но в большинстве прикладных сервисов вы получите больше пользы от типобезопасности и предсказуемости, чем потеряете на лишних аллокациях.
Legacy, базы и грязные данные
Встретился один вопрос после доклада:
«У нас уже есть база, в ней могут лежать отрицательные веса и кривые проценты. Если я добавлю VO c жёсткими инвариантами, я просто не смогу прочитать старые данные!»
Да, это реальная проблема. Стратегия обычно такая:
Слой персистенции — это граница. Repository / DAO вытаскивает сырые данные и пытается собрать из них VO.
Если данные несовместимы с инвариантами VO, у вас есть несколько вариантов:
• маппитьnull/ «грязные» значения в безопасные (Weight(0)вместо-1);
• помечать такие записи как некорректные и не пускать дальше;
• запускать миграции данных, чтобы привести базу в консистентное состояние.
Постепенно всё, что проходит в доменный слой, становится чистым и проверенным, а весь legacy остаётся снаружи.
Где Value Object не нужны
Как и с любым решением, важно не превратить подход в религию. В моём докладе это так и называется — Value Object Obsession.
Не стоит заводить VO, если:
Нет бизнес‑логики над данными. Вы просто получаете поля и тут же сохраняете их в базу, не делая проверок и вычислений.
Сервис тривиален. Пара классов, простой CRUD, никакой сложной предметной области. Там и так всё влезает в голову.
Вы посчитали, и накладные расходы неприемлемы. Это бывает редко, но в особо горячих местах оптимизация важнее выразительности.
В остальных случаях VO чаще помогают, чем мешают.
Мечтают ли AI-агенты о Value Object?
Когда вы используете типы с явным смыслом (Weight, Percent, PickupPointId), вы помогаете не только коллегам и компилятору, но и AI-агентам. В файле AGENTS.md я добавляю, если агент сам не написал: «Вот в этой папке у меня лежат все VO, используй их и дополняй при необходимости». Скорее всего, агент и сам их найдёт при индексации проекта, но уточнить — никогда не лишнее.
Плюсы достаточно очевидны. Меньше неоднозначности — меньше шансов, что агент предложит ерунду. Инварианты в VO — это источник подсказок. Агент видит, что Weight нельзя создать с отрицательным значением. Поведение VO (его методы) подсказывает агентам, как правильно работать с доменом, вместо придумывания своих решений.
То есть VO — это не только про людей, но и про сотрудничество с инструментами AI. А возможно, это ещё одна помощь для AI избавиться от разработчиков :)
С чего начать прямо завтра
Если хочется попробовать, но неясно, как подступиться — можно так:
Найдите в коде одно «больное» место: вес/длину, деньги, проценты, e‑mail, телефоны, адреса и т.д.
Определите инварианты (это чаще всего самый сложный пункт). Надо исследовать ограничения, если их не было в коде. Какие значения допустимы, какие единицы измерения используются, какие преобразования вы делаете постоянно.
Сделайте маленький VO. Содержит одно или пару полей. Если есть желание сделать больше, то возможно, это что-то действительно сложнее VO и стоит остановиться. Конструктор с проверкой инвариантов. Добавьте по вкусу несколько методов мини‑API (например,
isBetween,plus). Обеспечьте сравнение объектов по значению поля и при возможности добавьте иммутабельность. Напишите тесты на ваш VO, по моему мнению, без тестов сложно обеспечить гарантию правильной работы вашего VO. Всё-таки это ячейка с вашей базовой бизнес-логикой, и она должна быть максимально надёжной, у вас не должно быть в ней сомнений.Замените примитивы на VO в одном use‑case или сервисе.
Через какое‑то время поймайте себя на мысли, что вам не хватает VO в других местах.
Самое приятное в этом подходе — он не требует менять фреймворк, архитектуру или тащить весь DDD. Это просто ещё один способ использовать типы так, чтобы код лучше отражал вашу предметную область и вам было удобнее и спокойнее работать.
Вместо заключения
Подведём итог:
Primitive obsession — это не «преступление», но источник массы мелких и неприятных багов в сложной бизнес‑логике.
Value Object — простой способ инкапсулировать инварианты, сделать код безопаснее и сократить дублирование.
Разделяйте инварианты и валидацию: первые зашивайте в VO, вторые оставляйте в доменных сервисах.
Не превращайте VO в культ: используйте их там, где они реально снижают сложность.
Начать можно с одного
Weight— и уже этого хватает, чтобы почувствовать разницу.
Ссылки
Подготовил мини-гайд на GitHub с полезными ссылками на статьи, избранные видео по теме и примерами простых проектов с VO на Java, Kotlin, Go, Python, C#.
Выступление на конференции UfaDevConf, на котором основана статья, можно найти на YouTube | RuTube
Комментарии (6)

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

sendelust Автор
16.12.2025 08:19Конечно есть, в Java есть стандарт JSR 385 (Units of Measurement API 2.0) и популярная реализация это библиотека Indriya. Мы его используем, особенно востребована библиотека в "международных" сервисах, когда величины разные и зависят от страны.
В некоторых случаях используется дополнительная обертка над библиотечными классами для добавления собственных свойств и инвариантов. Почему используется не всегда и везде, потому что иногда это избыточно и усложняет код.
Для денег тоже есть устоявшаяся библиотека JSR 354 RI Moneta. И я бы ее рекомендовал для работы с деньгами по умолчанию.
BadDancer
Имхо, местами с функциональщиной перебор:
return new Weight (kilograms * 1000L); - вполне достаточно.
sendelust Автор
Да, вы правы, окрулять не надо если у нас и так целые значения и храним в long.
Мне хотелось этим показать, что можно упаковать в метод правила округления.
Думаю вариант с аргументов double лучше демонстрирует идею:
Хотя если копать дальше, я бы предпочёл округлять и нормализовать scale именно в конструкторе в итоге, тогда точно будет одна точка подготовки данных.
AlexViolin
В C# double не даёт нужной точности значения. Всегда использую decimal.
sendelust Автор
Коммент для треда "ваша джава творит дичь" :)
В Java тоже обычно используют BigDecimal для точности. Но есть бесячая проблемка. Если в C# при сравнении decimal "созданной с разной точностью" сравнения происходят без проблем:
То в Java разная точность и надо сравнивать через compare, так как через equals не будет true. Поэтому связываться с этим в бизнес логики совсем неинтересно:
Строки взяты для быстрой настройки scale и для точности.