Как использовать Dependency Injection, не нарушая инкапсуляцию? - PullRequest
32 голосов
/ 29 сентября 2010

Как я могу выполнить внедрение зависимостей, не нарушая инкапсуляцию?

Используя Пример внедрения зависимостей из Википедии :

public Car {
    public float getSpeed();
}

Примечание: Другие методы и свойства (например, PushBrake (), PushGas (), SetWheelPosition ()) для ясности опущены

Это работает хорошо;Вы не знаете, как мой объект реализует getSpeed - это " инкапсулировано ".

На самом деле мой объект реализует getSpeed как:

public Car {
    private m_speed;
    public float getSpeed( return m_speed; );
}

И все хорошо.Кто-то конструирует мой Car объект, педали пюре, гудок, руль и автомобиль отвечает.

Теперь допустим, что я изменил внутреннюю деталь реализации моего автомобиля:

public Car {
    private Engine m_engine;
    private float m_currentGearRatio;
    public float getSpeed( return m_engine.getRpm*m_currentGearRatio; );
}

Все хорошо.Car следует правильным ОО-принципам, скрывая детали , как что-то делается.Это освобождает звонящего от решения его проблем, а не пытается понять, как работает автомобиль.Это также дает мне свободу изменять мою реализацию по своему усмотрению.

Но внедрение зависимостей вынудит меня выставить мой класс объекту Engine, который я не создал или инициализировал.Еще хуже то, что я теперь показал, что мой Car даже имеет двигатель:

public Car {
   public constructor(Engine engine);
   public float getSpeed();
}

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

public Car {
    private Gps m_gps;
    public float getSpeed( return m_gps.CurrentVelocity.Speed; )
}

без прерывания вызывающей стороны:

public Car {
   public constructor(Gps gps);
   public float getSpeed();
}

Но введение зависимости открывает целую банку с червями: открывая всю банку с червями.Внедрение зависимостей требует раскрытия всех деталей моей реализации private .Потребитель моего Car класса теперь должен понимать и иметь дело со всеми ранее скрытыми внутренними сложностями моего класса:

public Car {
   public constructor(
       Gps gps, 
       Engine engine, 
       Transmission transmission,
       Tire frontLeftTire, Tire frontRightTire, Tire rearLeftTire, Tire rearRightTire, 
       Seat driversSeat, Seat passengersSeat, Seat rearBenchSeat,
       SeatbeltPretensioner seatBeltPretensioner,
       Alternator alternator, 
       Distributor distributor,
       Chime chime,
       ECM computer,
       TireMonitoringSystem tireMonitor
       );
   public float getSpeed();
}

Как я могу использовать достоинства Inpendency Injection, чтобы помочь юнит-тестированию, не нарушая достоинства инкапсуляции для удобства использования?

См. также


Ради интереса я могу обрезать пример getSpeed до того, что нужно:

public Car {
   public constructor(
       Engine engine, 
       Transmission transmission,
       Tire frontLeftTire, Tire frontRightTire
       TireMonitoringSystem tireMonitor,
       UnitConverter unitsConverter
       );
   public float getSpeed()
   {
      float tireRpm = m_engine.CurrentRpm * 
              m_transmission.GetGearRatio( m_transmission.CurrentGear);

      float effectiveTireRadius = 
         (
            (m_frontLeftTire.RimSize + m_frontLeftTire.TireHeight / 25.4)
            +
            (m_frontRightTire.RimSize + m_frontRightTire.TireHeight / 25.4)
         ) / 2.0;

      //account for over/under inflated tires
      effectiveTireRadius = effectiveTireRadius * 
            ((m_tireMonitor.FrontLeftInflation + m_tireMontitor.FrontRightInflation) / 2.0);

      //speed in inches/minute
      float speed = tireRpm * effetiveTireRadius * 2 * Math.pi;

      //convert to mph
      return m_UnitConverter.InchesPerMinuteToMilesPerHour(speed);
   }
}

Обновление: Возможно, какой-нибудь ответ может последовать за вопросом и дать пример кода?

public Car {
    public float getSpeed();
}

Другой пример, когда мой класс зависит от другого объекта:

public Car {
    private float m_speed;
}

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

