Застосування принципу єдиної відповідальності


40

Нещодавно у мене виникла начебто тривіальна архітектурна проблема. У мене був простий сховище в коді, який називався так (код знаходиться в C #):

var user = /* create user somehow */;
_userRepository.Add(user);
/* do some other stuff*/
_userRepository.SaveChanges();

SaveChanges була простою обгорткою, яка здійснює зміни в базі даних:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
}

Потім через деякий час мені потрібно було застосувати нову логіку, яка надсилатиме сповіщення електронною поштою щоразу, коли користувач створюється в системі. Оскільки було багато дзвінків до _userRepository.Add()та SaveChangesнавколо системи, я вирішив оновити SaveChangesтак:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
    foreach (var newUser in dataContext.GetAddedUsers())
    {
       _eventService.RaiseEvent(new UserCreatedEvent(newUser ))
    }
}

Таким чином, зовнішній код може підписатися на UserCreateEvent і обробляти необхідну бізнес-логіку, яка надсилатиме сповіщення.

Але мені вказувалося, що моя модифікація SaveChangesпорушила принцип єдиної відповідальності, і це SaveChangesповинно просто врятувати, а не подіяти будь-які події.

Це дійсний пункт? Мені здається, що піднесення події тут - це по суті те саме, що і реєстрація: просто додати деяку побічну функціональність до функції. І SRP не забороняє вам використовувати журнал або запускати події у своїх функціях, він просто говорить про те, що така логіка повинна бути інкапсульована в інших класах, і в порядку сховища буде нормально називати ці інші класи.


22
Ваш реторт: "Добре, то як би ви це записали, щоб він не порушував SRP, але все ж допускав єдину точку модифікації?"
Роберт Харві

43
Моє зауваження полягає в тому, що проведення події не додає додаткової відповідальності. Насправді зовсім навпаки: відповідальність делегує десь інше.
Роберт Харві

Я думаю, що ваш колега правий, але ваше запитання є дійсним і корисним, тому високо цінується!
Андрес Ф.

16
Не існує такого поняття, як остаточне визначення єдиної відповідальності. Особа, яка вказує, що вона порушує SRP, є правильною, використовуючи її особисте визначення, і ви правильно використовуєте своє визначення. Я думаю, що ваш дизайн ідеально чудовий із застереженням, що ця подія не є одноразовою, коли інші подібні функції виконуються по-різному. Послідовність - це набагато важливіше звернути увагу, ніж на якусь невиразну інструкцію на кшталт SRP, яка переноситься на крайні межі, складається з тонн дуже простих для розуміння класів, які ніхто не знає, як змусити працювати в системі.
Данк

Відповіді:


14

Так, це може бути дійсна вимога мати сховище , яке стріляє певні події на певні дії , як Addі SaveChanges- і я не буду сумніватися в цьому (як і деякі інші відповіді) тільки тому , що ваш конкретний приклад додавання користувачів і надсилання електронного листа може виглядати трохи надуманий. Далі, припустимо, ця вимога цілком виправдана в контексті вашої системи.

Так, так , кодування механіки подій, а також ведення журналів, а також збереження в одному методі порушує SRP . Для багатьох випадків це, мабуть, прийнятне порушення, особливо коли ніхто ніколи не хоче розповсюджувати обов'язки щодо "збереження змін" та "підвищення події" на різні команди / обслуговуючі. Але давайте припустимо, що одного дня хтось хоче зробити саме це, чи можна це вирішити простим способом, можливо, помістивши код цих проблем у бібліотеки різних класів?

Рішення цього полягає в тому, щоб дозволити вашому оригінальному сховищу нести відповідальність за внесення змін до бази даних, нічого іншого, і створити проксі- сховище, яке має точно такий же публічний інтерфейс, повторно використовує оригінальний сховище та додає додаткову механіку подій до методів.

// In EventFiringUserRepo:
public void SaveChanges()
{
  _basicRepo.SaveChanges();
   FireEventsForNewlyAddedUsers();
}

private void FireEventsForNewlyAddedUsers()
{
  foreach (var newUser in _basicRepo.DataContext.GetAddedUsers())
  {
     _eventService.RaiseEvent(new UserCreatedEvent(newUser))
  }
}

