Таким чином я пишу цей код можна перевірити, але чи не в цьому щось не вистачає?


13

У мене інтерфейс називається IContext. Для цього не важливо, що він робить, крім наступного:

T GetService<T>();

Що цей метод робить, це подивитися на поточний контейнер DI програми та спроби вирішити залежність. Дуже стандартно, я думаю.

У моєму додатку ASP.NET MVC мій конструктор виглядає приблизно так.

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetService<ISomeService>();
    AnotherService = ctx.GetService<IAnotherService>();
}

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

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

Я можу. Не буде важко знущатися над IContextтестуванням контролера. Мені все одно доведеться:

public class MyMockContext : IContext
{
    public T GetService<T>()
    {
        if (typeof(T) == typeof(ISomeService))
        {
            // return another mock, or concrete etc etc
        }

        // etc etc
    }
}

Але, як я вже сказав, це почувається неправильно. Будь-які думки / зловживання вітаються.


8
Це називається Locator Service, і мені це не подобається. Про це було написано багато інформації - див. Martinfowler.com/articles/injection.html та blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern для початку.
Бенджамін Ходжсон

З статті Мартіна Фаулера: "Я часто чув скаргу на те, що подібні локатори служб - це погана річ, тому що вони не перевіряються, тому що ви не можете замінити їх реалізацію. Звичайно, ви можете їх погано спроектувати, щоб вступити в це". певна проблема, але вам не доведеться. У цьому випадку екземпляр локатора послуг - це просто простий власник даних. Я легко можу створити локатор за допомогою тестових реалізацій моїх служб ". Чи можете ви пояснити, чому вам це не подобається? Може, у відповідь?
LiverpoolsNumber9

8
Він правий, це поганий дизайн. Це легко : public SomeClass(Context c). Цей код цілком зрозумілий, чи не так? Зазначається, that SomeClassзалежить від а Context. Помилка, але чекай, це не так! Він покладається лише на залежність, Xяку отримує від контексту. Це означає, що кожного разу, коли ви вносите зміни, Contextце може зламатися SomeObject, навіть якщо ви лише змінювали Contexts Y. Але так, ви знаєте, що ви тільки Yне змінилися X, так SomeClassце добре. Але писати хороший код - це не те, що ви знаєте, а те, що знає новий працівник, коли він дивиться ваш код вперше.
валентери

@DocBrown Для мене це саме те, що я сказав - я не бачу різниці тут. Чи можете ви поясніть далі?
валентери

1
@DocBrown Я бачу вашу думку зараз. Так, якщо його Контекст - це лише зв'язок усіх залежностей, то це не поганий дизайн. Це може бути погано називати, але це також лише припущення. ОП повинна уточнити, чи існує більше методів (внутрішніх об'єктів) контексту. Крім того, обговорення коду чудово, але це программіст.stackexchange, тому мені слід також намагатися бачити "позаду" речі, щоб покращити ОП.
валентери

Відповіді:


5

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

Ваш перший приклад може бути змінено на

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetSomeService();
    AnotherService = ctx.GetAnotherService();
}

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

  • переконайтеся TheContext, SomeServiceі AnotherServiceзавжди все не започатковано фіктивними об'єктами, або все з них з реальними об'єктами
  • або, щоб дозволити ініціалізацію їх різними комбінаціями трьох об'єктів (це означає, що в цьому випадку вам потрібно буде передавати параметри індивідуально)

Тож використання лише одного параметра замість 3 у конструкторі може бути цілком розумним.

Проблемним є IContextвикриття GetServiceметоду публічно. IMHO, вам слід цього уникати, а не зберігати "фабричні методи". Тож буде нормально реалізувати методи GetSomeServiceта GetAnotherServiceметоди з мого прикладу за допомогою сервісного локатора? ІМХО, що залежить. Поки IContextклас просто несе просту фабрику-конспект із певною метою надання чіткого списку об’єктів обслуговування, що є IMHO прийнятним. Анотаційні фабрики, як правило, є лише "клейовим" кодом, який не повинен сам перевірятись. Поза тим, ви повинні запитати себе, чи в контексті таких методів, як GetSomeServiceвам дійсно потрібен локатор сервісу, чи явний виклик конструктора не був би простішим.

Тож будьте обережні, коли ви дотримуєтесь дизайну, де IContextреалізація є лише обгорткою навколо загальнодоступного, загального GetServiceметоду, що дозволяє вирішувати будь-які довільні залежності залежними класами, тоді все застосовується те, що @BenjaminHodgson написав у своїй відповіді.


Я згоден з цим. Проблема з прикладом - це загальний GetServiceметод. Рефакторинг на явно названі та введені методи краще. Ще краще було б чітко прояснити залежності IContextреалізації в конструкторі.
Бенджамін Ходжсон

1
@BenjaminHodgson: це може бути іноді краще, але це не завжди краще. Ви зазначаєте запах коду, коли список параметрів ctor стає довшим і довшим. Дивіться мою колишню відповідь тут: programmers.stackexchange.com/questions/190120/…
Doc Brown

