- Functional C#: Immutability
- Functional C#: Primitive obsession
- Functional C#: Non-nullable reference types
- Functional C#: работа с ошибками
Что такое одержимость примитивами (Primitive obsession)?
Если коротко, то это когда для моделирования домена приложения используются в основном примитивные типы (string, int и т.п.). К примеру, вот как класс Customer может выглядеть в типичном приложении:
public class Customer
{
public string Name { get; private set; }
public string Email { get; private set; }
public Customer(string name, string email)
{
Name = name;
Email = email;
}
}
Проблема здесь в том, что если вам необходимо обеспечить соблюдение каких-то бизнес-правил, вам приходится дублировать логику валидации по всему коду класса:
public class Customer
{
public string Name { get; private set; }
public string Email { get; private set; }
public Customer(string name, string email)
{
// Validate name
if (string.IsNullOrWhiteSpace(name) || name.Length > 50)
throw new ArgumentException(“Name is invalid”);
// Validate e-mail
if (string.IsNullOrWhiteSpace(email) || email.Length > 100)
throw new ArgumentException(“E-mail is invalid”);
if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”))
throw new ArgumentException(“E-mail is invalid”);
Name = name;
Email = email;
}
public void ChangeName(string name)
{
// Validate name
if (string.IsNullOrWhiteSpace(name) || name.Length > 50)
throw new ArgumentException(“Name is invalid”);
Name = name;
}
public void ChangeEmail(string email)
{
// Validate e-mail
if (string.IsNullOrWhiteSpace(email) || email.Length > 100)
throw new ArgumentException(“E-mail is invalid”);
if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”))
throw new ArgumentException(“E-mail is invalid”);
Email = email;
}
}
Более того, точно такой же код имеет тенденцию попадать в application слой:
[HttpPost]
public ActionResult CreateCustomer(CustomerInfo customerInfo)
{
if (!ModelState.IsValid)
return View(customerInfo);
Customer customer = new Customer(customerInfo.Name, customerInfo.Email);
// Rest of the method
}
public class CustomerInfo
{
[Required(ErrorMessage = “Name is required”)]
[StringLength(50, ErrorMessage = “Name is too long”)]
public string Name { get; set; }
[Required(ErrorMessage = “E-mail is required”)]
[RegularExpression(@”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”, ErrorMessage = “Invalid e-mail address”)]
[StringLength(100, ErrorMessage = “E-mail is too long”)]
public string Email { get; set; }
}
Очевидно, такой подход нарушает принцип DRY. Этот принцип говорит нам о том, что каждая часть информации о домене должна иметь единственный авторитетный источник в коде нашего приложения. В примере выше мы имеем 3 таких источника.
Как избавиться от одержимости примитивами?
Чтобы избавиться от одержимости примитивами, мы должны добавить два новых типа, которые бы агрегировали в себе логику валидации. Таким образом мы сможем избавиться от дублирования:
public class Email
{
private readonly string _value;
private Email(string value)
{
_value = value;
}
public static Result<Email> Create(string email)
{
if (string.IsNullOrWhiteSpace(email))
return Result.Fail<Email>(“E-mail can’t be empty”);
if (email.Length > 100)
return Result.Fail<Email>(“E-mail is too long”);
if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”))
return Result.Fail<Email>(“E-mail is invalid”);
return Result.Ok(new Email(email));
}
public static implicit operator string(Email email)
{
return email._value;
}
public override bool Equals(object obj)
{
Email email = obj as Email;
if (ReferenceEquals(email, null))
return false;
return _value == email._value;
}
public override int GetHashCode()
{
return _value.GetHashCode();
}
}
public class CustomerName
{
public static Result<CustomerName> Create(string name)
{
if (string.IsNullOrWhiteSpace(name))
return Result.Fail<CustomerName>(“Name can’t be empty”);
if (name.Length > 50)
return Result.Fail<CustomerName>(“Name is too long”);
return Result.Ok(new CustomerName(name));
}
// Остальная часть класса такая же, как Email
}
Достоинство этого подхода в том, что в случае изменения логики валидации, нам достаточно отразить это изменение только единожды.
Обратите вниманите, что конструктор класса Email закрыт, так что единственный способ создать его экземпляр — использовать статический метод Create, который проводит всю необходимую валидацию. Этот подход позволяет нам быть уверенными в том, что все экземпляры класса Email находятся в валидном состоянии на протяжении всей их жизни.
Вот как контроллер может использовать эти классы:
[HttpPost]
public ActionResult CreateCustomer(CustomerInfo customerInfo)
{
Result<Email> emailResult = Email.Create(customerInfo.Email);
Result<CustomerName> nameResult = CustomerName.Create(customerInfo.Name);
if (emailResult.Failure)
ModelState.AddModelError(“Email”, emailResult.Error);
if (nameResult.Failure)
ModelState.AddModelError(“Name”, nameResult.Error);
if (!ModelState.IsValid)
return View(customerInfo);
Customer customer = new Customer(nameResult.Value, emailResult.Value);
// Rest of the method
}
Экземпляры Result<Email> и Result<CustomerName> явным образом говорят нам о том, что метод Create может потерпеть неудачу, и если это так, то мы сможем узнать причину прочитав свойство Error.
Вот как класс Customer выглядит после рефакторинга:
public class Customer
{
public CustomerName Name { get; private set; }
public Email Email { get; private set; }
public Customer(CustomerName name, Email email)
{
if (name == null)
throw new ArgumentNullException(“name”);
if (email == null)
throw new ArgumentNullException(“email”);
Name = name;
Email = email;
}
public void ChangeName(CustomerName name)
{
if (name == null)
throw new ArgumentNullException(“name”);
Name = name;
}
public void ChangeEmail(Email email)
{
if (email == null)
throw new ArgumentNullException(“email”);
Email = email;
}
}
Почти все проверки переехали в Email и CustomerName. Единственная оставшаяся валидация — это проверка на null. Мы посмотрим как избавиться и от нее в следующей статье.
Итак, какие преимущества дает нам избавление от одержимости примитивами?
- Мы создаем единственный авторитетный источник знаний для каждой проблемы, решаемой нашим кодом. Никаких дублирований, только чистый и «сухой» (dry) код.
- Более строгая система типов. Компилятор работает на нас с удвоенной силой: теперь невозможно ошибочно присвоить свойству типа Email объект типа CustomerName, такой код не будет скомпилирован.
- Нет необходимости в проверке входящих значений. Если мы получаем объект класса Email или CustomerName, мы можем быть на 100% уверены, что он находится в корректном состоянии.
Небольшое замечание. Некоторые разработчики имеют тенденцию «оборачивать» и «разворачивать» примитивные типы по нескольку раз в течение единственной операции:
public void Process(string oldEmail, string newEmail)
{
Result<Email> oldEmailResult = Email.Create(oldEmail);
Result<Email> newEmailResult = Email.Create(newEmail);
if (oldEmailResult.Failure || newEmailResult.Failure)
return;
string oldEmailValue = oldEmailResult.Value;
Customer customer = GetCustomerByEmail(oldEmailValue);
customer.Email = newEmailResult.Value;
}
Лучше всего использовать кастомные типы во всем приложении, разворачивая их в примитивы только когда они выходят за границы домена, к примеру сохраняются в базу или рендерятся в HTML. В ваших доменный классах старайтесь всегда использовать кастомные типы, код в таком случае будет более простым и читаемым:
public void Process(Email oldEmail, Email newEmail)
{
Customer customer = GetCustomerByEmail(oldEmail);
customer.Email = newEmail;
}
Ограничения
К сожалению, создание типов-оберток в C# — процесс не настолько простой как в к примеру в F#. Это возможно изменится в C# 7 если будет реализован pattern matching и record types на уровне языка. До того момента, нам приходится иметь дело с неуклюжестью этого подхода.
Из-за этого некоторые примитивные типы не стоят того, чтобы быть обернутыми. К примеру, тип «money amount» с единственным инвариантом, говорящим о том, что количество денег не может быть отрицательным, может быть представлен как обычный decimal. Это приведет к некоторому дублированию логики валидации, но даже не смотря на это, такой подход будет более простым решением, даже в долгосрочной перспективе.
Как обычно, придерживайтесь здравого смысла и взвешивайте плюсы и минусы решений в каждом конкретном случае.
Заключение
С неизменяемыми и непримитивными типами мы подходим ближе к проектированию приложений на C# в более функциональном стиле. В следующей статье мы обсудим как облегчить «ошибку на миллиард долларов» (mitigate the billion dollar mistake).
Исходники
Остальные статьи в цикле
- Functional C#: Immutability
- Functional C#: Primitive obsession
- Functional C#: Non-nullable reference types
- Functional C#: работа с ошибками
Английская версия статьи: Functional C#: Primitive obsession
Комментарии (45)
impwx
16.09.2015 10:41+7Пример с
Customer
и методами-сеттерами, имхо, высосан из пальца. Почему бы просто не разместить валидацию в коде сеттера свойства?vladimirkolyada
16.09.2015 10:44-1Мне кажется плохим наделять Setter такими свойствами валидации. Какая-то двуответственность (и более) получается. Но я не автор, может он что-то иное скажет. Но мне подсознательно видится это плохим решением.
impwx
16.09.2015 10:59+4Отнюдь, как раз для этого и придуманы сеттеры:
Properties have many uses: they can validate data before allowing a change; they can transparently expose data on a class where that data is actually retrieved from some other source, such as a database; they can take an action when data is changed, such as raising an event, or changing the value of other fields.
vladimirkolyada
16.09.2015 11:30Да никто не против, что их так можно использовать, что MSDN вам и пишет. Вопрос в целесообразности использования такого подхода при каких-то цепочках валидации, зависимости одной проперти от другой и так далее. По мне сеттер должен быть сеттером.
lair
16.09.2015 11:35+4Вопрос в целесообразности использования такого подхода при каких-то цепочках валидации, зависимости одной проперти от другой и так далее.
В таких случаях надо не методChangeSmth
делать, а разносить присвоение и валидацию. А если валидация свойства атомарна (не зависит от других свойств), то ее целесообразно делать в сеттере.
Sing
16.09.2015 15:09+1> По мне сеттер должен быть сеттером.
Сеттер — это банальный метод, который запускается при попытке присвоить параметру значение. То есть, методы из примера — ChangeEmail и ChangeName — это лишние сущности, которые должны быть запрятаны в set, заодно избавив от двух проверок в конструкторе и приведя решение к DRY.
Также мне лично непонятно разночтение между приватными сеттерами параметров, но публичными ChangeEmail и ChangeName. Это ставит в ступор, как минимум.
vkhorikov
16.09.2015 14:08Отдельные методы-сеттеры сделаны для большей выразительности. Всю ту же логику можно написать и в сеттере свойства, безусловно, разницы в коде при этом не будет.
vkhorikov
16.09.2015 14:45Еще раз посмотрел на код. Да, вы правы.
Старался сделать примеры как можно проще, не хотел накручивать какую-то логику поверх обычного изменения имейла и нэйма.
darkdaskin
16.09.2015 18:57Валидацию не всегда можно уместить в сеттер. Более-менее сложные модели часто имеют правила, распространяющиеся на несколько свойств.
Ситуацию с проверками в сеттере можно довести до абсурда:
class Range { private int _min; private int _max; public int Min { get { return _min; } set { if(value > Max) throw new InvalidOperationException(); _min = value; } } public int Max { get { return _max; } set { if (value < Min) throw new InvalidOperationException(); _max = value; } } }
Получаем класс, свойства которого надо задавать в определённом порядке. Мне подобный код как-то попадался, работать с ним было очень тяжело.lair
16.09.2015 19:03+1Если валидация захватывает несколько свойств, ее нужно выносить в отдельный метод, который вызывается после присвоения всех свойств (либо, если возможно — делать метод, который меняет несколько свойств разом).
impwx
16.09.2015 20:01У вашего примера есть принципиальное отличие — вы допускаете, что объект может в какой-то момент быть инициализирован лишь частично, и следовательно быть непригодным к использованию. Тогда, как сказал lair выше, делается отдельный метод валидации, или атомарный сеттер для связанных свойств.
lair
16.09.2015 11:46+5Обратите вниманите, что конструктор класса Email закрыт, так что единственный способ создать его экземпляр — использовать статический метод Create, который проводит всю необходимую валидацию.
В этот момент вы потеряли возможность пользоваться кучей встроенных механизмов. Что, собственно и видно дальше:
[HttpPost] public ActionResult CreateCustomer(CustomerInfo customerInfo) { Result<Email> emailResult = Email.Create(customerInfo.Email); Result<CustomerName> nameResult = CustomerName.Create(customerInfo.Name); if (emailResult.Failure) ModelState.AddModelError(“Email”, emailResult.Error); if (nameResult.Failure) ModelState.AddModelError(“Name”, nameResult.Error); ... }
Теперь вместо биндинга «из коробки» нужно совершить кучу ручных действий. А если свойств десятки?
Более строгая система типов. Компилятор работает на нас с удвоенной силой: теперь невозможно ошибочно присвоить свойству типа Email объект типа CustomerName, такой код не будет скомпилирован.
Но при этом для других свойств (скажем,City
иCountry
) такой проверки нет, и теперь программисту нужно помнить про два разных механизма. Не ужас, но неудобно.
Ну и на самом деле, есть более фундаментальный вопрос к этому подходу. Он опирается на ту идею, что бизнес-сущность не может иметь в себе данные, не удовлетворяющие правилам валидации. Но в долгоживущих системах правила валидации меняются и эволюционируют, и в какой-то момент может так случиться, что данные, выдаваемые из БД, нынешним правилам валидации не соответствуют, но отображать их все еще надо (сохранять обратно при этом нельзя, но это второй вопрос). Описанный в статье подход этого просто не позволит.mayorovp
16.09.2015 12:47По поводу биндинга — можно и универсальное решение написать, будет почти из коробки (почти — потому что в коробку-то надо самому положить).
lair
16.09.2015 12:54Можно, вопрос усилий. Для классов со статическими фабриками это, скажем так, требует времени.
kekekeks
16.09.2015 13:30+1По ходу, для продвижения идеи в массы нужно сделать небольшой фреймворк для вкручивания поддержки этого счастья в ASP.NET.
vkhorikov
16.09.2015 14:30Good points.
1) По поводу ручного биндинга — вы правы, здесь теряется часть встроенного функционала, который есть в ASP.NET. Это вопрос взвешивания «за» и «против». Для меня плюсы более выразительной доменной модели перевешивают минусы необходимости писать подобный код вручную. Для более простых проектов вполне можно отказаться от этого подхода и делать по старинке.
Но при этом для других свойств (скажем, City и Country) такой проверки нет, и теперь программисту нужно помнить про два разных механизма. Не ужас, но неудобно.
Если у City и Country есть какие-то более-менее сложные инварианты, то их тоже стоит обернуть в классы-обертки.
2) По поводу невалидных данных в БД — отличный point. Есть два распространенных подхода к проблеме. Первый — вместе с ужесточением инвариантов писать скрипты для миграции данных в БД, чтобы они соответствовали новым инвариантам. Второй — создавать отдельный класс, к примеру EmailStrong для хранения имейлов, инварианты в которых были ужесточены и не давать присваивать кастомерам объекты старого Email, только нового. Со временем, когда БД придет в соответствие с новыми требованиями, старый Email удаляется, новый EmailStrong переименовывется в Email. Второй вариант сложнее, я как правило пользуюсь первым.lair
16.09.2015 14:34Если у City и Country есть какие-то более-менее сложные инварианты, то их тоже стоит обернуть в классы-обертки.
Нет у них инвариантов, поэтому не будет классов-оберток, поэтому нельзя выработать привычку к type safety net.
Первый — вместе с ужесточением инвариантов писать скрипты для миграции данных в БД, чтобы они соответствовали новым инвариантам.
К сожалению, это возможно далеко не всегда.
Второй — создавать отдельный класс, к примеру EmailStrong для хранения имейлов, инварианты в которых были ужесточены и не давать присваивать кастомерам объекты старого Email, только нового.
Я боюсь, что это не «сложнее», а «сильно сложнее», особенно учитывая, что где-то присваивать можно (мало ли, мы данные из системы в систему гоним). Опять-таки, в систему типов этот запрет присвоения уже не обернешь, снова боль.vkhorikov
16.09.2015 15:13особенно учитывая, что где-то присваивать можно (мало ли, мы данные из системы в систему гоним). Опять-таки, в систему типов этот запрет присвоения уже не обернешь, снова боль.
Обернуть можно. Где-то метод принимает старый Email, а где-то — только новый EmailStrong, который является наследником старого Email.lair
16.09.2015 15:14… и как понять, какой метод использовать в какой момент? А учитывая, что лучше все-таки использовать сеттеры, а не методы — так и вовсе весело.
vkhorikov
16.09.2015 15:19Понять — по сигнатуре метода. Т.е. какие-то методы будут работать только с новыми имейлами, для других — мы оставляем как было.
лучше все-таки использовать сеттеры, а не методы
Здесь как раз пригодится сделать небольшие методы-сеттеры для обозначения новых инвариантов класса.lair
16.09.2015 15:21Т.е. какие-то методы будут работать только с новыми имейлами, для других — мы оставляем как было.
А что мешает использовать методы со старыми сигнатурами?
Здесь как раз пригодится сделать небольшие методы-сеттеры для обозначения новых инвариантов класса.
… и теперь сломался ранее работавший маппинг.vkhorikov
16.09.2015 15:24А что мешает использовать методы со старыми сигнатурами?
Ну мы же хотим чтобы кастомерам можно было присваивать только имейлы с новыми инвариантами? Если так, то можно на уровне компилятора обозначить это изменение.
… и теперь сломался ранее работавший маппинг.
Маппинг куда? Если речь про ORM, то они умеют памить на protected setter-ы. В EF это посложнее сделать, в NH — попроще.lair
16.09.2015 15:28Ну мы же хотим чтобы кастомерам можно было присваивать только имейлы с новыми инвариантами? Если так, то можно на уровне компилятора обозначить это изменение.
Хотим. Но если мы для обратной совместимости оставили возможность присвоить «старые» емейлы, как мы запретим программисту использовать эти механизмы?
Маппинг куда?
Куда угодно. На вью-модели, на DTO.vkhorikov
16.09.2015 15:36+1Запретить не получится. Но я бы поспорил с посылкой этого утверждения. Программисты-коллеги — друзья, а не враги, цель нового класса EmailStrong — не запретить им что-то делать, а подсказать, направить на правильный путь. Ну и плюс общение между коллегами должны быть intensive, чтобы все были в курсе нововведений.
lair
16.09.2015 15:39Ну и плюс общение между коллегами должны быть intensive, чтобы все были в курсе нововведений.
К сожалению, чем больше проект (и коллектив), тем это сложнее.
withkittens
16.09.2015 23:56как мы запретим программисту использовать эти механизмы?
Запретить не запретим, но если добавить запашку — Deprecated?lair
17.09.2015 00:20Тогда эти же примечания будут показываться в других местах, и как следствие, перестанут восприниматься.
sentyaev
16.09.2015 12:42+3Customer — это объект из бизнес логики приложения.
CustomerInfo — это ваша view model.
Это абсолютно разные части приложения и они могут иметь разную валидационную логику. Они же в принципе могут сильно различаться (просто в вашем примере они одинаковые).
Поэтому вы принцип DRY не нарушаете.
CustomerInfo — это данные от пользователя, их нуно проверить на корректность (длинна, формат, обязательные/не обязательные поля и т.д.)
Customer — скорее всего я бы не позволял в контроллере делать new Customer… Я бы сделал CustomerService.Create
И сдесь может быть довольно много бизнес логики, помимо валидации. Например при создании Customer можно проверить что он уникален. Или предположим, что у нас еще есть сущьность Business, у каждого Business может быть много Customer, и Customer должен быть уникален в пределах Business.
withkittens
16.09.2015 23:59
Не является ли это анти-паттерном в C#? Такой тип идеален в Rust, там нет исключений, но в C# используются две модели — исключения иResult<Email>
bool TryXXX(..., out yyy)
.mayorovp
17.09.2015 00:01+2Для задачи валидации данных — нет, не является. Исключения слишком дорогие чтобы вызывать их постоянно — а
bool TryXXX(..., out yyy)
возвращает слишком мало информации в случае ошибки.withkittens
17.09.2015 12:54Исключения — да, понятно. Меня смутил пример, уж больно
Result
похож на собрата из Rust.
vkhorikov
17.09.2015 00:10+1Использовать исключения для валидации — плохая практика. Тут более подробно на эту тему: habrahabr.ru/post/263685
Trueteller
17.09.2015 11:54Что-то я не понял: первую статью пишете про immutability, а вторая статья показывает mutable класс Customer, как до так и после рефакторинга… Зачем?
Mixim333
17.09.2015 12:18При всем уважении, я немного не понял статью. Сам поступаю следующим образом, когда мне нужна валидация: создаю в классе protected-поле необходимого типа, например, string и для него public-свойство (get{return xxx;}set{xxx=value;}), которое выполняет все проверки, т.е.
set
{
if(this.CheckValid(value))
{
this.xxx=value;
}
else
{
throw new…
}
и все, обращения выполняю только через свойство. Ни подскажите, чем это плохо, какой паттерн я нарушаю?mayorovp
17.09.2015 12:33+2Никакого паттерна вы не нарушаете. Более того, сеттеры свойств были созданы в том числе для таких вот проверок.
Но есть несколько проблем.
1. Задача «собрать все ошибки валидации» при таком подходе превращается в кошмар.
2. Если проверка валидности не раскладывается на отдельные проверки независимых полей — то все становится очень весело (выше был пример с границами диапазона).
3. Если значение свойства по умолчанию невалидно, то подход просто не работает.
PsyHaSTe
15.10.2015 11:46Насколько мне известно, эволюция программирования шла от использования кодов возвратов к использованию исключений. Печально, что статья описывает как раз такой «шажище» назад.
lair
15.10.2015 11:56+1Вы совершенно зря считаете, что развитие программирования — это линейный эволюционный процесс. Многие вещи, которые были предложены раньше, сейчас снова становятся актуальны.
Впрочем, предлагаемая в статье монада Try — это не код возврата, это пламенный привет от функционального програмирования и явных контрактов.
mayorovp
15.10.2015 14:24Код возврата — это никому не понятное число… Result.Fail же принимает в качестве параметра строку на человеческом языке. Это не шаг назад, это шаг в сторону.
vladimirkolyada
Не совсем корректный пример с логикой проверки на уровне ViewModel MVC, там логика атрибутов пролезла потому, что по ней строится клиентская валидация автоматизированно умеет. Я действую наоборот, на границе, перед входом в домен, проверяю и валидирую все, атрибутами и дополнительными валидаторами, по необходимости, а дальше, в домене, уже оперирую чистыми моделями, считая, что они валидацию прошли и корректны. Допустим, если я получаю по ключу значение из какого-то хранилища, где он должен быть, то я делаю First(), а не FirstOrDefault(). Т.е. другими словами гарантированно генерирую исключение. Но подход с логикой валидации в одном месте исключительно правильный, надо к нему стремиться. Но я его вижу на базе атрибутов + обработчика этих правил (универсального) без реализации одного и того же в виде списка if — ов и так далее.
vladimirkolyada
Вдогонку, не успел отредактировать, прекрасный цикл статей намечается, продолжайте, пожалуйста!
Quanzi
А не получается ли, что представление должно знать о том, что для домена нужно валидировать — ведь это не его обязанность, по хорошему? Кроме того, в командной разработке вполне может сложиться ситуация, что кто-то где-то когда-то не вспомнит о какой-то из валидаций при использовании домена (т.е. ответственность за стабильность кода перекладывается на использующего). Вам так не кажется?
vladimirkolyada
У меня нет личных проектов, вся разработка от 2 до 15 человек в команде. Но я с вами полностью согласен, бывает — забывают, но пишутся UT, где быстро вспоминают, все же редкость какая-то сложная логика валидации, в основном это наличие, промежуток, даже регулярка редкость. Да, представление действительно должно знать о том, какие правила валидации есть в домене, по одной простой причине, с представлением работает человек и его не интересует моя внутренняя логика, архитектура, ему интересно как можно скорее заполнить формы и видеть свои нарушения сразу, а не после отправки данных. Приходится искать консенсус.
kekekeks
Можно по идее попробовать сделать ajax-хелпер, который пинает логику валидации конкретного типа. Тогда мы себя не ограничиваем тем, что позволяют сделать атрибуты.
Quanzi
Хорошее покрытие логики тестами действиетльно должно помогать, да. А по поводу второй части — не лучше ли, чтобы представление знало о наличии валидации, но сама валидация была скрыта внутри домена? Опять же, валидация при каждом обращении к домену это явное нарушение DRY (если каждый объект не вызывается ровно один раз).
Хотя, возможно, у вас хватает и такого способа, раз валидация не очень сложная.