Ви можете зателефонувати до класу проксі NotifyingRepositoryабо, ObservableRepositoryякщо вам це подобається, відповідно до вкрай голосованої відповіді @ Петра (яка насправді не говорить про те, як вирішити порушення SRP, лише кажучи, що порушення нормально).

Новий і старий клас репозиторію повинні походити із загального інтерфейсу, як показано в класичному описі шаблону проксі .

Потім у своєму початковому коді ініціалізуйте _userRepositoryоб’єкт нового EventFiringUserRepoкласу. Таким чином, ви зберігаєте оригінальний сховище відокремленим від механіки подій. Якщо потрібно, ви можете мати сховище для запуску подій та оригінальний сховище поруч і дозволити абонентам вирішити, чи вони використовують перше чи останнє.

Щоб вирішити одне занепокоєння, згадане в коментарях: чи це не призводить до того, що проксі-сервери зверху над проксі-серверами і так далі? Власне, додавання механіки подій створює основу для подальших вимог типу "надсилати електронні листи", просто підписуючись на події, таким чином дотримуючись SRP з цими вимогами, також без додаткових проксі. Але одне, що тут потрібно додати, це сама механіка подій.

Якщо подібне розмежування дійсно того варте в контексті вашої системи, це ви повинні вирішити самостійно. Я, мабуть, не відокремлював би журнал від початкового коду, не використовуючи іншого проксі, не додаючи реєстратор до події слухача, хоча це було б можливо.


3
Окрім цієї відповіді. Є альтернативи проксі, як АОП .
Лаїв

1
Я думаю, що ви пропускаєте суть, це не те, що підвищення події порушує SRP, а те, що підняття події лише для "Нових" користувачів вимагає, щоб репо несе відповідальність за те, що знає, що являє собою "Новий" користувач, а не "Щойно доданий до мене" "користувач
Ewan

@Ewan: будь ласка, прочитайте питання ще раз. Йшлося про місце в коді, яке виконує певні дії, які потрібно поєднувати з іншими діями поза межами відповідальності цього об'єкта. А розміщення акції та підняття подій в одному місці експерт-рецензент поставив під сумнів, як зрив СРП. Приклад "збереження нових користувачів" є лише з метою демонстрації. Назвіть приклад надуманий, якщо вам подобається, але це не ІМХО.
Док Браун

2
Це найкраща / правильна відповідь ІМО. Він не тільки підтримує SRP, але і підтримує принцип відкритого / закритого типу. І подумайте про всі автоматизовані тести, які зміни в класі можуть бути зламані. Модифікація існуючих тестів при додаванні нової функціональності викликає великий запах.
Кіт Пейн

1
Як це рішення працює, якщо транзакція триває? Коли це триває, SaveChanges()насправді запис бази даних насправді не створюється, і це може призвести до повернення назад. Здається, вам доведеться або перекрити AcceptAllChangesабо підписатися на подію TransactionCompleted.
Джон Ву

29

Надіслати сповіщення про зміну постійного сховища даних здається розумною справою під час збереження.

Звичайно, ви не повинні ставитися до "Додавання" як до окремого випадку - вам також доведеться запускати події для "Змінити" та "Видалити". Це особливе поводження з випадком "Додати", який пахне, змушує читача пояснити, чому він пахне, і в кінцевому підсумку змушує деяких читачів коду зробити висновок, що він повинен порушувати SRP.

Репозиторій "сповіщення", який можна запитувати, змінювати та запускати події щодо змін - це абсолютно нормальний об'єкт. Ви можете розраховувати на те, що ви знайдете кілька варіантів у майже будь-якому пристойному розмірі проекту.


Але чи сховане "сповіщення" сховище насправді те, що вам потрібно? Ви згадали C #: Багато людей погодилися б, що використовувати System.Collections.ObjectModel.ObservableCollection<>замість того, System.Collections.Generic.List<>коли останній - все, що вам потрібно, це всі види поганого і неправильного, але мало хто одразу вказує на SRP.

Те, що ви зараз робите, - це обміняти свою UserList _userRepositoryна ObservableUserCollection _userRepository. Якщо це найкращий спосіб дії чи ні, залежить від програми. Але хоча це, безперечно, робить _userRepositoryзначно менш легким, на мою скромну думку, це не порушує SRP.


