Чи повинен я передати об'єкт конструктору чи інстанцію в класі?


10

Розглянемо ці два приклади:

Передача об'єкта конструктору

class ExampleA
{
  private $config;
  public function __construct($config)
  {
     $this->config = $config;
  }
}
$config = new Config;
$exampleA = new ExampleA($config);

Миттєвий пошук класу

class ExampleB
{
  private $config;
  public function __construct()
  {
     $this->config = new Config;
  }
}
$exampleA = new ExampleA();

Який правильний спосіб обробляти додавання об’єкта як властивості? Коли я повинен використовувати один над іншим? Чи впливає тестування одиниць, що я повинен використовувати?


Чи може це бути краще на codereview.stackexchange.com/questions ?
FrustratedWithFormsDesigner

9
@FrustratedWithFormsDesigner - це не підходить для перегляду коду.
ChrisF

Відповіді:


14

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

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


1
Отже, A добре для тестування одиниць, B добре для інших обставин ... чому б не мати обох? У мене часто є два конструктори, один для ручного DI, один для звичайного використання (я використовую Unity для DI, який генерує конструктори для виконання його вимог щодо DI).
Ед Джеймс

3
@EdWoodcock мова йде не про тестування одиниць порівняно з іншими обставинами. Об'єкт або вимагає залежності ззовні, або керує нею зсередини. Ніколи і / і інше.
MattDavey

9

Не забувайте про заповітність!

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

Тому я б пішов із першим варіантом ExampleA


Ось чому я згадав про тестування - дякую, що додав це :)
В'язень

5

Якщо об’єкт несе відповідальність за управління життєвою залежністю, тоді це нормально створити об'єкт у конструкторі (і розмістити його в деструкторі). *

Якщо об'єкт не несе відповідальності за управління життєвою залежністю, його слід передавати в конструктор і керувати ззовні (наприклад, контейнером IoC).

У цьому випадку я не думаю, що ваш ClassA не несе відповідальності за створення $ config, якщо він також не несе відповідальність за його розміщення або якщо конфігурація унікальна для кожного примірника ClassA.

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

//Object which manages the lifetime of its dependency (C#):
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA()
    {
        this.Config = new Config(); // Tightly coupled to Config class...
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

// Object which does not manage its dependency:
public class ClassA
{
    public Config Config { get; set; }

    public ClassA(Config config) // dependency may be injected...
    {
        this.Config = config;
    }
}

// Object which manages its dependency in a testable way:
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA(IConfigFactory configFactory) // dependency may be mocked...
    {
        this.Config = configFactory.BuildConfig();
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

2

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

У вашому прикладі, що робити, якщо ваш клас Config:

а) має нетривіальне виділення, наприклад, що надходить із пулу чи фабричного методу.

б) не вдалося виділити. Передача його в конструктор акуратно уникає цього питання.

в) насправді є підкласом Config.

Передача об'єкта в конструктор дає найбільшу гнучкість.


1

Відповідь нижче неправильна, але я буду тримати її для того, щоб інші дізналися від неї (див. Нижче)

У ExampleA, ви можете використовувати один і той же Configекземпляр для кількох класів. Однак якщо Configу всій програмі має бути лише один екземпляр, розгляньте можливість застосування шаблону Singleton, Configщоб уникнути появи декількох екземплярів Config. І якщо Configце Singleton, ви можете зробити наступне:

class ExampleA
{
  private $config;
  public function __construct()
  {
     $this->config = Config->getInstance();
  }
}
$exampleA = new ExampleA();

З ExampleBіншого боку, ви завжди отримаєте окремий екземпляр Configдля кожного екземпляра ExampleB.

Яку версію слід застосувати насправді, залежить від того, як програма буде обробляти екземпляри Config:

  • якщо кожен екземпляр ExampleXповинен мати окремий екземпляр Config, перейдіть з ExampleB;
  • якщо кожен екземпляр ExampleXбуде ділити один (і лише один) екземпляр Config, використовувати ExampleA with Config Singleton;
  • якщо екземпляри ExampleXможуть використовувати різні екземпляри Config, дотримуйтесь ExampleA.

Чому перетворення Configв Singleton є неправильним:

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

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

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

  3. Незважаючи на те, що один-єдиний екземпляр Config, можливо, правдивий на всю вічність, може статися так, що ви хочете мати можливість використовувати якийсь підклас Config(при цьому, маючи лише один примірник). Але, так як код безпосередньо отримує екземпляр через getInstance()з Config, який являє собою staticметод, немає ніякого способу , щоб отримати підклас. Знову код потрібно переписати.

  4. Те, що ExampleAвикористовує, Configбуде приховано, принаймні, лише переглядаючи API ExampleA. Це може бути або не бути поганою справою, але особисто я відчуваю, що це відчуває себе недоліком; наприклад, при підтримці не існує простого способу з’ясувати, на які класи впливатимуть зміни, Configне вивчаючи впровадження будь-якого іншого класу.

  5. Навіть якщо факт ExampleAвикористання Singleton Config сам по собі не є проблемою, він все ж може стати проблемою з точки зору тестування. Об'єкти Singleton матимуть стан, який зберігатиметься до припинення роботи програми. Це може бути проблемою при запуску одиничних тестів, оскільки ви хочете, щоб один тест був ізольований від іншого (тобто те, що виконавши один тест, не повинно впливати на результат іншого). Щоб виправити це, об'єкт Singleton повинен бути знищений між кожним тестовим запуском (можливо, доведеться перезапустити всю програму), що може зайняти багато часу (не кажучи вже про стомлювальний і дратівливий).

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


4
Я б не рекомендував робити це .. таким чином ви щільно паруєте клас ExampleAі Config- що не дуже добре.
Пол

@Paul: Це правда. Хороший улов, не думав про це.
габлін

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

4
Я б завжди рекомендував повторно використовувати Singletons з причин того, що ви робите це неправильно.
Райнос

1

Найпростіше , що потрібно зробити , це пару ExampleAдо Config. Вам слід зробити найпростішу справу, якщо тільки немає вагомих причин зробити щось складніше.

Однією з причин для розчеплення ExampleAта Configбуло б поліпшити тестованості ExampleA. Пряме з'єднання погіршить доказовість, ExampleAякщо Configє методи, які повільні, складні або швидко розвиваються. Для тестування метод є повільним, якщо він працює більше декількох мікросекунд. Якщо всі методи Configпрості і швидкі, то я б скористався простим підходом і безпосередньо підключився ExampleAдо Config.


1

Ваш перший приклад - приклад схеми вприскування залежності. Клас із зовнішньою залежністю передає залежність конструктором, сетером тощо.

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

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

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


0

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

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


0

ExampleA від'єднується від конкретного класу Config, що добре , якщо об'єкт отриманий, якщо не типу Config, а типу абстрактного надкласу Config.

ExampleB сильно поєднаний з конкретним класом Config, що погано .

Ігнорування об'єкта створює міцну зв’язок між класами. Це слід робити у фабричному класі.

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