CodeFest 2019. Максим Аршинов (Хайтек Груп) — Быстрорастворимое проектирование

16b6c87229eaf58768d25ed7b2bbbf52?s=47 CodeFest
April 05, 2019

CodeFest 2019. Максим Аршинов (Хайтек Груп) — Быстрорастворимое проектирование

Люди учатся архитектуре по старым книжкам, которые писались для Java. Книжки хорошие, но дают решение задач того времени инструментами того времени. Время поменялось, C# уже больше похож на лайтовую Scala, чем Java, а новых хороших книжек мало. В этом докладе Максим расскажет о критериях хорошего кода и плохого кода, как и чем мерить. Сделает обзор типовых задач и подходов, разберет плюсы и минусы. В конце даст рекомендации и best practices по проектированию web-приложений.

16b6c87229eaf58768d25ed7b2bbbf52?s=128

CodeFest

April 05, 2019
Tweet

Transcript

  1. Быстрорастворимое проектирование Типовые решения для web-приложений

  2. max@hightech.group https://habr.com/users/marshinov 2 Максим Аршинов предприниматель, блогер, преподаватель

  3. О чем этот доклад 3

  4. О чем этот доклад • Критерии 4

  5. О чем этот доклад • Критерии • Краткая история развития

    архитектурной мысли 5
  6. О чем этот доклад • Критерии • Краткая история развития

    архитектурной мысли • Обзор недостатков классической слоеной архитектуры 6
  7. О чем этот доклад • Критерии • Краткая история развития

    архитектурной мысли • Обзор недостатков классической слоеной архитектуры • Решение 7
  8. О чем этот доклад • Критерии • Краткая история развития

    архитектурной мысли • Обзор недостатков классической слоеной архитектуры • Решение • Пошаговый разбор реализации без погружения в детали 8
  9. О чем этот доклад • Критерии • Краткая история развития

    архитектурной мысли • Обзор недостатков классической слоеной архитектуры • Решение • Пошаговый разбор реализации без погружения в детали • Все вместе 9
  10. Критерии

  11. None
  12. None
  13. 13

  14. Критерии • Количество фич в единицу времени 14

  15. Критерии • Количество фич в единицу времени • Количество возвратов

    на этапе код-ревью 15
  16. Критерии • Количество фич в единицу времени • Количество возвратов

    на этапе код-ревью • Количество возвратов на этапах тестирования и сдачи-приемки 16
  17. Критерии • Количество фич в единицу времени • Количество возвратов

    на этапе код-ревью • Количество возвратов на этапах тестирования и сдачи-приемки • Регрессия и количество багов, дошедших до продакшна 17
  18. Критерии • Количество фич в единицу времени • Количество возвратов

    на этапе код-ревью • Количество возвратов на этапах тестирования и сдачи-приемки • Регрессия и количество багов, дошедших до продакшна 18 Последний критерий сложно измерить
  19. Развитие архитектуры 19

  20. 20

  21. 21

  22. Традиционная слоеная архитектура 22 UI Business Logic Data Access

  23. Традиционная слоеная архитектура 23 UI Business Logic Data Access

  24. Традиционная слоеная архитектура 24 UI Business Logic Data Access

  25. Традиционная слоеная архитектура 25 UI Business Logic Data Access

  26. Луковая 26 Инверсия зависимостей

  27. Слоеная VS Луковая 27 UI Business Logic Data Access

  28. Слоеная VS Луковая 28 UI Business Logic Data Access UI

    Domain Data Access
  29. 29 •Луковая •Гексагональная •Порты и адаптеры UI Domain Services

  30. Простой пример Обновление email

  31. 31 Request Validation Update DB Record Send Verification Email Response

  32. 32 Request Validation Update DB Record Send Verification Email Response

  33. 33 Request Validation Update DB Record Send Verification Email Response

  34. 34 Request Validation Update DB Record Send Verification Email Response

  35. 35 Request Validation Update DB Record Send Verification Email Response

  36. 36 Request Validation Update DB Record Send Verification Email Response

  37. 37 public IActionResult UpdateEmail(int id, string email) { if (!ModelState.IsValid)

    return BadRequest(); var previousEmail = user.Email; var user = _dbContext.Set<User>() .FirstOrDefault(x => x.Id == id); user.Email = email; _dbContext.SaveChanges(); _emailSender.SendEmail(email, "Ваш email изменен", $"c {previousEmail} на {email}"); return Ok(); }
  38. 38 public IActionResult UpdateEmail(int id, string email) { if (!ModelState.IsValid)

    return BadRequest(); var previousEmail = user.Email; var user = _dbContext.Set<User>() .FirstOrDefault(x => x.Id == id); user.Email = email; _dbContext.SaveChanges(); _emailSender.SendEmail(email, "Ваш email изменен", $"c {previousEmail} на {email}"); return Ok(); } Validate
  39. 39 public IActionResult UpdateEmail(int id, string email) { if (!ModelState.IsValid)

    return BadRequest(); var previousEmail = user.Email; var user = _dbContext.Set<User>() .FirstOrDefault(x => x.Id == id); user.Email = email; _dbContext.SaveChanges(); _emailSender.SendEmail(email, "Ваш email изменен", $"c {previousEmail} на {email}"); return Ok(); } Update DB record
  40. 40 public IActionResult UpdateEmail(int id, string email) { if (!ModelState.IsValid)

    return BadRequest(); var previousEmail = user.Email; var user = _dbContext.Set<User>() .FirstOrDefault(x => x.Id == id); user.Email = email; _dbContext.SaveChanges(); _emailSender.SendEmail(email, "Ваш email изменен", $"c {previousEmail} на {email}"); return Ok(); } Send verification email
  41. 41 public IActionResult UpdateEmail(int id, string email) { if (!ModelState.IsValid)

    return BadRequest(); var previousEmail = user.Email; var user = _dbContext.Set<User>() .FirstOrDefault(x => x.Id == id); user.Email = email; _dbContext.SaveChanges(); _emailSender.SendEmail(email, "Ваш email изменен", $"c {previousEmail} на {email}"); return Ok(); } Return response
  42. 42 [Authorize] public IActionResult UpdateEmailRealLife(int id, [EmailAddress, Required] string email)

    { try { string previousEmail = null; using (MiniProfiler.Current.Step("Update email")) { if (!ModelState.IsValid) return BadRequest(); email = email.ToLower(); var user = _dbContext.Set<User>().FirstOrDefault(x => x.Id == id); if (user == null) { return NotFound(); } if (user.Id != id) { _logger.LogWarning($"Попытка изменить чужие данные {user.Id}/{id}"); return Forbid(); } previousEmail = user.Email; user.Email = email; user.LastUpdated = DateTime.UtcNow; _dbContext.SaveChanges(); } _emailSender.SendEmail(email, "Ваш email изменен", $"c {previousEmail} на {email}"); } catch (DbUpdateException e) { _logger.LogCritical(e.Message); return new ObjectResult(new { message = "Произошла ошибка при обновлении данных в БД" }) { StatusCode = 500 }; } catch (SmtpException e) { _logger.LogError(e.Message); } return Ok(); } Суровая реальность ☹
  43. 43 [Authorize] public IActionResult UpdateEmailRealLife(int id, [EmailAddress, Required] string email)

    { try { string previousEmail = null; using (MiniProfiler.Current.Step("Update email")) { if (!ModelState.IsValid) return BadRequest(); email = email.ToLower(); var user = _dbContext.Set<User>().FirstOrDefault(x => x.Id == id); if (user == null) { return NotFound(); } if (user.Id != id) { _logger.LogWarning($"Попытка изменить чужие данные {user.Id}/{id}"); return Forbid(); } previousEmail = user.Email; user.Email = email; user.LastUpdated = DateTime.UtcNow; _dbContext.SaveChanges(); } _emailSender.SendEmail(email, "Ваш email изменен", $"c {previousEmail} на {email}"); } catch (DbUpdateException e) { _logger.LogCritical(e.Message); return new ObjectResult(new { message = "Произошла ошибка при обновлении данных в БД" }) { StatusCode = 500 }; } catch (SmtpException e) { _logger.LogError(e.Message); } return Ok(); } •Авторизация •Обработка ошибок •Форматирование •Профилирование •Аудит-лог •Логирование
  44. Точно, мы же забыли сервисы и репозитории! Сейчас добавим и

    все встанет на свои места
  45. 45 public IActionResult UpdateEmailWithService(int id, string email) { try {

    if (!ModelState.IsValid) return BadRequest(); _userService.UpdateEmail(id, email); var previousEmail = _dbContext .Set<User>().Where(x => x.Id == id) .Select(x => x.Email).First(); _emailSender.SendEmail(email, "Ваш email изменен", $"c {previousEmail} на {email}"); } //... }
  46. 46 public IActionResult UpdateEmailWithService(int id, string email) { try {

    if (!ModelState.IsValid) return BadRequest(); _userService.UpdateEmail(id, email); var previousEmail = _dbContext .Set<User>().Where(x => x.Id == id) .Select(x => x.Email).First(); _emailSender.SendEmail(email, "Ваш email изменен", $"c {previousEmail} на {email}"); } //... } • Стало лучше?
  47. 47 public IActionResult UpdateEmailWithService(int id, string email) { try {

    if (!ModelState.IsValid) return BadRequest(); _userService.UpdateEmail(id, email); var previousEmail = _dbContext .Set<User>().Where(x => x.Id == id) .Select(x => x.Email).First(); _emailSender.SendEmailAsync(email, "Ваш email изменен", $"c {previousEmail} на {email}"); } //... } • Стало лучше? • Метод UpdateEmail можно использовать повторно
  48. 48 public IActionResult UpdateEmailWithService(int id, string email) { try {

    if (!ModelState.IsValid) return BadRequest(); _userService.UpdateEmail(id, email); var previousEmail = _dbContext .Set<User>().Where(x => x.Id == id) .Select(x => x.Email).First(); _emailSender.SendEmailAsync(email, "Ваш email изменен", $"c {previousEmail} на {email}"); } //... } • Стало лучше? • Метод UpdateEmail можно использовать повторно • Заглянем внутрь
  49. 49 public void UpdateEmail(int id, string email) { try {

    using (MiniProfiler.Current.Step("Update email")) { email = email.ToLower(); var user = _userRepository.ById(id); if (user.Id != id) { var message = $"Попытка изменить чужие данные {user.Id}/{id}"; _logger.LogWarning(message); throw new SecurityException(message); } user.Email = email; _dbContext.SaveChanges(); } } catch (DbUpdateException e) { _logger.LogCritical(e.Message); throw; } }
  50. 50 public void UpdateEmail(int id, string email) { try {

    using (MiniProfiler.Current.Step("Update email")) { email = email.ToLower(); var user = _userRepository.ById(id); if (user.Id != id) { var message = $"Попытка изменить чужие данные {user.Id}/{id}"; _logger.LogWarning(message); throw new SecurityException(message); } user.Email = email; _dbContext.SaveChanges(); } } catch (DbUpdateException e) { _logger.LogCritical(e.Message); throw; } }
  51. 51 public void UpdateEmail(int id, string email) { try {

    using (MiniProfiler.Current.Step("Update email")) { email = email.ToLower(); var user = _userRepository.ById(id); if (user.Id != id) { var message = $"Попытка изменить чужие данные {user.Id}/{id}"; _logger.LogWarning(message); throw new SecurityException(message); } user.Email = email; _dbContext.SaveChanges(); } } catch (DbUpdateException e) { _logger.LogCritical(e.Message); throw; } } • Валидация
  52. 52 public void UpdateEmail(int id, string email) { try {

    using (MiniProfiler.Current.Step("Update email")) { email = email.ToLower(); var user = _userRepository.ById(id); if (user.Id != id) { var message = $"Попытка изменить чужие данные {user.Id}/{id}"; _logger.LogWarning(message); throw new SecurityException(message); } user.Email = email; _dbContext.SaveChanges(); } } catch (DbUpdateException e) { _logger.LogCritical(e.Message); throw; } } • Валидация • Обработка ошибок
  53. 53 public void UpdateEmail(int id, string email) { try {

    using (MiniProfiler.Current.Step("Update email")) { email = email.ToLower(); var user = _userRepository.ById(id); if (user.Id != id) { var message = $"Попытка изменить чужие данные {user.Id}/{id}"; _logger.LogWarning(message); throw new SecurityException(message); } user.Email = email; _dbContext.SaveChanges(); } } catch (DbUpdateException e) { _logger.LogCritical(e.Message); throw; } } • Валидация • Обработка ошибок • Здесь или в контроллере?
  54. 54 public void UpdateEmail(int id, string email) { try {

    using (MiniProfiler.Current.Step("Update email")) { email = email.ToLower(); var user = _userRepository.ById(id); if (user.Id != id) { var message = $"Попытка изменить чужие данные {user.Id}/{id}"; _logger.LogWarning(message); throw new SecurityException(message); } user.Email = email; _dbContext.SaveChanges(); } } catch (DbUpdateException e) { _logger.LogCritical(e.Message); throw; } } • Валидация • Обработка ошибок • Здесь или в контроллере? • Стоит ли вызывать SaveChanges?
  55. Что если я скажу тебе… слои не делают код лучше

  56. 56

  57. 57

  58. Альтернатива 58 Варианты использования вместо слоев

  59. 59 Handler Вариант использования

  60. 60 public interface IHandler<in TIn, out TOut> { TOut Handle(TIn

    input); } Handler
  61. 61 public interface IHandler<in TIn, out TOut> { TOut Handle(TIn

    input); } Handler
  62. 62 public interface IHandler<in TIn, out TOut> { TOut Handle(TIn

    input); } Handler
  63. 63 public interface IHandler<in TIn, out TOut> { TOut Handle(TIn

    input); } Handler
  64. 64 Use Case Dto Dto Handler

  65. 65 Domain Model Dto Dto Handler

  66. 66 Read Model Dto Dto Handler

  67. 67 Dapper Dto Dto Handler

  68. 68 Elastic Search Dto Dto Handler

  69. 69 Stored Procedure Dto Dto Handler

  70. 70 Http Client Dto Dto Handler

  71. 71 Whatever… Dto Dto Handler

  72. Слоев нет

  73. 73 void UpdateEmail (int id) User GetUser (int id) void

    BanUser (int id) UserService
  74. 74 void UpdateEmail (int id) void BanUser (int id) UserService

    class GetUser
  75. 75 void BanUser (int id) UserService class UpdateEmail class GetUser

  76. 76 UserService class UpdateEmail class GetUser class BanUser

  77. 77 UserService class UpdateEmail class GetUser class BanUser

  78. 78 class UpdateEmail class GetUser class BanUser

  79. 79 class UpdateEmail class GetUser class BanUser Отлично, сервисов нет…

    Что дальше?
  80. 80 class UpdateEmail class GetUser class BanUser Возвращает данные

  81. 81 class UpdateEmail class GetUser class BanUser Возвращают результат операции

    и изменяют состояние
  82. GET 82 • Возвращает данные • Не меняет состояние сервера

    GET Browser Return Data Browser Browser Web Server Web Server
  83. POST, PUT, DELETE 83 • Возвращает результат операции • Меняет

    состояние сервера Web Server POST Browser Web Server Browser Return Result
  84. CQRS + HTTP = ♥

  85. CQRS + HTTP = ♥ Не пиши где читаешь

  86. 86 public interface IQueryHandler<in TIn, out TOut> : IHandler<TIn, TOut>

    where TIn: IQuery<TOut> QueryHandler
  87. 87 public interface ICommandHandler<in TIn, out TOut> : IHandler<TIn, TOut>

    where TIn: ICommand<TOut> CommandHandler
  88. 88 public interface ICommandHandler<in TIn, out TOut> : IHandler<TIn, TOut>

    where TIn: ICommand<TOut> CommandHandler Это пригодится чуть позже
  89. Реализация Handler

  90. 90 public void Handle(UpdateEmail command) { try { using (MiniProfiler.Current.Step("Update

    email")) { command.Email = command.Email.ToLower(); var user = _userRepository.ById(command.Id); if (user.Id != command.Id) { var message = $"Попытка изменить чужие данные {user.Id}/{command.Id}"; _logger.LogWarning(message); throw new SecurityException(message); } user.Email = command.Email; _dbContext.SaveChanges(); } } catch (DbUpdateException e) { _logger.LogCritical(e.Message); throw; } }
  91. 91 public void Handle(UpdateEmail command) { try { using (MiniProfiler.Current.Step("Update

    email")) { command.Email = command.Email.ToLower(); var user = _userRepository.ById(command.Id); if (user.Id != command.Id) { var message = $"Попытка изменить чужие данные {user.Id}/{command.Id}"; _logger.LogWarning(message); throw new SecurityException(message); } user.Email = command.Email; _dbContext.SaveChanges(); } } catch (DbUpdateException e) { _logger.LogCritical(e.Message); throw; } }
  92. Декораторы Спешат на помощь

  93. 93 Dto Dto Handler Decorator Decorator Decorator

  94. 94 public abstract class HandlerDecoratorBase<TIn, TOut>: IHandler<TIn, TOut> { protected

    readonly IHandler<TIn, TOut> Decorated; protected HandlerDecoratorBase(IHandler<TIn, TOut> decorated) { Decorated = decorated; } public abstract TOut Handle(TIn input); } Dto Dto Handler Decorator Decorator Decorator
  95. Pipeline Command Response Validation CommandHandler Security Domain Events []

  96. Validation Command Response Validation CommandHandler Security Domain Events []

  97. public override TOut Handle(TIn input) { IEnumerable<ValidationResult> res = _validators.Validate(input);

    if (!res.IsValid()) { if (typeof(TOut) == typeof(IEnumerable<ValidationResult>)) return (TOut) res; throw new ValidationException(message); } return Decorated.Handle(input); } 97 Command Response Validation CommandHandler Security Domain Events []
  98. public override TOut Handle(TIn input) { IEnumerable<ValidationResult> res = _validators.Validate(input);

    if (!res.IsValid()) { if (typeof(TOut) == typeof(IEnumerable<ValidationResult>)) return (TOut) res; throw new ValidationException(message); } return Decorated.Handle(input); } 98 Command Response Validation CommandHandler Security Domain Events []
  99. public override TOut Handle(TIn input) { IEnumerable<ValidationResult> res = _validators.Validate(input);

    if (!res.IsValid()) { if (typeof(TOut) == typeof(IEnumerable<ValidationResult>)) return (TOut) res; throw new ValidationException(message); } return Decorated.Handle(input); } 99 Command Response Validation CommandHandler Security Domain Events []
  100. public override TOut Handle(TIn input) { IEnumerable<ValidationResult> res = _validators.Validate(input);

    if (!res.IsValid()) { if (typeof(TOut) == typeof(IEnumerable<ValidationResult>)) return (TOut) res; throw new ValidationException(message); } return Decorated.Handle(input); } 100 Command Response Validation CommandHandler Security Domain Events []
  101. Security Command Response Validation CommandHandler Security Domain Events []

  102. 102 102 Command Response Validation CommandHandler Security Domain Events []

    public override TOut Handle(TIn input) { if (!CheckPermissions(input)) { throw new SecurityException(); } return Decorated.Handle(input); }
  103. 103 103 Command Response Validation CommandHandler Security Domain Events []

    public override TOut Handle(TIn input) { if (!CheckPermissions(input)) { throw new SecurityException(); } return Decorated.Handle(input); }
  104. 104 104 Command Response Validation CommandHandler Security Domain Events []

    public override TOut Handle(TIn input) { if (!CheckPermissions(input)) { throw new SecurityException(); } return Decorated.Handle(input); }
  105. Domain Command Response Validation CommandHandler Security Domain Events []

  106. Одержимость примитивами

  107. 107 public class UpdateUserEmail: IValidatableCommand { public int UserId {

    get; set; } public string Email { get; set; } }
  108. 108 public class UpdateUserEmail: IValidatableCommand { [Required, Id] public int

    UserId { get; set; } [Required, EmailAddress] public string Email { get; set; } }
  109. 109 public class UpdateUserEmail: IValidatableCommand { [Required, Id] public int

    UserId { get; set; } [Required, EmailAddress] public string Email { get; set; } } • Как быть с проверкой значений в БД?
  110. 110 public class UpdateUserEmail: IValidatableCommand { [Required, Id] public int

    UserId { get; set; } [Required, EmailAddress] public string Email { get; set; } } • Как быть с проверкой значений в БД? • Добавить типы!
  111. 111 public class Id<T>: Id<int, T> where T : class,

    IHasId<int> { public Id(T entity) : base(entity) {} public Id(int value, Func<int, T> loader) : base(value, loader) {} }
  112. 112 public class Id<T>: Id<int, T> where T : class,

    IHasId<int> { public Id(T entity) : base(entity) {} public Id(int value, Func<int, T> loader) : base(value, loader) {} }
  113. 113 public class Id<T>: Id<int, T> where T : class,

    IHasId<int> { public Id(T entity) : base(entity) {} public Id(int value, Func<int, T> loader) : base(value, loader) {} }
  114. 114 public class Id<T>: Id<int, T> where T : class,

    IHasId<int> { public Id(T entity) : base(entity) {} public Id(int value, Func<int, T> loader) : base(value, loader) {} }
  115. 115 public class Id<T>: Id<int, T> where T : class,

    IHasId<int> { public Id(T entity) : base(entity) {} public Id(int value, Func<int, T> loader) : base(value, loader) {} }
  116. 116 [JsonConverter(typeof(ValueTypeConverter))] [ModelBinder(typeof(EmailModelBinder))] public class Email: StringValueObject { private static

    readonly EmailAddressAttribute Attr = new EmailAddressAttribute(); public Email(string email): base(email?.ToLowerInvariant()) { if (!Attr.IsValid(email)) { throw new ArgumentException(nameof(email), $"\"{email}\" is not valid email"); } } }
  117. 117 [JsonConverter(typeof(ValueTypeConverter))] [ModelBinder(typeof(EmailModelBinder))] public class Email: StringValueObject { private static

    readonly EmailAddressAttribute Attr = new EmailAddressAttribute(); public Email(string email): base(email?.ToLowerInvariant()) { if (!Attr.IsValid(email)) { throw new ArgumentException(nameof(email), $"\"{email}\" is not valid email"); } } }
  118. 118 [JsonConverter(typeof(ValueTypeConverter))] [ModelBinder(typeof(EmailModelBinder))] public class Email: StringValueObject { private static

    readonly EmailAddressAttribute Attr = new EmailAddressAttribute(); public Email(string email): base(email?.ToLowerInvariant()) { if (!Attr.IsValid(email)) { throw new ArgumentException(nameof(email), $"\"{email}\" is not valid email"); } } }
  119. 119 [JsonConverter(typeof(ValueTypeConverter))] [ModelBinder(typeof(EmailModelBinder))] public class Email: StringValueObject { private static

    readonly EmailAddressAttribute Attr = new EmailAddressAttribute(); public Email(string email): base(email?.ToLowerInvariant()) { if (!Attr.IsValid(email)) { throw new ArgumentException(nameof(email), $"\"{email}\" is not valid email"); } } }
  120. 120 [JsonConverter(typeof(ValueTypeConverter))] [ModelBinder(typeof(EmailModelBinder))] public class Email: StringValueObject { private static

    readonly EmailAddressAttribute Attr = new EmailAddressAttribute(); public Email(string email): base(email?.ToLowerInvariant()) { if (!Attr.IsValid(email)) { throw new ArgumentException(nameof(email), $"\"{email}\" is not valid email"); } } }
  121. 121 public class UpdateUserEmail: IValidatableCommand { [Required] public Id<User> UserId

    { get; set; } [Required] public Email Email { get; set; } }
  122. 122 public class UpdateUserEmail: IValidatableCommand { [Required] public Id<User> UserId

    { get; set; } [Required] public Email Email { get; set; } } Эти значения точно корректны
  123. Инварианты

  124. 124 public class User: EntityBase, IHasDomainEvents { protected User() {}

    public User(Email email, string firstName, string lastName) { Created = DateTime.UtcNow; Email = email ?? throw new ArgumentNullException(nameof(email)); ChangeName(firstName, lastName); } }
  125. 125 public class User: EntityBase, IHasDomainEvents { protected User() {}

    public User(Email email, string firstName, string lastName) { Created = DateTime.UtcNow; Email = email ?? throw new ArgumentNullException(nameof(email)); ChangeName(firstName, lastName); } } • Ждем nullable reference type в C#8
  126. 126 public class User: EntityBase, IHasDomainEvents { protected User() {}

    public User(Email email, string firstName, string lastName) { Created = DateTime.UtcNow; Email = email ?? throw new ArgumentNullException(nameof(email)); ChangeName(firstName, lastName); } } • Ждем nullable reference type в C#8 • Имя и фамилию меняем вместе
  127. 127 private static DefaultStringLengthAttribute _attr = new DefaultStringLengthAttribute(); public void

    ChangeName(string firstName, string lastName) { if (!_defaultStringLength.IsValid(firstName)) throw new ArgumentException(_ attr.ErrorMessage, nameof(firstName)); if (!_defaultStringLength.IsValid(lastName)) throw new ArgumentException(_ attr.ErrorMessage, nameof(lastName)); FirstName = firstName; LastName = lastName; }
  128. 128 private static DefaultStringLengthAttribute _attr = new DefaultStringLengthAttribute(); public void

    ChangeName(string firstName, string lastName) { if (!_defaultStringLength.IsValid(firstName)) throw new ArgumentException(_ attr.ErrorMessage, nameof(firstName)); if (!_defaultStringLength.IsValid(lastName)) throw new ArgumentException(_ attr.ErrorMessage, nameof(lastName)); FirstName = firstName; LastName = lastName; }
  129. 129 private static DefaultStringLengthAttribute _attr = new DefaultStringLengthAttribute(); public void

    ChangeName(string firstName, string lastName) { if (!_defaultStringLength.IsValid(firstName)) throw new ArgumentException(_ attr.ErrorMessage, nameof(firstName)); if (!_defaultStringLength.IsValid(lastName)) throw new ArgumentException(_ attr.ErrorMessage, nameof(lastName)); FirstName = firstName; LastName = lastName; }
  130. 130 private static DefaultStringLengthAttribute _attr = new DefaultStringLengthAttribute(); public void

    ChangeName(string firstName, string lastName) { if (!_defaultStringLength.IsValid(firstName)) throw new ArgumentException(_ attr.ErrorMessage, nameof(firstName)); if (!_defaultStringLength.IsValid(lastName)) throw new ArgumentException(_ attr.ErrorMessage, nameof(lastName)); FirstName = firstName; LastName = lastName; } Можно повторно использовать в DTO
  131. 131 public class UpdateUserInfo: IValidatableCommand { [Required, DefaultStringLength] public string

    FirstName { get; set;} [Required, DefaultStringLength] public string LastName { get; set; } }
  132. 132 public class UpdateUserInfo: IValidatableCommand { [Required, DefaultStringLength] public string

    FirstName { get; set;} [Required, DefaultStringLength] public string LastName { get; set; } } А как же конструктор?
  133. 133 public class UpdateUserInfo: IValidatableCommand { [Required, DefaultStringLength] public string

    FirstName { get; set;} [Required, DefaultStringLength] public string LastName { get; set; } } На границах программы не объектно-ориентированы А как же конструктор?
  134. 134 Clean Dirty Dirty Bounded Context

  135. Clean Dirty Dirty Bounded Context 135

  136. 136 Clean Dirty Dirty Bounded Context

  137. DDD-чеклист • Конструктор со всеми параметрами для инициализации сущности (но

    не DTO) public или internal • Приватные setter’ы, специализинованные методы для изменения состояния 137
  138. DDD-чеклист • Конструктор со всеми параметрами для инициализации сущности (но

    не DTO) public или internal • Приватные setter’ы, специализинованные методы для изменения состояния • Система типов (Value Objects) > валидации • TryParse для создания Value Objects 138
  139. DDD-чеклист • Конструктор со всеми параметрами для инициализации сущности (но

    не DTO) public или internal • Приватные setter’ы, специализинованные методы для изменения состояния • Система типов (Value Objects) > валидации • TryParse для создания Value Objects • Работа с агрегатами только через корень, не создаем агрегаты, где их нет • Для чтения возвращаем сразу DTO 139
  140. Все еще есть вопросы про реализацию DDD? Сообщите в отзыве,

    что хотите, чтобы я подготовил новый доклад в следующем году
  141. Handler

  142. 142 public void Handle(UpdateUserEmail command) { var user = command.UserId.Entity;

    user.Email = command.Email; }
  143. 143 public void Handle(UpdateUserEmail command) { var user = command.UserId.Entity;

    user.Email = command.Email; } • Данные корректны, email не занят
  144. 144 public void Handle(UpdateUserEmail command) { var user = command.UserId.Entity;

    user.Email = command.Email; } • Данные корректны, email не занят • Такой пользователь есть и мы имеем право менять ему email
  145. 145 public void Handle(UpdateUserEmail command) { var user = command.UserId.Entity;

    user.Email = command.Email; } • Данные корректны, email не занят • Такой пользователь есть и мы имеем право менять ему email • Где SaveChanges(), нотификации, логи и профайлер?
  146. Events Command Response Validation CommandHandler Security Domain Events []

  147. 147 DomainEvents.Raise (new Event())

  148. 148 DomainEvents.Raise (new Event()) У меня есть кое-что получше!

  149. 149 public Email Email { get => _email; set {

    if (value != _email) _domainEventStore.Raise( new UserEmailChanged(this, _email, value)); _email = value; } }
  150. 150 public TOut Handle(TIn input) { var res = _decorated.Handle(input);

    _dbContext.ChangeTracker .Entries() .Where(x => x is IHasDomainEvents) .SelectMany(x => ((IHasDomainEvents)x) .GetDomainEvents()) .ToList() .ForEach(_dispatcher.Handle); _dbContext.SaveChanges(); return res; }
  151. 151 public TOut Handle(TIn input) { var res = _decorated.Handle(input);

    _dbContext.ChangeTracker .Entries() .Where(x => x is IHasDomainEvents) .SelectMany(x => ((IHasDomainEvents)x) .GetDomainEvents()) .ToList() .ForEach(_dispatcher.Handle); _dbContext.SaveChanges(); return res; }
  152. 152 public TOut Handle(TIn input) { var res = _decorated.Handle(input);

    _dbContext.ChangeTracker .Entries() .Where(x => x is IHasDomainEvents) .SelectMany(x => ((IHasDomainEvents)x) .GetDomainEvents()) .ToList() .ForEach(_dispatcher.Handle); _dbContext.SaveChanges(); return res; }
  153. 153 public TOut Handle(TIn input) { var res = _decorated.Handle(input);

    _dbContext.ChangeTracker .Entries() .Where(x => x is IHasDomainEvents) .SelectMany(x => ((IHasDomainEvents)x) .GetDomainEvents()) .ToList() .ForEach(_dispatcher.Handle); _dbContext.SaveChanges(); return res; }
  154. 154 public TOut Handle(TIn input) { var res = _decorated.Handle(input);

    _dbContext.ChangeTracker .Entries() .Where(x => x is IHasDomainEvents) .SelectMany(x => ((IHasDomainEvents)x) .GetDomainEvents()) .ToList() .ForEach(_dispatcher.Handle); _dbContext.SaveChanges(); return res; } • Почему просто не переопределить метод SaveChanges()?
  155. 155 public TOut Handle(TIn input) { var res = _decorated.Handle(input);

    _dbContext.ChangeTracker .Entries() .Where(x => x is IHasDomainEvents) .SelectMany(x => ((IHasDomainEvents)x) .GetDomainEvents()) .ToList() .ForEach(_dispatcher.Handle); _dbContext.SaveChanges(); return res; } • Почему просто не переопределить метод SaveChanges()? • Чтобы избежать циклических зависимостей
  156. Логирование и профилирование

  157. 157 public override TOut Handle(TIn input) { var output =

    Decorated.Handle(input); _logger.LogInformation( $"{_decoratedType}: {input} => {output}"); return output; } public override TOut Handle(TIn input) { using (MiniProfiler.Current.Step(_decoratedType.ToString())) { return Decorated.Handle(input); } }
  158. 158 public override TOut Handle(TIn input) { var output =

    Decorated.Handle(input); _logger.LogInformation( $"{_decoratedType}: {input} => {output}"); return output; } public override TOut Handle(TIn input) { using (MiniProfiler.Current.Step(_decoratedType.ToString())) { return Decorated.Handle(input); } }
  159. Response 159 Command Response Validation CommandHandler Security Domain Events []

  160. Мы кое-что забыли 160 Command Response Validation CommandHandler Security Domain

    Events [] Exception
  161. 161

  162. 162 public class Result<TSuccess, TFailure> { public Result(TSuccess success) {

    _success = success; _isSuccess = true; } public Result(TFailure failure) { _failure = failure; } }
  163. 163 public class Result<TSuccess, TFailure> { public Result(TSuccess success) {

    _success = success; _isSuccess = true; } public Result(TFailure failure) { _failure = failure; } }
  164. 164 public class Result<TSuccess, TFailure> { public Result(TSuccess success) {

    _success = success; _isSuccess = true; } public Result(TFailure failure) { _failure = failure; } }
  165. 165 public TResult Match<TResult>( Func<TSuccess, TResult> success, Func<TFailure, TResult> failure)

    => _isSuccess ? success(_success) : failure(_failure);
  166. 166 public TResult Match<TResult>( Func<TSuccess, TResult> success, Func<TFailure, TResult> failure)

    => _isSuccess ? success(_success) : failure(_failure);
  167. 167 public TResult Match<TResult>( Func<TSuccess, TResult> success, Func<TFailure, TResult> failure)

    => _isSuccess ? success(_success) : failure(_failure);
  168. 168 from r1 in result1 from r2 in result2 from

    r3 in result3 select r1 + r2 + r3;
  169. None
  170. Exception’ы – зло! Пишите непонятный код

  171. В C# нет discriminated unions и computation expressions, Карл!

  172. app.UseExceptionHandler(appError => { appError.Run(async context => { //… }); });

  173. • Валидация – 422 • Аутентификация - 401 • Авторизация

    – 403 • Сервер – 500 • IHasUserMessage
  174. Query Pipeline Query Response Validation Query Security Cache Read Model

  175. Query Pipeline Query Response Validation Query Security Cache Read Model

  176. Query Pipeline Query Response Validation Query Security Cache Read Model

  177. Security Query Response Validation Query Security Cache Read Model

  178. 178 public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); } public

    IQueryable<T> WithFilters() => _permissionFilters.Aggregate( (IQueryable<T>) _dbContext.Set<T>(), (current, permissionFilter) => permissionFilter.GetPermitted(current)); 178 178 Query Response Validation Query Security Cache Read Model
  179. 179 public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); } public

    IQueryable<T> WithFilters() => _permissionFilters.Aggregate( (IQueryable<T>) _dbContext.Set<T>(), (current, permissionFilter) => permissionFilter.GetPermitted(current)); 179 179 Query Response Validation Query Security Cache Read Model
  180. 180 public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); } public

    IQueryable<T> WithFilters() => _permissionFilters.Aggregate( (IQueryable<T>) _dbContext.Set<T>(), (current, permissionFilter) => permissionFilter.GetPermitted(current)); 180 180 Query Response Validation Query Security Cache Read Model
  181. 181 public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); } public

    IQueryable<T> WithFilters() => _permissionFilters.Aggregate( (IQueryable<T>) _dbContext.Set<T>(), (current, permissionFilter) => permissionFilter.GetPermitted(current)); 181 181 • Почему не Global Query Filters?
  182. 182 public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); } public

    IQueryable<T> WithFilters() => _permissionFilters.Aggregate( (IQueryable<T>) _dbContext.Set<T>(), (current, permissionFilter) => permissionFilter.GetPermitted(current)); 182 182 Query Response Validation Query Security Cache Read Model • Почему не Global Query Filters? • Чтобы избежать циклических зависимостей
  183. Query Pipeline Query Response Validation Query Security Cache Read Model

  184. 184 public class LinqQueryHandler<TQuery, TEntity, TProjection> : IQueryHandler<TQuery, IEnumerable<TProjection>> where

    TQuery : IQuery<IEnumerable<TProjection>> , IFilter<TProjection> , ISorter<TProjection> where TEntity : class
  185. 185 public class LinqQueryHandler<TQuery, TEntity, TProjection> : IQueryHandler<TQuery, IEnumerable<TProjection>> where

    TQuery : IQuery<IEnumerable<TProjection>> , IFilter<TProjection> , ISorter<TProjection> where TEntity : class
  186. 186 public class LinqQueryHandler<TQuery, TEntity, TProjection> : IQueryHandler<TQuery, IEnumerable<TProjection>> where

    TQuery : IQuery<IEnumerable<TProjection>> , IFilter<TProjection> , ISorter<TProjection> where TEntity : class
  187. 187 public class LinqQueryHandler<TQuery, TEntity, TProjection> : IQueryHandler<TQuery, IEnumerable<TProjection>> where

    TQuery : IQuery<IEnumerable<TProjection>> , IFilter<TProjection> , ISorter<TProjection> where TEntity : class
  188. 188 public IEnumerable<TProjection> Handle(TQuery query) => _entities .TryFilter(query) .ProjectTo<TProjection>() .FilterAndSort(query)

    .ToList();
  189. 189 public IEnumerable<TProjection> Handle(TQuery query) => _entities .TryFilter(query) .ProjectTo<TProjection>() .FilterAndSort(query)

    .ToList();
  190. 190 public IEnumerable<TProjection> Handle(TQuery query) => _entities .TryFilter(query) .ProjectTo<TProjection>() .FilterAndSort(query)

    .ToList();
  191. 191 public IEnumerable<TProjection> Handle(TQuery query) => _entities .TryFilter(query) .ProjectTo<TProjection>() .FilterAndSort(query)

    .ToList();
  192. Не все выражения одинаково полезны транслируются в SQL

  193. 193 public class PostListDto : HasIdBase { public string Title

    { get; set; } [JsonIgnore] public DateTime Created { get; set; } [JsonIgnore] public DateTime? LastUpdated { get; set; } public string SubTitle => LastUpdated.HasValue ? $"{Created.ToString(DateTimeFormats.Default)} /" + {LastUpdated.Value.ToString(DateTimeFormats.Default)}" : Created.ToString(DateTimeFormats.Default); public static void CreateMap(IMappingExpression<Post, PostListDto> mappingExpression) => mappingExpression .ForMember(x => x.Title, o => o.MapFrom(x => $"{x.Hub.Name} / {x.Name}")); }
  194. 194 public class PostListDto : HasIdBase { public string Title

    { get; set; } [JsonIgnore] public DateTime Created { get; set; } [JsonIgnore] public DateTime? LastUpdated { get; set; } public string SubTitle => LastUpdated.HasValue ? $"{Created.ToString(DateTimeFormats.Default)} /" + {LastUpdated.Value.ToString(DateTimeFormats.Default)}" : Created.ToString(DateTimeFormats.Default); public static void CreateMap(IMappingExpression<Post, PostListDto> mappingExpression) => mappingExpression .ForMember(x => x.Title, o => o.MapFrom(x => $"{x.Hub.Name} / {x.Name}")); }
  195. 195 public class PostListDto : HasIdBase { public string Title

    { get; set; } [JsonIgnore] public DateTime Created { get; set; } [JsonIgnore] public DateTime? LastUpdated { get; set; } public string SubTitle => LastUpdated.HasValue ? $"{Created.ToString(DateTimeFormats.Default)} /" + {LastUpdated.Value.ToString(DateTimeFormats.Default)}" : Created.ToString(DateTimeFormats.Default); public static void CreateMap(IMappingExpression<Post, PostListDto> mappingExpression) => mappingExpression .ForMember(x => x.Title, o => o.MapFrom(x => $"{x.Hub.Name} / {x.Name}")); }
  196. 196 public class PostListDto : HasIdBase { public string Title

    { get; set; } [JsonIgnore] public DateTime Created { get; set; } [JsonIgnore] public DateTime? LastUpdated { get; set; } public string SubTitle => LastUpdated.HasValue ? $"{Created.ToString(DateTimeFormats.Default)} /" + {LastUpdated.Value.ToString(DateTimeFormats.Default)}" : Created.ToString(DateTimeFormats.Default); public static void CreateMap(IMappingExpression<Post, PostListDto> mappingExpression) => mappingExpression .ForMember(x => x.Title, o => o.MapFrom(x => $"{x.Hub.Name} / {x.Name}")); }
  197. 197 public class PostListDto : HasIdBase { public string Title

    { get; set; } [JsonIgnore] public DateTime Created { get; set; } [JsonIgnore] public DateTime? LastUpdated { get; set; } public string SubTitle => LastUpdated.HasValue ? $"{Created.ToString(DateTimeFormats.Default)} /" + {LastUpdated.Value.ToString(DateTimeFormats.Default)}" : Created.ToString(DateTimeFormats.Default); public static void CreateMap(IMappingExpression<Post, PostListDto> mappingExpression) => mappingExpression .ForMember(x => x.Title, o => o.MapFrom(x => $"{x.Hub.Name} / {x.Name}")); }
  198. 198 public class PostListDto : HasIdBase { public string Title

    { get; set; } [JsonIgnore] public DateTime Created { get; set; } [JsonIgnore] public DateTime? LastUpdated { get; set; } public string SubTitle => LastUpdated.HasValue ? $"{Created.ToString(DateTimeFormats.Default)} /" + {LastUpdated.Value.ToString(DateTimeFormats.Default)}" : Created.ToString(DateTimeFormats.Default); public static void CreateMap(IMappingExpression<Post, PostListDto> mappingExpression) => mappingExpression .ForMember(x => x.Title, o => o.MapFrom(x => $"{x.Hub.Name} / {x.Name}")); }
  199. 199 Query Response Validation Query Security Cache Read Model Command

    Response Validation CommandHandler Security Domain Events []
  200. 200 Query Response Validation Query Security Cache Read Model Command

    Response Validation CommandHandler Security Domain Events [] • Разные pipeline – разный набор декораторов
  201. 201 Query Response Validation Query Security Cache Read Model Command

    Response Validation CommandHandler Security Domain Events [] • Разные pipeline – разный набор декораторов • Декораторы можно оформить в виде отдельных библиотек
  202. Регистрация декораторов

  203. 203 var updateEmailHandler = new SaveChangesDecorator<UpdateUserEmail, Unit>( new ProfilerDecorator<UpdateUserEmail, Unit>(

    new LoggerDecorator<UpdateUserEmail, Unit>( new UpdateEmailHandler( dbContext.Set<User>()), logger)), dbContext, dispatcher);
  204. 204 var updateEmailHandler = new SaveChangesDecorator<UpdateUserEmail, Unit>( new ProfilerDecorator<UpdateUserEmail, Unit>(

    new LoggerDecorator<UpdateUserEmail, Unit>( new UpdateEmailHandler( dbContext.Set<User>()), logger)), dbContext, dispatcher); Не айс ☹
  205. 205 MediatR

  206. 206 Simple Injector MediatR

  207. 207 public interface ICommandHandler<in TIn, out TOut> : IHandler<TIn, TOut>

    where TIn: ICommand<TOut> CommandHandler • Помните этот слайд?
  208. 208 public interface ICommandHandler<in TIn, out TOut> : IHandler<TIn, TOut>

    where TIn: ICommand<TOut> CommandHandler • Помните этот слайд? • Simple Injector поддерживает ограничения типов в дженерика
  209. 209 public interface ICommandHandler<in TIn, out TOut> : IHandler<TIn, TOut>

    where TIn: ICommand<TOut> CommandHandler • Помните этот слайд? • Simple Injector поддерживает ограничения типов в дженерика • CommandHandler закрывает транзакцию, а Handler - нет
  210. None
  211. Декораторы???

  212. Декораторы??? Сложна!

  213. Декораторы??? Сложна! Непонятна!

  214. В ASP.NET Core же есть middleware!

  215. В ASP.NET Core же есть middleware! А в ASP.NET MVC

    есть ActionFilter!
  216. Организация по модулям, а не слоям

  217. 217

  218. 218

  219. 219 Фреймворк диктует структуру

  220. 220

  221. 221 Структура соответствует функциональности системы

  222. 222

  223. 223

  224. Преимущества • Код добавляется, а не редактируется • Меньше конфликтов

    в VCS 224
  225. Преимущества • Код добавляется, а не редактируется • Меньше конфликтов

    в VCS • Лучшее разделение на Bounded Context • Фичу можно удалить целиком, удалив папку 225
  226. Преимущества • Код добавляется, а не редактируется • Меньше конфликтов

    в VCS • Лучшее разделение на Bounded Context • Фичу можно удалить целиком, удалив папку • Упрощает работу численными методами • Упрощает коммуникацию 226
  227. Недостатки • Не работает из коробки • Нужно писать много

    инфраструктурного кода и переопределить стандартное поведение 227
  228. Все вместе • IHandler<TIn, TOut> - строительный блок • ICommandHandler,

    IQueryHandler – холистические абстракции 228
  229. Все вместе • IHandler<TIn, TOut> - строительный блок • ICommandHandler,

    IQueryHandler – холистические абстракции • Декораторы = separation of concerns. Регистрируем средствами контейнера или фреймворка 229
  230. Все вместе • IHandler<TIn, TOut> - строительный блок • ICommandHandler,

    IQueryHandler – холистические абстракции • Декораторы = separation of concerns. Регистрируем средствами контейнера или фреймворка • Система типов + Инварианты > Валидация (но не на границах) • Ждем C#8, а пока пишем гарды на NRE 230
  231. Все вместе • IHandler<TIn, TOut> - строительный блок • ICommandHandler,

    IQueryHandler – холистические абстракции • Декораторы = separation of concerns. Регистрируем средствами контейнера или фреймворка • Система типов + Инварианты > Валидация (но не на границах) • Ждем C#8, а пока пишем гарды на NRE • События в рамках транзакции 231
  232. Все вместе • IHandler<TIn, TOut> - строительный блок • ICommandHandler,

    IQueryHandler – холистические абстракции • Декораторы = separation of concerns. Регистрируем средствами контейнера или фреймворка • Система типов + Инварианты > Валидация (но не на границах) • Ждем C#8, а пока пишем гарды на NRE • События в рамках транзакции • Exception’ы – для ошибок (либо писать на F#) 232
  233. Все вместе • IHandler<TIn, TOut> - строительный блок • ICommandHandler,

    IQueryHandler – холистические абстракции • Декораторы = separation of concerns. Регистрируем средствами контейнера или фреймворка • Система типов + Инварианты > Валидация (но не на границах) • Ждем C#8, а пока пишем гарды на NRE • События в рамках транзакции • Exception’ы – для ошибок (либо писать на F#) • LINQ, ProjectTo, Permission Filters и Expressions – строительный блок, для получения данных 233
  234. Все вместе • IHandler<TIn, TOut> - строительный блок • ICommandHandler,

    IQueryHandler – холистические абстракции • Декораторы = separation of concerns. Регистрируем средствами контейнера или фреймворка • Система типов + Инварианты > Валидация (но не на границах) • Ждем C#8, а пока пишем гарды на NRE • События в рамках транзакции • Exception’ы – для ошибок (либо писать на F#) • LINQ, ProjectTo, Permission Filters и Expressions – строительный блок, для получения данных • By Feature > By Layer 234
  235. • Vertical Slices • https://www.youtube.com/watch?v=SUiWfhAhgQw • https://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91 • https://cuttingedge.it/blogs/steven/pivot/entry.php?id=92 •

    Одержимость примитивами • https://habr.com/post/266937/ • https://habr.com/post/205088/ • https://habr.com/post/205108/ • Domain Events • http://udidahan.com/2009/06/14/domain-events-salvation/ • https://lostechies.com/jimmybogard/2014/05/13/a-better-domain-events-pattern/ • https://enterprisecraftsmanship.com/2017/10/03/domain-events-simple-and-reliable-solution/ • DDD • https://habr.com/post/334126/ • https://habr.com/post/432410/ • ROP • https://fsharpforfunandprofit.com/rop/ • https://habr.com/post/339606/ • https://habr.com/post/347284/ • LINQ и Expressions: https://habr.com/company/jugru/blog/423891/ • Clean Architecture: https://www.youtube.com/watch?v=JEeEic-c0D4
  236. None
  237. Вопросы