Проблема з використанням ObservableCollectionу цьому випадку полягає в тому, що вона викликає еквівалентну подію не на виклик до SaveChanges, а на виклик до Add, що призведе до зовсім іншої поведінки, ніж та, яка показана в прикладі. Дивіться мою відповідь, як зберегти оригінальний репо-полегшений і при цьому дотримуватися SRP, зберігаючи семантику недоторканою.
Док Браун

@DocBrown Я посилався на відомі класи ObservableCollection<>та List<>для порівняння та контексту. Я не мав на увазі рекомендувати використовувати фактичні класи ні для внутрішньої реалізації, ні для зовнішнього інтерфейсу.
Петро

Гаразд, але навіть якщо ОП додасть події до "Модифікувати" та "Видалити" (що, на мою думку, ОП вичерпано, щоб питання було стислим, для простоти), я думаю, рецензент може легко прийти до висновку порушення SRP. Це, мабуть, є прийнятним, але жоден, який не може бути вирішений у разі потреби.
Док Браун

16

Так, це порушення принципу єдиної відповідальності та дійсного пункту.

Кращою конструкцією було б мати окремий процес, щоб отримати "нових користувачів" із сховища та надіслати електронні листи. Відстеження того, яким користувачам було надіслано електронний лист, відмов, відправках тощо тощо.

Таким чином ви можете обробляти помилки, збої тощо тощо, а також уникати захоплення сховищем кожної вимоги, яка має уявлення про те, що події відбуваються "коли щось робиться в базі даних".

Репозиторій не знає, що доданий вами користувач - це новий користувач. Його відповідальність - просто зберігання користувача.

Напевно, варто розширити коментарі нижче.

Репозиторій не знає, що доданий вами користувач - це новий користувач - так, він має метод, який називається Додати. Її семантика означає, що всі додані користувачі - це нові користувачі. Поєднайте всі аргументи, передані Add перед тим, як викликати Зберегти - і ви отримаєте всіх нових користувачів

Неправильно. Ви співставляєте "Додано до сховища" та "Нове".

"Додано до сховища" означає лише те, що він пише. Я можу додавати та видаляти та повторно додавати користувачів у різні сховища.

"Новий" - це стан користувача, визначений правилами бізнесу.

В даний час правило бізнесу може бути "Нове == щойно додане до сховища", але це не означає, що знати і застосовувати це правило не є окремою відповідальністю.

Ви повинні бути обережними, щоб уникнути подібного мислення, орієнтованого на базу даних. У вас будуть кращі регістри процесів, які додають не схожих користувачів до сховища, і коли ви надсилаєте їм електронні листи, весь бізнес скаже: "Звичайно, це не" нові "користувачі! Дійсне правило - X"

У цій відповіді IMHO зовсім не вистачає суті: репо - саме одне центральне місце в коді, яке знає, коли додаються нові користувачі

Неправильно. З причин, зазначених вище, плюс це не центральне місце, якщо ви фактично не включите код відправлення електронної пошти в клас, а не просто викликаєте подію.

У вас будуть програми, які використовують клас сховища, але не мають коду для надсилання електронної пошти. Коли ви додасте користувачів у ці програми, електронний лист не надсилатиметься.


11
Репозиторій не знає, що доданий вами користувач - це новий користувач - так, він має метод, який називається Add. Її семантика означає, що всі додані користувачі - це нові користувачі. Поєднайте всі аргументи, передані до Addдзвінка Save- і ви отримаєте всіх нових користувачів.
Андре Борхес

Мені подобається ця пропозиція. Однак прагматизм переважає над чистотою. Залежно від обставин, додавання абсолютно нового архітектурного шару до існуючої програми може бути важко виправдати, якщо все, що вам потрібно зробити, це буквально надіслати один електронний лист, коли користувач додається.
Олександр

Але подія не говорить користувача додано. Він говорить, що користувач створив. Якщо ми розглядаємо як називати речі належним чином і погоджуємось із семантичними відмінностями між додаванням і створенням, то подія у фрагменті або неправильно названа, або пропущена неправильно. Я не думаю, що рецензент не мав нічого проти Notyfing сховищ. Напевно, це хвилювало вид події та її побічні ефекти.
Лаїв

