Не беспокойтесь о принципе единой ответственности. Это не поможет вам принять правильное решение, потому что вы можете субъективно выбрать конкретную концепцию в качестве «ответственности». Вы можете сказать, что ответственность класса заключается в управлении сохранением данных в базе данных, или вы можете сказать, что его ответственность заключается в выполнении всей работы, связанной с созданием пользователя. Это просто разные уровни поведения приложения, и оба они являются допустимыми концептуальными выражениями «единой ответственности». Так что этот принцип бесполезен для решения вашей проблемы.
Наиболее полезным принципом, который следует применять в этом случае, является принцип наименьшего удивления . Итак, давайте зададимся вопросом: удивительно ли, что хранилище с основной ролью сохранения данных в базе данных также отправляет электронные письма?
Да, это очень удивительно. Это две совершенно разные внешние системы, и название SaveChanges
не подразумевает также отправку уведомлений. Тот факт, что вы делегируете это событию, делает поведение еще более удивительным, поскольку тот, кто читает код, больше не может легко увидеть, какие дополнительные действия вызываются. Непрямость вредит читабельности. Иногда выгоды стоят затрат на удобочитаемость, но не тогда, когда вы автоматически вызываете дополнительную внешнюю систему, которая имеет эффекты, наблюдаемые для конечных пользователей. (В этом случае ведение журнала можно исключить, поскольку его эффект по сути состоит в ведении записей для целей отладки. Конечные пользователи не используют журнал, поэтому не всегда вредно вести журнал.) Что еще хуже, это уменьшает гибкость во времени отправки электронной почты, что делает невозможным чередование других операций между сохранением и уведомлением.
Если вашему коду обычно требуется отправить уведомление, когда пользователь успешно создан, вы можете создать метод, который делает это:
public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
repo.Add(user);
repo.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
Но увеличивает ли это ценность, зависит от специфики вашего приложения.
Я бы вообще не одобрял существование этого SaveChanges
метода. Этот метод предположительно будет фиксировать транзакцию базы данных, но другие репозитории могли изменить базу данных в той же транзакции . Тот факт, что он фиксирует их все, снова удивляет, поскольку SaveChanges
он специально привязан к этому экземпляру репозитория пользователя.
Наиболее простой шаблон для управления транзакцией базы данных - это внешний using
блок:
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
context.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
Это дает программисту явный контроль над тем, когда изменения для всех репозиториев сохраняются, заставляет код явно задокументировать последовательность событий, которые должны произойти перед фиксацией, обеспечивает откат при ошибке (при условии, что DataContext.Dispose
происходит откат) и избегает скрытого связи между классами с сохранением состояния.
Я также предпочел бы не отправлять электронную почту непосредственно в запросе. Было бы надежнее записать необходимость уведомления в очереди. Это позволит улучшить обработку ошибок. В частности, если при отправке электронной почты возникает ошибка, ее можно повторить позже, не прерывая сохранение пользователя, и это исключает случай, когда пользователь создается, но сайт возвращает ошибку.
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
_emailNotificationQueue.AddUserCreateNotification(user);
_emailNotificationQueue.Commit();
context.SaveChanges();
}
Лучше сначала зафиксировать очередь уведомлений, так как потребитель очереди может проверить, существует ли пользователь перед отправкой электронной почты, в случае context.SaveChanges()
сбоя вызова. (В противном случае вам понадобится полноценная двухфазная стратегия фиксации, чтобы избежать ошибок в коде.)
Суть в том, чтобы быть практичным. На самом деле продумайте последствия (как с точки зрения риска, так и выгоды) написания кода определенным образом. Я считаю, что «принцип единой ответственности» не очень часто помогает мне в этом, в то время как «принцип наименьшего удивления» часто помогает мне проникнуть в голову другого разработчика (так сказать) и подумать о том, что может произойти.