@DocBrown "кодовий запах" надмірного впорскування конструктора вказує на порушення SRP, що є реальною проблемою. Просто загортання декількох сервісів у клас Facade, лише для викриття їх як властивостей не робить нічого для вирішення джерела проблеми. Таким чином, фасад не повинен бути простим обгортком навколо інших компонентів, але в ідеалі повинен пропонувати спрощений API, за допомогою якого споживати їх (або слід спростити їх іншим чином) ...
AlexFoxGill

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

Де ви були, коли Microsoft запекла це IValidatableObject?
RubberDuck

15

Цей дизайн відомий як Locator Service * і мені це не подобається. Проти цього є багато аргументів:

Локатор обслуговування приєднує вас до вашого контейнера . Використовуючи регулярне введення залежності (де конструктор чітко викладає залежності), ви можете прямо замінити контейнер на інший або повернутися до new-виразів. З вашим, IContextщо насправді неможливо.

Локатор послуг приховує залежності . Як клієнту, дуже важко сказати, що потрібно для побудови екземпляра класу. Вам потрібен якийсь тип IContext, але вам також потрібно встановити контекст для повернення правильних об'єктів для того, щоб зробити MyControllerBaseроботу. Це зовсім не очевидно з підпису конструктора. За допомогою регулярного DI компілятор повідомляє вам саме те, що вам потрібно. Якщо у вашому класі багато залежностей, ви повинні відчувати цей біль, оскільки він підштовхне вас до рефактора. Локатор обслуговування приховує проблеми із поганими конструкціями.

Локатор обслуговування викликає помилки під час виконання . Якщо ви зателефонували GetServiceз неправильним параметром типу, ви отримаєте виняток. Іншими словами, ваша GetServiceфункція не є тотальною функцією. (Загальні функції - це ідея світу FP, але це в основному означає, що функції завжди повинні повертати значення.) Краще дозвольте компілятору допомогти і повідомити, коли ви неправильно залежите.

Локатор обслуговування порушує Принцип заміни Ліскова . Оскільки його поведінка змінюється залежно від аргументу типу, Локатор послуг може розглядатися так, ніби він фактично має нескінченну кількість методів в інтерфейсі! Цей аргумент детально описаний тут .

Локатор обслуговування важко перевірити . Ви подали приклад підробки IContextдля тестів, що добре, але, безумовно, краще не потрібно писати цей код в першу чергу. Просто введіть свої підроблені залежності безпосередньо, не переходячи через сервіс-локатор.

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

* Я визначаю Локатор Служб як об'єкт із загальним Resolve<T>методом, який здатний вирішувати довільні залежності і використовується у всій кодовій базі даних (не лише у кореневому складі). Це не те саме, що фасад служби (об’єкт, який поєднує деякий невеликий відомий набір залежностей) або абстрактний завод (об’єкт, який створює екземпляри одного типу - тип абстрактної фабрики може бути загальним, але метод не є) .


1
Ви скаржитеся на шаблон пошуку локатора послуг (з яким я згоден). Але насправді, в прикладі ОП, MyControllerBaseне пов'язаний ні з конкретним контейнером DI, ні це "насправді" приклад анти-шаблону служби "Локатор послуг".
Док Браун

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

2
Для мене відмітною ознакою антидіаграми службового локатора є загальний GetService<T>метод. Вирішення довільних залежностей - це справжній запах, який присутній і правильний на прикладі ОП.
Бенджамін Ходжсон

1
Ще одна проблема використання Локатора послуг полягає в тому, що це зменшує гнучкість: ви можете мати лише одну реалізацію кожного сервісного інтерфейсу. Якщо ви будуєте два класи, які покладаються на IFrobnicator, але пізніше вирішують, що для одного слід використовувати оригінальну реалізацію DefaultFrobnicator, а для іншого слід використати декоратор CacheingFrobnicator навколо нього, ви повинні змінити існуючий код, тоді як якщо ви введення залежностей безпосередньо все, що вам потрібно зробити, це змінити код налаштування (або конфігураційний файл, якщо ви використовуєте рамку DI). Таким чином, це порушення OCP.
Жуль

1
@DocBrown GetService<T>()Метод дозволяє запитувати довільні класи: "Що цей метод робить, це подивитися на поточний контейнер DI програми та намагатися вирішити залежність. Думаю, досить стандартний". . Я відповідав на ваш коментар у верхній частині цієї відповіді. Це 100% локатор обслуговування
AlexFoxGill

5

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

Гаразд, щоб відповісти на запитання - давайте перезгадаємо вашу фактичну проблему :

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

Існує питання, яке вирішує цю проблему в StackOverflow . Як згадувалося в одному з коментарів:

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

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