7
@Andre Нове в репо, але не обов'язково "нове" у діловому розумінні. це співвідношення цих двох ідей, що приховує додаткову відповідальність з першого погляду. Я можу імпортувати тонну старих користувачів у своє нове сховище, або видалити та повторно додати користувача тощо. Будуть ділові правила щодо того, що "новий користувач" поза "додано в дБ"
Еван

1
Модератор Примітка: Ваша відповідь не є журналістським інтерв'ю. Якщо у вас є зміни, природно включіть їх у свою відповідь, не створюючи цілого ефекту "новин". Ми не дискусійний форум.
Роберт Харві

7

Це дійсний пункт?

Так, хоча це дуже залежить від структури вашого коду. У мене немає повного контексту, тому я спробую поговорити взагалі.

Мені здається, що піднесення події тут - це по суті те саме, що і реєстрація: просто додати деяку побічну функціональність до функції.

Це абсолютно не так. Ведення журналу не є частиною бізнес-потоку, його можна відключити, воно не повинно викликати (ділових) побічних ефектів і жодним чином не впливати на стан та стан вашої програми, навіть якщо ви чомусь не змогли увійти в систему більше нічого. Тепер порівняйте це з доданою вами логікою.

І SRP не забороняє вам використовувати журнал або запускати події у своїх функціях, він просто говорить, що така логіка повинна бути інкапсульована в інших класах, і в порядку сховища буде добре називати ці інші класи.

SRP працює в тандемі з ISP (S і I в SOLID). Ви закінчуєте безліч класів і методів, які роблять дуже конкретні речі і більше нічого. Вони дуже зосереджені, їх дуже легко оновити або замінити, і взагалі легко (помилка) перевірити. Звичайно, на практиці ви також матимете кілька великих класів, які стосуються оркестрації: вони матимуть ряд залежностей, і вони зосереджуватимуться не на розпорошених діях, а на ділових діях, які можуть вимагати декількох кроків. Поки бізнес-контекст зрозумілий, їх теж можна назвати єдиною відповідальністю, але, як ви правильно сказали, у міру зростання коду, ви можете захотіти абстрагувати його частиною в нових класах / інтерфейсах.

Тепер повернемося до вашого конкретного прикладу. Якщо ви обов'язково повинні надсилати сповіщення щоразу, коли користувач створений і, можливо, навіть виконувати інші більш спеціалізовані дії, тоді ви можете створити окрему службу, яка інкапсулює цю вимогу, щось на зразок UserCreationService, який розкриває один метод Add(user), який обробляє обидва сховища (виклик до вашого сховища) та сповіщення як одна ділова дія. Або зробіть це у своєму оригінальному фрагменті, після_userRepository.SaveChanges();


2
Ведення журналу не є частиною бізнес-потоку - наскільки це актуально в контексті СРП? Якщо метою моєї події було б надіслати нові дані користувачів в Google Analytics - тоді її відключення матиме такий же діловий ефект, як і відключення журналу: не критично, але дуже засмучує. Яке правило додавання / не додавання нової логіки до функції? "Чи вимкнення його спричинить основні побічні ефекти для бізнесу?"
Андре Борхес

2
If the purpose of my event would be to send new user data to Google Analytics - then disabling it would have the same business effect as disabling logging: not critical, but pretty upsetting . Що робити, якщо ви обстрілюєте передчасні події, що спричиняють фейкові "новини". Що робити, якщо аналітика враховує "користувачів", які не були остаточно створені через помилки транзакції з БД? Що робити, якщо компанія приймає рішення щодо помилкових приміщень, підкріплених неточними даними? Ви занадто зосереджені на технічній стороні питання. "Іноді не можна побачити ліс для дерев" "
Laiv

@Laiv, ти робиш дійсну точку, але це не в моєму питанні чи у цій відповіді. Питання полягає в тому, чи це правильне рішення в контексті SRP, тому припустимо, що помилок транзакцій у БД немає.
Андре Борхес

