Верно ли, что результат невыполнения контракта — всегда исключение, а при валидации ситуация обрабатывается внутри метода?
Верно. Причем исключения эти не должны отлавливаться в коде (разве что для логирования ошибки), приложению в этом случае нужно дать упасть (fail-fast прицнип). Более подробно на тему исключений: habrahabr.ru/post/263685
Если внутри GetById проверить OrganizationId > 0, это будет контракт или валидация?
В целом ничего не мешает обозначить это как контракт, но с точки зрения best practices контрактами лучше всего помечать значимые с точки зрения модеолирования условия. К примеру то, что юзера нельзя удалить если он уже удален — значимое условие и дает разработчику важную информацию о доменной модели приложения. Еще пример:
public class Organization : Entity
{
public void ProvisionUser(User user, Subscription subscription)
{
Contracts.Require(UsersInternal.Contains(user));
Contracts.Require(SubscriptionsInternal.Contains(subscription));
Contracts.Require(subscription.SeatsRemaining > 0);
Contracts.Require(subscription.IsProvisionable);
Contracts.Require(!user.IsProvisionedTo(subscription));
subscription.AddSeat(user);
user.AddSeat(subscription);
}
}
Здесь мы даем кучу информации о том, какие условия должны быть соблюдены, чтобы подписать юзера на subscription, клиент этого кода должен быть ответственен за их выполнение перед вызовом ProvisionUser.
Заметил, что я не ответил на этот вопрос:
Чем технически различаются контракты и валидация?
Технически разницы нет. Весь инструментарий поверх контрактов — опционален. Разница в том, как трактовать результат невыполнения условия. В случае с контрактами невыполнение условия — это всегда ошибка в приложении, с точки зрения валидации, — обычное дело, которое можно выводить юзеру в качестве ошибки.
Если я правильно понял, вопрос в том, как правильно обозначать проверки в коде и как контракты?
У меня в коде это обычно выглядит так. Предусловия контактов обозначаю через самописный класс Contracts (внутри которого if с выбрасыванием специального исключения в случае невыполнения условия):
public void DeleteUser(User user)
{
Contracts.Require(user.Status != UserStatus.Deleted);
user.MarkAsDeleted();
}
Проверки выполняются обычными if-ами:
Organization org = _organizationRepository.GetById(model.OrganizationId);
if (orgOrNothing == null)
return Error("No organization found for Id = " + model.OrganizationId);
Так при чтении кода становится понятно что есть что.
Все описанные вами проверки не являются контрактами, предусловия контрактов не должны зависить от состояния внешних по отношению к коду систем (в вашем случае это диск).
Контракты в коде — это набор техник, направленных на
1) Документирование поведения кода
2) Обеспечение быстрого фидбека в случае некорректного поведения кода
Проект CodeContracts и статические проверки — инструментарий поверх этих техник. Они полезны, но не являются обязательными.
Мы точно также можем обозначить контракты в коде (как минимум предусловия) с помощью самописных конструкций (сводящихся к обычным if-ам), главное чтобы было очевидно, что это именно контакты, а не что-то другое.
В коде клиента — имеется ввиду в коде класса-клиента внутри системы, а не в коде системы, которая вызывает API извне.
Т.е. на первой картинке баг будет в классе Class1.
Это кстати не только в Go такая тенденция. В функциональных языках тоже заметно стремление использовать контейнеры с результатом выполнения операции вместо выбрасывания исключения. Either монада как пример
В целом согласен, действительно настраивать каскад в БД или в самой ORM — разница не большая. Лично я обычно стараюсь делать так, чтобы база данных была настолько «тупой» насколько можно, поэтому если есть возможность, то все-таки выношу каскады из БД. Комментарии читал :)
Да, я убрал эту часть из примера чтобы не делать нагромождений. У меня в целом была задача показать, что EF хуже поддерживает UoW из-за того, что у него только одна стратегия генерации идшников. Пример я думаю не выбран не слишком удачный для этого, с этим я согласен.
>кто мешает руками нужные ид сохранить самостоятельно?
В EF6 есть только одна стратегия генерации целочисленных Ids — database generated ids, в этом как раз и проблема. Вставка идшников руками решила бы проблему, как это решает Hi/Lo в NH.
Вы про ваш коммент? Если да, то это не совсем то, что нужно. Необходимо получение идентификаторов не в конце операции, а именно по мере создания сущностей.
В любом случае, сделать честный UoW, с database generated идентификаторами невозможно.
Либо через транзакцию, да. Суть в том, что в случае отмены операции эти данные придется удалять (либо вручную либо через транзакцию), что приводит к проседанию производительности. Гораздо эффективнее просто не вставлять эти данные в БД до момента завершения транзакции.
Имеется ввиду, что единственный способ в EF получить целочисленный идентификатор сущности — это сохранить ее в БД. Соответственно если в процессе операции вы решите отменить эту операцию, то вам придется удалить все сохраненные к этому моменту записи вручную. В NH есть различные стратегии генерации ID, что позволяет генерировать их не сохраняя саму сущность в базе.
В EF7 это кстати поправили, Rowan Miller говорит, что там уже есть Hi/Lo стратегия как в NH.
Да, этот момент я пропустил. Согласен, предусловия у MoveNext есть, но все же они не требуют наличия коннекта к БД (если за эталон брать реализацию List-а и пары других реализаций в BCL). Тут опять же вопрос — что брать за эталон, т.к. явных контрактов эти интерфейсы не объявляют.
Сначала вы берёте List как эталонную реализацию и говорите, что Reset, Current и MoveNext могут бросать исключения когда им вздумается:
Нет, я как раз пишу, что Reset и Current могут бросать исключения как им вздумается (исходя из нескольких реализаций в .NET), а вот MoveNext всегда возвращает bool, и не имеет при этом никаких предусловий. DbQuery нарушает принцип, потому что усиливает предусловия именно в MoveNext, а не в Reset или Current. Думаю стоило обозначить это более явно.
Вовсе нет. Нигде не объявлено, что Process работает с конечной коллекцией. Бесконечно обрабатывать запросы — вполне валидная задача.
Тут тоже наверное нужно было подобрать более подходящий пример. Этим примером я хотел показать, что Process ожидает именно конечную коллекцию. То, что бесконечная обработка запросов является валидной задачей — это безусловно.
Верно. Причем исключения эти не должны отлавливаться в коде (разве что для логирования ошибки), приложению в этом случае нужно дать упасть (fail-fast прицнип). Более подробно на тему исключений: habrahabr.ru/post/263685
В целом ничего не мешает обозначить это как контракт, но с точки зрения best practices контрактами лучше всего помечать значимые с точки зрения модеолирования условия. К примеру то, что юзера нельзя удалить если он уже удален — значимое условие и дает разработчику важную информацию о доменной модели приложения. Еще пример:
Здесь мы даем кучу информации о том, какие условия должны быть соблюдены, чтобы подписать юзера на subscription, клиент этого кода должен быть ответственен за их выполнение перед вызовом ProvisionUser.
Заметил, что я не ответил на этот вопрос:
Технически разницы нет. Весь инструментарий поверх контрактов — опционален. Разница в том, как трактовать результат невыполнения условия. В случае с контрактами невыполнение условия — это всегда ошибка в приложении, с точки зрения валидации, — обычное дело, которое можно выводить юзеру в качестве ошибки.
У меня в коде это обычно выглядит так. Предусловия контактов обозначаю через самописный класс Contracts (внутри которого if с выбрасыванием специального исключения в случае невыполнения условия):
Проверки выполняются обычными if-ами:
Так при чтении кода становится понятно что есть что.
1) Документирование поведения кода
2) Обеспечение быстрого фидбека в случае некорректного поведения кода
Проект CodeContracts и статические проверки — инструментарий поверх этих техник. Они полезны, но не являются обязательными.
Мы точно также можем обозначить контракты в коде (как минимум предусловия) с помощью самописных конструкций (сводящихся к обычным if-ам), главное чтобы было очевидно, что это именно контакты, а не что-то другое.
Т.е. на первой картинке баг будет в классе Class1.
Может. Почитайте про Hi/Lo algorithm.
Например тут: stackoverflow.com/questions/282099/whats-the-hi-lo-algorithm
В EF6 есть только одна стратегия генерации целочисленных Ids — database generated ids, в этом как раз и проблема. Вставка идшников руками решила бы проблему, как это решает Hi/Lo в NH.
В любом случае, сделать честный UoW, с database generated идентификаторами невозможно.
В EF7 это кстати поправили, Rowan Miller говорит, что там уже есть Hi/Lo стратегия как в NH.
Нет, я как раз пишу, что Reset и Current могут бросать исключения как им вздумается (исходя из нескольких реализаций в .NET), а вот MoveNext всегда возвращает bool, и не имеет при этом никаких предусловий. DbQuery нарушает принцип, потому что усиливает предусловия именно в MoveNext, а не в Reset или Current. Думаю стоило обозначить это более явно.
Тут тоже наверное нужно было подобрать более подходящий пример. Этим примером я хотел показать, что Process ожидает именно конечную коллекцию. То, что бесконечная обработка запросов является валидной задачей — это безусловно.