Очень удобная штука. Вынесли валидацию и транзакции
DataAnnotation валидация
public class DataAnnotationsValidationPreProcessor<TRequest> : IRequestPreProcessor<TRequest>
{
public Task Process([NotNull] TRequest request, CancellationToken cancellationToken)
{
var context = new ValidationContext(request);
var results = new List<ValidationResult>();
if (Validator.TryValidateObject(request, context, results))
{
return Task.CompletedTask;
}
var errors = results.Select(x => new ValidationFailure(x.MemberNames.First(), x.ErrorMessage));
throw new ValidationException(errors);
}
}
"Не пользуйтесь ООП в ООП"? Ну круто, теперь заживём.
ООП это не только наследование. Я не призываю отказываться от наследования совсем, но мой подход чем реже, тем лучше.
А если писать код как попало, то это "частенько" ведёт к нарушению всего SOLID. Это не причина.
Серебряной пули и правда нет. Но есть best practices и они появились не с пустого места. А насчёт аббревиатур — это удобный способ донести мысль другому человеку по-быстрому.
Ага. Все рекламируют композицию и делегирование, а что в большинстве языков нет реализации интерфейса через член, советующих не волнует — ну, крутись как хочешь, плоди сотни строк непродуктивного кода.
Везде свои компромиссы. Хотите хорошую систему, с которой приятно работать и удобно вносить изменения — делегируйте; проект небольшой — колбасьте код как угодно.
Да и про какие сотни строк вы говорите? Если у вас есть класс, который реализует так много интерфейсов, то проблема возникла раньше. Опять же из-за не следования хорошим практикам.
Жить вообще опасно. Просто если хочешь унаследоваться вместо композиции — подумай 10 раз, унаследоваться глубоко — 100, унаследоваться множественно — 1000.
Об этом и речь — зачем усложнять и думать 10-100-1000 раз, если можно сделать просто?
Интерфейсы ни от чего не спасают. Это ужасная сущность с точки зрения развития системы, потому что их изменять вообще невозможно. Любое изменение — всё, система сломана.
Интерфейсы это ваш контракт. Это ваш api, если хотите. И если контракт меняется, значит на то была причина — изменилось требуемое поведение.
Чем это лучше классов, где что-то в предках изменилось, и вдруг поломался потомок? Ну, хотя бы есть ненулевой шанс, что оно будет работать. Изменение интерфейса ломает систему всегда.
В том и проблема, что оно может будет работать. А может не будет. А может будет работать не так, как надо. А может появиться новое поведение, которое не ожидалось. В любом случае, чтобы быть уверенным придётся проверить всех наследников. С композицией надо проверить только там, где изменилось.
Рассуждение выше про интерфейсы немного устаревает с введением костыля под названием "default interface implementation" — интерфейсы теперь становятся недо-классами.
Согласен, default interface implementation неоднозначная фича. Пока что, я вижу ей применение для добавление утилитарного поведения, вроде того же observer'a.
Кстати, насчёт глубокой цепочки наследования в GUI — тот же реакт построен на High Order Components и там этот подход весьма органичен.
Лениться делегировать — это экономия на спичках. Больше огребётесь от огромного количества наследников.
Наследование это не способ писать меньше кода. Это способ выразить отношение is-a (ну и ещё способ сделать discriminated union, в языках где его нет). Более того, я считаю что наследование как раз более вредно — чем чаще вы наследуете, тем более костной становится ваша система.
Примеры:
У вас есть базовый класс, который часто наследуется в вашей системе. Затем, только некоторому числу наследников понадобилось новое поведение — вы меняете базовый класс, но вместе с этим вы также меняете и контракт тех классов, которым это поведение не нужно. Как итог, вам нужно протестировать те компоненты, которые даже не менялись — нарушение OCP. А ещё частенько ведёт и к нарушению LSP.
В укор первому примеру, вы можете сказать — "да я щас наделаю много мелких классов (например, VisibleAndSolid, VisibleAndMovable, VisibleAndSolidAndMovable) с точечным поведением и буду множественно наследовать от них". Ок, но чего вы этим сэкономите? Количество LOC будет примерно сравнимым при композиции. Только в этот раз вы усложнили систему, наделав в ней кучу ненужных сущностей.
Имея некоторый базовый класс, вы делаете вид, что знаете как он будет использоваться. Нарушение инкапсуляции здесь ещё грубее — каждый разработчик должен знать детали реализации в базовом классе (иначе опять можно нарушить LSP).
Ещё более опасна длинная цепочка наследования. Например, есть у вас некоторая иерархия с некоторым поведением в самом верхнем родителе. Затем, одному или нескольким наследникам нужно отличное поведение. Как итог порождается ещё одна иерархия классов, что в конечном итоге ведёт к сложности системе.
С двумя интерфейсами проще вешать декоратор/сегрегировать пайплан в том же медиатре.
Например, запускать транзакцию только для комманд. Можно пойти еще дальше и добавить отдельный маркерный интерфейс/атрибут для комманд, требующих транзакцию с параметром уровня изоляции.
По поводу только одного интерфейса- достаточно завести свой собственный интерфейс и унаследовать от IRequest
Что-то вроде
interface IQuery: IRequestКовариацию не забудьте только, мне с телефона неудобно редактировать
У медиатра ваш прием с ResultHandler'ом не пройдет, поскольку вы меняете тип возвращаемого значения.
Но всякие валидации, транзакции и прочие шаблоны очень здорово залетают в pipeline behavior
Mediatr очень хорошо подойдет. Сделали на нем уже два проекта.
Посмотрите мои ссылки выше и ниже, там статьи от создателя медиатра с хорошей теорией и практикой.
Спасибо за статью!
У меня нубский вопрос — а как быть с загрузкой данных во write части? Городить ещё репозиториев для CommandHandler'ов?
Сейчас я в обработчиках использую EF контекст напрямую и при тестировании подменяю хранение на InMemoryStorage (у нас ef core) и вроде всё хорошо, но как-то не по себе.
В общем-то, пример можно посмотреть здесь у Jimmy Bogard'a
Всё просто, на самом деле. Смотрите здесь https://github.com/jbogard/MediatR/wiki/Behaviors
Очень удобная штука. Вынесли валидацию и транзакции
rider нынче позволяте дебажить чужие исходники
Не то, чтобы это прям решение для всех, но в решарпере есть возможность делегировать реализацию в один клик.
ООП это не только наследование. Я не призываю отказываться от наследования совсем, но мой подход чем реже, тем лучше.
Серебряной пули и правда нет. Но есть best practices и они появились не с пустого места. А насчёт аббревиатур — это удобный способ донести мысль другому человеку по-быстрому.
Везде свои компромиссы. Хотите хорошую систему, с которой приятно работать и удобно вносить изменения — делегируйте; проект небольшой — колбасьте код как угодно.
Да и про какие сотни строк вы говорите? Если у вас есть класс, который реализует так много интерфейсов, то проблема возникла раньше. Опять же из-за не следования хорошим практикам.
Об этом и речь — зачем усложнять и думать 10-100-1000 раз, если можно сделать просто?
Интерфейсы это ваш контракт. Это ваш api, если хотите. И если контракт меняется, значит на то была причина — изменилось требуемое поведение.
В том и проблема, что оно может будет работать. А может не будет. А может будет работать не так, как надо. А может появиться новое поведение, которое не ожидалось. В любом случае, чтобы быть уверенным придётся проверить всех наследников. С композицией надо проверить только там, где изменилось.
Согласен, default interface implementation неоднозначная фича. Пока что, я вижу ей применение для добавление утилитарного поведения, вроде того же observer'a.
Кстати, насчёт глубокой цепочки наследования в GUI — тот же реакт построен на High Order Components и там этот подход весьма органичен.
Лениться делегировать — это экономия на спичках. Больше огребётесь от огромного количества наследников.
Наследование это не способ писать меньше кода. Это способ выразить отношение is-a (ну и ещё способ сделать discriminated union, в языках где его нет). Более того, я считаю что наследование как раз более вредно — чем чаще вы наследуете, тем более костной становится ваша система.
Примеры:
У вас есть базовый класс, который часто наследуется в вашей системе. Затем, только некоторому числу наследников понадобилось новое поведение — вы меняете базовый класс, но вместе с этим вы также меняете и контракт тех классов, которым это поведение не нужно. Как итог, вам нужно протестировать те компоненты, которые даже не менялись — нарушение OCP. А ещё частенько ведёт и к нарушению LSP.
В укор первому примеру, вы можете сказать — "да я щас наделаю много мелких классов (например, VisibleAndSolid, VisibleAndMovable, VisibleAndSolidAndMovable) с точечным поведением и буду множественно наследовать от них". Ок, но чего вы этим сэкономите? Количество LOC будет примерно сравнимым при композиции. Только в этот раз вы усложнили систему, наделав в ней кучу ненужных сущностей.
Имея некоторый базовый класс, вы делаете вид, что знаете как он будет использоваться. Нарушение инкапсуляции здесь ещё грубее — каждый разработчик должен знать детали реализации в базовом классе (иначе опять можно нарушить LSP).
Да и вообще сто раз это исписанно.
ps. В java (и скоро в c#) ведь есть partial interface implementation — пользуйтесь.
"Бойлерплейтить" — это добавить один метод MainAsync и дождаться его завершения в Main?
AutoMoq AutoFixture
А вообще, мне кажется, проблемы у вас поглубже
Не пробовали в CoreFx закомитить переключалку способа аутентификации?
Но ведь это https://github.com/mobxjs/mobx-state-tree
Не пробовали https://habrahabr.ru/post/319996/ ?
https://martinfowler.com/bliki/ContextualValidation.html
https://lostechies.com/jimmybogard/2009/02/15/validation-in-a-ddd-world/
Главный посыл таков — "Instead of answering the question, “is this object valid”, try and answer the question, “Can this operation be performed?”.
Например, мы можем сохранить некоторый заказ без адреса в базу, но выполнить операцию отправки нет.
Есть такой подход в ddd, как ContextValidation. Думаю, его лучше юзать
С guid'ами проще, например, мержить две таблицы в одну либо данные с двух разных бд. Был реальный кейс объединения двух клиентских баз
BTW у автомаппера также есть возможность не писать явный маппинг
С двумя интерфейсами проще вешать декоратор/сегрегировать пайплан в том же медиатре.
Например, запускать транзакцию только для комманд. Можно пойти еще дальше и добавить отдельный маркерный интерфейс/атрибут для комманд, требующих транзакцию с параметром уровня изоляции.
Есть несколько сервисов типа LdapServise/AuthService, но всякие бизнес и crud операции напрямую в обработчике.
Вот кстати еще вопрос — а кошерно ли использовать CommandHandler'ы внутри CommandHandler'ов?
Джимми Боггард пишет, что нет и лучше юзать композицию/реализовывать несколько commandHandler'ов в одном классе
По поводу только одного интерфейса- достаточно завести свой собственный интерфейс и унаследовать от IRequest
Что-то вроде
interface IQuery: IRequestКовариацию не забудьте только, мне с телефона неудобно редактировать
У медиатра ваш прием с ResultHandler'ом не пройдет, поскольку вы меняете тип возвращаемого значения.
Но всякие валидации, транзакции и прочие шаблоны очень здорово залетают в pipeline behavior
Mediatr очень хорошо подойдет. Сделали на нем уже два проекта.
Посмотрите мои ссылки выше и ниже, там статьи от создателя медиатра с хорошей теорией и практикой.
Спасибо за статью!
У меня нубский вопрос — а как быть с загрузкой данных во write части? Городить ещё репозиториев для CommandHandler'ов?
Сейчас я в обработчиках использую EF контекст напрямую и при тестировании подменяю хранение на InMemoryStorage (у нас ef core) и вроде всё хорошо, но как-то не по себе.
В общем-то, пример можно посмотреть здесь у Jimmy Bogard'a