Ви в основному просите мене сказати вам те, що ви хочете почути. Я просто даю вам розмах. Більш широке поле для вирішення того, чи має значення SRP чи ні, тому що SRP марний без належного контексту. ІМО у тому, як ви підходите до концерну, є неправильним, оскільки ви зосереджуєтесь лише на технічному рішенні. Вам слід надати достатньої актуальності всьому контексту. І так, БД може вийти з ладу. Існує шанс, що це станеться, і ви не повинні пропускати цього, тому що, як відомо, все трапляється, і ці речі можуть змінити вашу думку щодо сумнівів щодо СРП чи інших добрих практик.
Лаїв

1
З цього приводу пам’ятайте, що принципи - це не правила, написані каменем. Вони проникні (адаптивні). Як бачите, вони відкриті для тлумачення. Ваш рецензент має тлумачення, а у вас - інше. Постарайтеся побачити те, що бачите, вирішіть його / її сумніви та проблеми, або дозвольте йому / їй вирішити ваші. Тут не знайдете "правильної" відповіді. Правильну відповідь вирішувати ви та ваш рецензент, задаючи спочатку вимоги (функціональні та нефункціональні) проекту.
Лаїв

4

SRP теоретично стосується людей , як пояснює дядько Боб у своїй статті "Принцип єдиної відповідальності" . Дякуємо Роберту Харві за надання цього у вашому коментарі

Правильне питання:

Хто із зацікавлених сторін додав вимогу "надсилати електронні листи"?

Якщо цей власник також відповідає за збереження даних (малоймовірно, але можливо), це не порушує SRP. Інакше так і є.


2
Цікаво - я ніколи не чув про таке трактування СРП. Чи є у вас вказівки на додаткову інформацію / літературу про цю інтерпретацію?
sleske

2
@sleske: Від самого дядька Боба : "І це досягає сутності Принципу єдиної відповідальності. Цей принцип стосується людей. Коли ви пишете програмний модуль, ви хочете переконатися, що коли вимагаються зміни, ці зміни можуть відбуватися тільки від однієї людини, а точніше, єдиної щільно зв'язаної групи людей, що представляють єдину вузько визначену ділову функцію ".
Роберт Харві

Дякую Роберту. ІМО, назва "Принцип єдиної відповідальності" є жахливою, оскільки це звучить просто, але занадто мало людей слідкує за тим, яке значення передбачає "відповідальність". На кшталт того, як OOP мутував багато своїх оригінальних концепцій, і зараз це досить безглуздий термін.
user949300

1
Так. Ось що сталося з терміном REST. Навіть Рой Філдінг каже, що люди неправильно його використовують.
Роберт Харві

Хоча цитування пов'язане, я вважаю, що ця відповідь не відповідає тому, що вимога "надсилати електронні листи" не є жодною з прямих вимог, щодо яких йдеться про порушення порушення СРП. Однак, кажучи: "Який" учасник "додав вимогу" події підвищення " , ця відповідь стане більш пов'язаною з фактичним питанням. Я трохи змінив власну відповідь, щоб зробити це більш зрозумілим.
Док Браун

2

Хоча технічно немає нічого поганого в сховищах, що сповіщають про події, я б запропонував переглянути це з функціональної точки зору, коли його зручність викликає певні проблеми.

Створення користувача, вирішення питання про те, що таке новий користувач та його наполегливість - це 3 різні речі .

Приміщення моє

Розгляньте попередню передумову, перш ніж вирішити, чи сховище є правильним місцем для сповіщення ділових подій (незалежно від SRP). Зверніть увагу , що я сказав , діловий захід , тому що для мене UserCreatedмає інший відтінок , ніж UserStoredабо UserAdded 1 . Я також вважаю, що кожна з цих подій буде адресована різній аудиторії.

З одного боку, створення користувачів - правило, специфічне для бізнесу, яке може вимагати наполегливості або не може. Це може включати більше ділових операцій, залучаючи більше операцій з базою даних / мережею. Операції, про які є стійкі шари, не знають. У шару стійкості недостатньо контексту, щоб вирішити, чи вдало закінчився випадок використання чи ні.

З іншого боку, це не обов'язково, що _dataContext.SaveChanges();успішно зберігається користувач. Це залежатиме від періоду транзакцій бази даних. Наприклад, це може бути правдою для баз даних, таких як MongoDB, транзакції яких є атомними, але це не може, для традиційних RDBMS, що здійснюють транзакції ACID, де може бути задіяно більше транзакцій і вони ще мають бути здійснені.