public Car {
    public Constructor(
        float speed,
        float weight,
        float wheelBase,
        float width,
        float length,
        float height,
        float headRoom,
        float legRoom,
        DateTime manufactureDate,
        DateTime designDate,
        DateTime carStarted,
        DateTime runningTime,
        Gps gps, 
        Engine engine, 
        Transmission transmission,
        Tire frontLeftTire, Tire frontRightTire, Tire rearLeftTire, Tire rearRightTire, 
        Seat driversSeat, Seat passengersSeat, Seat rearBenchSeat,
        SeatbeltPretensioner seatBeltPretensioner,
        Alternator alternator, 
        Distributor distributor,
        Chime chime,
        ECM computer,
        TireMonitoringSystem tireMonitor,
        ...
     }

Это действительно детали реализации, на которые я не хочу, чтобы клиент обращал внимание.

Ответы [ 9 ]

13 голосов
/ 29 сентября 2010

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

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

Самый простой - просто иметь конструктор по умолчанию, который выполняет внедрение со значениями по умолчанию. Пока вызывающий код использует только конструктор по умолчанию, вы можете изменять зависимости за кулисами, не влияя на вызывающий код.

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

Помимо этого, используется Inversion of Control. Я недостаточно использовал IoC, чтобы говорить об этом слишком много, но здесь есть множество вопросов, а также статей в Интернете, которые объясняют это гораздо лучше, чем я мог.

Если он должен быть действительно инкапсулирован там, где вызывающий код не может узнать о зависимостях, тогда есть возможность либо сделать инъекцию (либо конструктор с параметрами зависимости, либо установщиками) internal, если язык поддерживает это, или делает их закрытыми, и ваши юнит-тесты используют что-то вроде Reflection, если ваш язык поддерживает это. Если ваш язык не поддерживает ни то, ни другое, я полагаю, возможно, что класс, вызывающий код, создает экземпляр фиктивного класса, который просто инкапсулирует класс, который выполняет реальную работу (я считаю, что это шаблон Facade, но я никогда не помню правильно имена ):

public Car {
   private RealCar _car;
   public constructor(){ _car = new RealCar(new Engine) };
   public float getSpeed() { return _car.getSpeed(); }
}
7 голосов
/ 29 сентября 2010

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

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

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

И всегда есть подход инжектирования сеттера.Объект будет иметь разумные значения по умолчанию для своих свойств, которые при необходимости можно будет заменить на test-double.Однако я предпочитаю инъекцию в конструктор.


Изменить, чтобы показать пример кода:

interface ICar { float getSpeed(); }
interface ICarFactory { ICar CreateCar(); }

class Car : ICar { 
  private Engine _engine;
  private float _currentGearRatio;

  public constructor(Engine engine, float gearRatio){
    _engine = engine;
    _currentGearRatio = gearRatio;
  }
  public float getSpeed() { return return _engine.getRpm*_currentGearRatio; }
}

class CarFactory : ICarFactory {
  public ICar CreateCar() { ...inject real dependencies... }    
}

И затем классы-потребители просто взаимодействуют с ним через интерфейс, полностью скрывая любые конструкторы.

class CarUser {
  private ICarFactory _factory;

  public constructor(ICarFactory factory) { ... }

  void do_something_with_speed(){
   ICar car = _factory.CreateCar();

   float speed = car.getSpeed();

   //...do something else...
  }
}
3 голосов
/ 30 сентября 2010

Я думаю, что вы нарушаете инкапсуляцию с помощью вашего Car конструктора. В частности, вы диктуете, что Engine должен быть введен в Car вместо некоторого типа интерфейса, используемого для определения вашей скорости (IVelocity в приведенном ниже примере.)

С интерфейсом Car может получить текущую скорость независимо от того, что определяет эту скорость. Например:

public Interface IVelocity {
   public float getSpeed();
}

public class Car {
   private m_velocityObject;
   public constructor(IVelocity velocityObject) { 
       m_velocityObject = velocityObject; 
   }
   public float getSpeed() { return m_velocityObject.getSpeed(); }
}

public class Engine : IVelocity {
   private float m_rpm;
   private float m_currentGearRatio;
   public float getSpeed( return m_rpm * m_currentGearRatio; );
}

public class GPS : IVelocity {
    private float m_foo;
    private float m_bar;
    public float getSpeed( return m_foo * m_bar; ); 
}

Двигатель или GPS могут иметь несколько интерфейсов в зависимости от типа выполняемой работы. Интерфейс является ключом к DI, без него DI нарушает инкапсуляцию.

2 голосов
/ 29 сентября 2010

Фабрики и интерфейсы.

У вас есть пара вопросов здесь.

  1. Как я могу иметь несколько реализаций одних и тех же операций?
  2. КакМогу ли я скрыть подробности конструкции объекта от потребителя объекта?

Итак, вам нужно скрыть реальный код за интерфейсом ICar, создайте отдельный EnginelessCar, если выкогда-нибудь понадобится, и используйте интерфейс ICarFactory и класс CarFactory, чтобы скрыть детали конструкции от потребителя автомобиля.

Это, вероятно, в конечном итоге будет выглядеть как инфраструктура внедрения зависимостей, ноВы не должны использовать один.

Согласно моему ответу на другой вопрос, нарушает ли это инкапсуляцию или нет, полностью зависит от того, как вы определяете инкапсуляцию.Я видел два общих определения инкапсуляции:

  1. Все операции над логической сущностью представлены как члены класса, и потребителю класса не нужно больше ничего использовать.
  2. Класс несет единоличную ответственность, и код для управления этой ответственностью содержится внутри класса.То есть при кодировании класса вы можете эффективно изолировать его от его среды и уменьшить область действия кода, с которым вы работаете.

(код, подобный первому определению, может существовать в кодовой базе, котораяработает со вторым условием - оно, как правило, ограничивается фасадами, и эти фасады, как правило, имеют минимальную логику или не имеют ее).

2 голосов
/ 29 сентября 2010

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

http://components.symfony -project.org / dependency-инъекция / документация

есть раздел, посвященный контейнерам внедрения зависимостей.

Чтобы сделать его кратким и обобщить все, приведенные непосредственно на странице документации:

При использовании контейнера мы просто спрашиваемдля почтового объекта [это будет ваша машина в вашем примере], и нам больше не нужно ничего знать о том, как его создать;все знания о том, как создать экземпляр почтовой программы [car], теперь встроены в контейнер.

Надеемся, что это поможет вам

1 голос
/ 05 октября 2010

Вы должны разбить ваш код на две фазы:

  1. Построение графа объектов для определенного времени жизни с помощью решения фабрики или DI
  2. Запуск этих объектов (что потребует ввода ивыходной)

На автомобильном заводе им нужно знать, как построить автомобиль.Они знают, какой у него двигатель, как подключен гудок и т. Д. Это фаза 1 выше.Автомобильный завод может производить разные автомобили.

Когда вы ведете машину, вы можете управлять всем, что соответствует ожидаемому интерфейсу автомобиля.например педали, руль, клаксон.Когда вы за рулем, вы не знаете внутренних деталей, когда нажимаете на тормоз.Однако вы можете увидеть результат (изменение скорости).

Инкапсуляция сохраняется, поскольку никому за рулем автомобиля не нужно знать, как он был построен.Следовательно, вы можете использовать одного и того же водителя на разных автомобилях.Когда драйву нужна машина, им нужно дать один.Если они построят свои собственные, когда поймут, что нуждаются в них, инкапсуляция будет нарушена.

1 голос
/ 30 сентября 2010

Я не думаю, что автомобиль - это особенно хороший пример реальной полезности внедрения зависимостей.

Я думаю, что в случае с последним примером кода цель класса Car не ясна.,Это класс, который содержит данные / состояние?Это сервис для расчета таких вещей, как скорость?Или это микс, позволяющий вам построить его состояние и затем вызывать сервисы для него, чтобы производить расчеты на основе этого состояния?

На мой взгляд, сам класс Car, скорее всего, будет объектом с состоянием, чейцель состоит в том, чтобы хранить детали его состава, а сервис для расчета скорости (который может быть введен при желании) будет отдельным классом (с методом, подобным «getSpeed(ICar car)»).Многие разработчики, использующие DI, имеют тенденцию разделять объекты с состоянием и объекты службы - хотя в некоторых случаях объект будет иметь состояние и службу, большинство из них, как правило, разделяются.Кроме того, подавляющее большинство случаев использования DI, как правило, связано с обслуживанием.

Следующий вопрос: как составить класс автомобиля?Является ли намерение, что каждый конкретный автомобиль является просто экземпляром класса Car, или существует отдельный класс для каждой марки и модели, наследуемый от CarBase или ICar?Если это первое, тогда должно быть какое-то средство для установки / ввода этих значений в машину - нет никакого способа обойти это, даже если вы никогда не слышали об инверсии зависимости.Если это последнее, то эти значения являются просто частью автомобиля, и я не вижу причин, чтобы когда-либо захотеть сделать их устанавливаемыми / вводимыми.Все сводится к тому, являются ли такие вещи, как Engine и Tyres, специфичными для реализации (жесткие зависимости) или являются ли они компонуемыми (слабо связанные зависимости).

Я понимаю, что автомобиль - всего лишь пример, но в реальном миревы будете тем, кто знает, нарушает ли инвертирование зависимостей в ваших классах инкапсуляцию.Если да, то вопрос, который вы должны задать, это «почему?»а не "как?"(что вы, конечно, делаете).

1 голос
/ 29 сентября 2010

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

EDIT: Если вы используете каркас или нет, вы делаете то же самое, у вас есть фабрика, которая соединяет ваши объекты. Таким образом, ваши объекты предоставляют эти детали в конструкторе или в установщиках, но код вашего приложения (за пределами фабрики, не считая тестов) никогда не использует их. В любом случае вы предпочитаете получать свой объектный граф с фабрики, а не создавать экземпляры на лету, и вы решаете не делать таких вещей, как использование сеттеров в коде, в который нужно вставить. Это изменение сознания от философии «гвоздь все внизу», которую я вижу из кода некоторых людей.

0 голосов
/ 30 сентября 2010

Теперь, для чего-то совершенно другого ...

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

Однако у него есть серьезное ограничение: в каждом проекте вы можете иметь только один определенный класс Engine.Выбор движка выбора не возможен, хотя, если подумать, вы могли бы возиться со значением переменной мета-класса, чтобы достичь именно этого.Но я забегаю вперед.

Еще одним ограничением является единственная линия наследования: просто ствол, а не ветви.По крайней мере, в отношении модулей, включенных в один проект.

Похоже, вы используете Delphi, и поэтому приведенный ниже метод будет работать, так как это то, что мы использовали с D5 в проектах, которым требуется один экземпляркласса TBaseX, но разные проекты нуждаются в разных потомках этого базового класса, и мы хотим иметь возможность менять классы, просто выбрасывая один модуль и добавляя другой.Решение не ограничивается Delphi.Он будет работать с любым языком, который поддерживает виртуальные конструкторы и мета-классы.

Так что вам нужно?

Ну, каждый класс, который вы хотите иметь возможность менять местами, зависит от единиц, включенных в проект., где-то должна быть переменная, в которой вы можете сохранить тип класса для создания экземпляра:

var
  _EngineClass: TClass;

Каждый класс, который реализует Engine, должен зарегистрироваться в переменной _EngineClass, используя метод, который предотвращает место предков.потомка (чтобы избежать зависимости от порядка инициализации модуля):

procedure RegisterMetaClass(var aMetaClassVar: TClass; const aMetaClassToRegister: TClass);
begin
  if Assigned(aMetaClassVar) and aMetaClassVar.InheritsFrom(aMetaClassToRegister) then
    Exit;

  aMetaClassVar := aMetaClassToRegister;
end;

Регистрация классов может быть выполнена в общем базовом классе:

  TBaseEngine
  protected
    class procedure RegisterClass; 

class procedure TBaseEngine.RegisterClass;
begin
  RegisterMetaClass(_EngineClass, Self);
end;

Каждый потомок регистрируется самостоятельновызвав метод регистрации в разделе инициализации своего устройства:

type
  TConcreteEngine = class(TBaseEngine)
  ...
  end;

initialization
  TConcreteEngine.RegisterClass;

Теперь все, что вам нужно, - это создать экземпляр «самого потомственного» зарегистрированного класса вместо жестко закодированного конкретного класса.

  TBaseEngine
  public
    class function CreateRegisteredClass: TBaseEngine; 

class function TBaseEngine.CreateRegisteredClass: TBaseEngine;
begin
  Result := _EngineClass.Create;
end;

Конечно, теперь вы всегда должны использовать эту функцию класса для создания экземпляров движков, а не нормальный конструктор.

Если вы сделаете это, ваш код теперь всегда будет создавать экземпляр «самого потомка»класс двигателя присутствует в вашем проекте.И вы можете переключаться между классами, включая и не включая конкретные единицы.Например, вы можете убедиться, что ваши тестовые проекты используют фиктивные классы, сделав фиктивный класс предком реального класса и не включая фактический класс в тестовый проект;или сделав фиктивный класс потомком фактического класса и не включая его в ваш обычный код;или - еще проще - включив или макет или фактический класс в ваших проектах.


В классах Mock и реальных классах есть конструктор без параметров вэтот пример реализации.Не обязательно, но вам нужно будет использовать определенный мета-класс (вместо TClass) и некоторую приведение при вызове процедуры RegisterMetaClass из-за параметра var.

type
  TBaseEngine = class; // forward
  TEngineClass = class of TBaseEngine;
var
  _EngineClass: TEngineClass

type
  TBaseEngine = class
  protected
    class procedure RegisterClass;
  public
    class function CreateRegisteredClass(...): TBaseEngine;
    constructor Create(...); virtual;

  TConcreteEngine = class(TBaseEngine)
    ...
  end;

  TMockEngine = class(TBaseEngine)
    ...
  end;

class procedure TBaseEngine.RegisterClass;
begin
  RegisterMetaClass({var}TClass(_EngineClass), Self);
end;

class function TBaseEngine.CreateRegisteredClass(...): TBaseEngine;
begin
  Result := _EngineClass.Create(...);
end;

constructor TBaseEngine.Create(...);
begin
  // use parameters in creating an instance.
end;

Естьвесело!

...