Не хвилюйтеся за принцип єдиної відповідальності. Це не допоможе вам прийняти хороше рішення, оскільки ви можете суб'єктивно вибрати конкретну концепцію як "відповідальність". Ви можете сказати, що відповідальність класу - це управління збереженням даних у базі даних, або ви можете сказати, що її відповідальність полягає у виконанні всіх робіт, пов'язаних зі створенням користувача. Це просто різні рівні поведінки програми, і вони обидва є дійсними концептуальними виразами "єдиної відповідальності". Тож цей принцип не є корисним для вирішення вашої проблеми.
Найбільш корисним принципом, який слід застосувати в цьому випадку, є принцип найменшого сюрпризу . Тож давайте задамося питанням: чи дивно, що сховище з основною роллю збережених даних у базу даних також надсилає електронні листи?
Так, це дуже дивно. Це дві абсолютно окремі зовнішні системи, а назва 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()
виклик не вдасться. (В іншому випадку вам знадобиться повномасштабна двофазна стратегія введення, щоб уникнути випадкових помилок.)
Суть має бути практичною. Насправді продумайте наслідки (як з точки зору ризику, так і користі) написання коду певним чином. Я вважаю, що "принцип єдиної відповідальності" не дуже часто допомагає мені це зробити, тоді як "принцип найменшого здивування" часто допомагає мені потрапити в голову іншого розробника (так би мовити) і подумати про те, що може статися.