Должен ли я передать объект в конструктор или создать экземпляр в классе?


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 - это не подходит для Code Review.
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:

а) имеет нетривиальное распределение, например, из пула или фабричного метода.

б) может быть не выделен. Передача его в конструктор аккуратно избегает этой проблемы.

c) фактически является подклассом 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 только вчера (читая книгу « First First», посвященную шаблонам проектирования). Наивно я пошел и применил его для этого примера, но, как многие отмечали, один путь - другой (некоторые были более загадочными и говорили только: «Вы делаете это неправильно!»), Это не очень хорошая идея. Итак, чтобы не допустить повторения той же ошибки, которую я только что совершил, ниже приведено краткое изложение причин, по которым паттерн Синглтон может быть вредным (на основе комментариев и того, что я обнаружил, погугляя)

  1. Если ExampleAполучить собственную ссылку на Configэкземпляр, классы будут тесно связаны. Не будет никакого способа иметь экземпляр ExampleAдля использования другой версии Config(скажем, некоторого подкласса). Это ужасно, если вы хотите протестировать ExampleAс использованием экземпляра макета, Configпоскольку нет способа предоставить его ExampleA.

  2. Предположение о том, что будет один, и только один, Configможет быть, имеет место сейчас , но вы не всегда можете быть уверены, что то же самое сохранится в будущем . Если в какой-то более поздний момент окажется, что несколько экземпляров Configбудут желательны, нет способа достичь этого без переписывания кода.

  3. Даже если один-единственный-единственный экземпляр Configможет быть верным на всю вечность, может случиться так, что вы захотите использовать некоторый подкласс Config(хотя при этом у вас будет только один экземпляр). Но, поскольку код напрямую получает экземпляр с помощью getInstance()of Config, который является staticметодом, нет способа получить подкласс. Опять же, код должен быть переписан.

  4. Тот факт, что ExampleAиспользует, Configбудет скрыт, по крайней мере, при просмотре только API ExampleA. Это может или не может быть плохой вещью, но лично я чувствую, что это чувствует себя недостатком; например, при ведении не существует простого способа выяснить, на какие классы будут влиять изменения, Configне рассматривая реализацию любого другого класса.

  5. Даже если сам факт ExampleAиспользования Singleton Config не является проблемой сам по себе, он все равно может стать проблемой с точки зрения тестирования. Одиночные объекты будут нести состояние, которое будет сохраняться до завершения приложения. Это может быть проблемой при запуске модульных тестов, поскольку вы хотите, чтобы один тест был изолирован от другого (то есть выполнение одного теста не должно влиять на результат другого). Чтобы исправить это, объект Singleton должен быть уничтожен между каждым запуском теста (возможно, потребуется перезапустить все приложение), что может занять много времени (не говоря уже об утомительном и раздражающем).

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


4
Я бы не рекомендовал делать это .. таким образом, вы плотно объединяете класс ExampleAи Config- что не очень хорошая вещь.
Пол

@ Пол: это правда. Хороший улов, не думал об этом.
Габлин

3
Я бы всегда рекомендовал не использовать Singletons из соображений тестируемости. По сути, они являются глобальными переменными, и зависимость от насмешки невозможно.
MattDavey

4
Я бы всегда рекомендовал повторять использование Singletons по причинам, по которым вы делаете это неправильно.
Рейнос

1

Самое простое, что нужно сделать, это соединиться ExampleAс Config. Вы должны делать самое простое, если нет веских причин делать что-то более сложное.

Одной из причин развязки ExampleAи Configбудет улучшение тестируемости ExampleA. Прямое соединение будет деградировать тестируемости ExampleAесли Configесть методы , которые являются медленными, сложными, или быстро развивается. Для тестирования метод медленный, если он выполняется более нескольких микросекунд. Если бы все методы Configбыли простыми и быстрыми, то я бы выбрал простой подход и прямо связался ExampleAс ним Config.


1

Ваш первый пример - пример шаблона внедрения зависимостей. Классу с внешней зависимостью передается зависимость конструктору, сеттеру и т. Д.

Этот подход приводит к слабосвязанному коду. Большинство людей считают, что слабая связь - это хорошая вещь, потому что вы можете легко заменить конфигурацию в тех случаях, когда один конкретный экземпляр объекта должен быть настроен иначе, чем другие, вы можете передать фиктивный объект конфигурации для тестирования и т. Д. на.

Второй подход ближе к шаблону создателя GRASP. В этом случае объект создает свои собственные зависимости. В результате получается тесно связанный код, это может ограничить гибкость класса и усложнить его тестирование. Если вам нужен один экземпляр класса, чтобы иметь зависимость, отличную от других, единственный вариант - это создать его подкласс.

Однако это может быть подходящим шаблоном в тех случаях, когда продолжительность жизни зависимого объекта определяется временем жизни зависимого объекта и когда зависимый объект не используется нигде, кроме объекта, который от него зависит. Обычно я бы посоветовал DI быть позицией по умолчанию, но вам не нужно полностью исключать другой подход, если вы знаете о его последствиях.


0

Если ваш класс не предоставляет $configвнешние классы, я бы создал его внутри конструктора. Таким образом, вы сохраняете свое внутреннее состояние частным.

Если $configrequire требует, чтобы его собственное внутреннее состояние было правильно установлено (например, требуется подключение к базе данных или инициализированы некоторые внутренние поля) перед использованием, то имеет смысл отложить инициализацию до некоторого внешнего кода (возможно, фабричного класса) и внедрить его в конструктор Или, как уже отмечали другие, если это необходимо для совместного использования среди других объектов.


0

ExampleA отделен от конкретного класса Config, что хорошо , если объект получен, если не типа Config, а типа абстрактного суперкласса Config.

ExampleB сильно связан с конкретным классом Config, что плохо .

Создание объекта создает сильную связь между классами. Это должно быть сделано в классе Factory.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.