Це дійсний пункт?

Це може бути. Однак я б наважився сказати, що це не лише питання СРП (технічно кажучи), це також питання зручності (функціонально кажучи).

  • Чи зручно звільняти ділові події з компонентів, які не знають про поточну дію?
  • Вони представляють правильне місце стільки, скільки потрібний момент для цього?
  • Чи варто дозволити цим компонентам упорядкувати мою ділову логіку завдяки таким повідомленням?
  • Чи можу я визнати недійсними побічні ефекти, викликані передчасними подіями? 2

Мені здається, що підняття події тут - це по суті те саме, що і реєстрація

Абсолютно ні. Однак реєстрація не повинна мати побічних ефектів, як ви припускали, що ця подія UserCreatedможе призвести до інших бізнес-операцій. Як сповіщення. 3

це просто говорить про те, що таку логіку слід інкапсулювати в інших класах, і в порядку сховища викликати ці інші класи

Не обов'язково правда. SRP не стосується лише класу. Він працює на різних рівнях абстракцій, як шари, бібліотеки та системи! Йдеться про згуртованість, про збереження разом, що змінюється з тих самих причин руками одних і тих же зацікавлених сторін . Якщо користувачеві створення ( використання випадку ) зміниться, швидше за все, міняється і момент, і причина події також змінюється.


1: Названня речей адекватним також має значення.

2: Скажімо, що ми надіслали UserCreatedпісля _dataContext.SaveChanges();, але пізніше ця транзакція з базою даних не вдалася через проблеми з підключенням або порушення обмежень. Будьте обережні з передчасним трансляцією подій, оскільки його побічні ефекти можуть бути важко скасувати (якщо це навіть можливо).

3: Процеси сповіщень, не оброблені належним чином, можуть призвести до запуску сповіщень, які неможливо відмінити


1
+1 Дуже хороший момент щодо тривалості транзакцій. Стверджувати, що створений користувач може бути передчасним, оскільки можливі відмови; і на відміну від журналу, ймовірно, якась інша частина програми щось робить із подією.
Андрес Ф.

2
Саме так. Події означають певність. Щось сталося, але все закінчилося.
Лаїв

1
@Laiv: За винятком випадків, коли вони цього не роблять. Microsoft має всілякі події з префіксом Beforeабо Previewвзагалі не дають гарантій щодо впевненості.
Роберт Харві

1
@ jpmc26: Без альтернативи, ваша пропозиція не корисна.
Роберт Харві

1
@ jpmc26: Отже, ваша відповідь - "перехід на зовсім іншу екосистему розвитку із зовсім іншим набором інструментів та характеристиками продуктивності". Назвіть мені навпаки, але я думаю, що це неможливо для переважної більшості зусиль з розвитку.
Роберт Харві

1

Ні це СРП не порушує.

Багато хто, здається, вважають, що Принцип єдиної відповідальності означає, що функція повинна виконувати лише "одне", а потім зациклюватися на обговоренні того, що являє собою "одне".

Але це не те, що означає принцип. Йдеться про проблеми на рівні бізнесу. Клас не повинен застосовувати багато питань або вимог, які можуть змінюватися незалежно на рівні бізнесу. Скажімо, клас обидва зберігає користувача та надсилає твердо кодоване вітальне повідомлення електронною поштою. Кілька незалежних проблем можуть спричинити зміни вимог такого класу. Дизайнер може вимагати змінити HTML / таблицю стилів пошти. Експерт із комунікацій може вимагати змінити формулювання пошти. І експерт UX міг би вирішити, що пошта насправді повинна бути надіслана в іншу точку вбудованого потоку. Тож клас підлягає багаторазовим змінам вимог із незалежних джерел. Це порушує СРП.

Але запуск події не порушує SRP тоді, оскільки подія залежить лише від збереження користувача, а не від будь-якої іншої турботи. Події насправді є дуже приємним способом підтримувати SRP, оскільки ви можете мати електронну пошту, викликану збереженням, без сховища впливати - або навіть знати про нього -.


1