У всіх проектах, над якими я працював, де є базовий контролер, це робилося виключно для обміну зручними властивостями та методами, такими як IsUserLoggedIn()і GetCurrentUserId(). СТОП . Це жахливе зловживання спадщиною. Натомість створіть компонент, який відкриває ці методи, і приймайте залежність від нього там, де вам це потрібно. Таким чином, ваші компоненти залишаться перевіреними, і їх залежність буде очевидною.

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

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


+1 - це нове питання щодо питання, на яке жоден з інших відповідей справді не вирішено
Бенджамін Ходжсон

1
"Однією з чудових переваг конструкторської інжекції є те, що вона робить порушення Принципу єдиної відповідальності яскраво очевидним" Любіть це. Дійсно хороша відповідь. Не згоден з усім цим. Моє використання - це, як не дивно, не дублювати код у системі, яка матиме понад 100 контролерів (принаймні). Однак за SRP - кожен сервіс, що вводиться, несе єдину відповідальність, як і контролер, який використовує їх усі - як би один рефрактор, що ??
LiverpoolsNumber9

1
@ LiverpoolsNumber9 Ключовим моментом є переміщення функціональності з BaseController в залежності, поки у вас не залишиться нічого в BaseController, окрім protectedделегації з одного лайнера на нові компоненти. Тоді ви можете втратити BaseController і замінити ці protectedметоди прямими дзвінками до залежностей - це спростить вашу модель і зробить всі ваші залежності явними (що дуже добре в проекті з контролерами на 100!)
AlexFoxGill

@ LiverpoolsNumber9 - якщо ви зможете скопіювати частину свого BaseController на пастбін, я можу зробити конкретні пропозиції
AlexFoxGill

-2

Я додаю відповідь на це, грунтуючись на внесках усіх інших. Стільки спасибі ніколи нікому. Спочатку ось моя відповідь: "Ні, в цьому немає нічого поганого".

Відповідь Дока Брауна "Фасад сервісу" Я прийняв цю відповідь, тому що те, що я шукав (якщо відповідь було "ні"), - це деякі приклади чи деяке розширення того, що я робив. Він зазначив це, підказавши, що А) воно отримало ім'я, і ​​В), мабуть, є кращі способи зробити це.

Відповідь "Локатора послуг" Бенджаміна Ходжсона. Я настільки високо оцінюю свої знання, які я тут здобув, а не "Локатор послуг". Це "Фасад сервісу". У цій відповіді все правильно, але не для моїх обставин.

Відповіді USR

Я вирішу це детальніше:

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

Я не втрачаю ніяких інструментів і не втрачаю жодного "статичного" набору тексту. Фасад служби поверне те, що я налаштував у своєму контейнері DI, або default(T). І те, що воно повертає, "набирається". Рефлексія інкапсульована.

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

Це, звичайно, не "рідко". Оскільки я використовую базовий контролер, щоразу, коли мені потрібно змінити його конструктор, можливо, мені доведеться змінити 10, 100, 1000 інших контролерів.

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

Я використовую ін'єкцію залежності. В тім-то й річ.

І нарешті, коментар Жуля на відповідь Бенджаміна: Я не втрачаю гнучкості. Це мій фасад обслуговування. Я можу додати стільки параметрів, GetService<T>скільки я хочу розрізнити між різними реалізаціями так само, як це робиться під час налаштування контейнера DI. Так, наприклад, я міг би змінитись, GetService<T>()щоб GetService<T>(string extraInfo = null)обійти цю "потенційну проблему".


У будь-якому разі, ще раз дякую всім. Це було дійсно корисно. Ура.


4
Я не погоджуюся, що у вас тут є Фасад обслуговування. GetService<T>()Метод здатний (спроба) Рішучість залежностей довільної . Це робить Локатор Служб, а не Фасад Сервісу, як я пояснив у виносці своєї відповіді. Якби ви замінили його невеликим набором GetServiceX()/ GetServiceY()методів, як пропонує @DocBrown, то це був би Фасад.
Бенджамін Ходжсон

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

3
Щодо статичного набору тексту - тобі не вистачає точки. Якщо я спробую написати код, який не забезпечує клас, використовуючи DI, з потрібним йому параметром конструктора, він не буде компілюватися. Це статична безпека для компіляції в часі від проблем. Якщо ви пишете свій код, надаючи своєму конструктору IContext, який не був правильно налаштований для надання аргументу, який він насправді потребує, він збиратиметься та виходить з ладу під час виконання
Бен Ааронсон,

3
@ LiverpoolsNumber9 Ну чому тоді взагалі сильно набрана мова? Помилки компілятора - це перша лінія захисту від помилок. Одиничні випробування є другими. Ваших не було б підібрано одиничними тестами, оскільки це взаємодія між класом та його залежністю, тож ви потрапили б у третю лінію захисту: інтеграційні тести. Я не знаю, як часто ви запускаєте їх, але зараз ви говорите про зворотній зв'язок про порядок хвилин або більше, а не про мілісекунди, які потрібні вашому IDE, щоб підкреслити помилку компілятора.
Бен Ааронсон

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