Не хвилюйтеся за принцип єдиної відповідальності. Це не допоможе вам прийняти хороше рішення, оскільки ви можете суб'єктивно вибрати конкретну концепцію як "відповідальність". Ви можете сказати, що відповідальність класу - це управління збереженням даних у базі даних, або ви можете сказати, що її відповідальність полягає у виконанні всіх робіт, пов'язаних зі створенням користувача. Це просто різні рівні поведінки програми, і вони обидва є дійсними концептуальними виразами "єдиної відповідальності". Тож цей принцип не є корисним для вирішення вашої проблеми.

Найбільш корисним принципом, який слід застосувати в цьому випадку, є принцип найменшого сюрпризу . Тож давайте задамося питанням: чи дивно, що сховище з основною роллю збережених даних у базу даних також надсилає електронні листи?

Так, це дуже дивно. Це дві абсолютно окремі зовнішні системи, а назва 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()виклик не вдасться. (В іншому випадку вам знадобиться повномасштабна двофазна стратегія введення, щоб уникнути випадкових помилок.)


Суть має бути практичною. Насправді продумайте наслідки (як з точки зору ризику, так і користі) написання коду певним чином. Я вважаю, що "принцип єдиної відповідальності" не дуже часто допомагає мені це зробити, тоді як "принцип найменшого здивування" часто допомагає мені потрапити в голову іншого розробника (так би мовити) і подумати про те, що може статися.


4
чи дивно, що сховище з основною роллю збережених даних у базі даних також надсилає електронні листи - я думаю, ви пропустили суть мого питання. Мій сховище не надсилає електронні листи. Це просто викликає подію, а те, як поводитися з цією подією, - це відповідальність зовнішнього коду.
Андре Борхес

4
Ви в основному формулюєте аргумент "не використовуйте події".
Роберт Харві

3
[знизує плечима] Події є центральними у більшості фреймворків інтерфейсу. Усуньте події, і ці рамки взагалі не працюють.
Роберт Харві

2
@ jpmc26: Це називається ASP.NET Webforms. Це смокче.
Роберт Харві

2
My repository is not sending emails. It just raises an eventпричинно-наслідковий. Репозиторій запускає процес сповіщення.
Лаїв

0

Наразі SaveChangesце робить дві речі: він зберігає зміни та записує до цього записи. Тепер ви хочете додати ще одну річ: надсилати сповіщення електронною поштою.

У вас була розумна ідея додати подію до неї, але це було піддано критиці за порушення принципу єдиної відповідальності (СРП), не помічаючи, що воно вже було порушено.

Щоб отримати чисте рішення SRP, спочатку запустіть подію, потім зателефонуйте до всіх гаків для цієї події, яких зараз три: збереження, ведення журналів та нарешті надсилання електронних листів.

Або ви спочатку запускаєте подію, або потрібно додати SaveChanges. Ваше рішення - гібрид між ними. Він не звертається до наявного порушення, в той час як заохочує запобігти його зростанню понад три речі. Перестроювання існуючого коду на відповідність SRP може зажадати більше роботи, ніж це суворо необхідно. Від вашого проекту залежить, наскільки далеко вони хочуть зайняти SRP.


-1

Код вже порушив SRP - той самий клас відповідав за спілкування з контекстом даних та ведення журналів.

Ви просто оновите його до 3 обов'язків.

Одним із способів зняти речі з 1 відповідальності буде абстрагувати _userRepository; зробити його командно-мовним.

У ньому є набір команд, плюс набір слухачів. Він отримує команди та передає їх своїм слухачам. Можливо, ці слухачі замовлені, і, можливо, вони навіть можуть сказати, що команда не вдалася (що, в свою чергу, транслюється на слухачів, які вже були сповіщені).

Тепер більшість команд може мати лише 1 слухача (контекст даних). Перед вашими змінами SaveChanges має 2 - контекст даних, а потім реєстратор.

Потім ваша зміна додає іншого слухача для збереження змін, який полягає у залученні нових подій, створених користувачем, у службі подій.

У цьому є кілька переваг. Тепер ви можете видалити, оновити чи копіювати код журналу без решти свого коду. Ви можете додати більше тригерів при зміні збереження для більшої кількості речей, які потребують цього.

Все це вирішується, коли _userRepositoryстворено та підключено мережу (або, можливо, ці додаткові функції додаються / видаляються на льоту; можливість додавати / покращувати ведення журналу, коли запуск програми може бути корисним).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.