Подтвердить что ты не робот

Как использовать инъекцию зависимостей без нарушения инкапсуляции?

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

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

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 выполняет правильные OO-принципы, скрывая детали того, как что-то делается. Это освобождает вызывающего абонента для решения его проблем, а не пытается понять, как работает автомобиль. Это также дает мне свободу изменить мою реализацию, поскольку я считаю нужным.

Но инъекция зависимостей заставит меня подвергнуть мой класс объекту 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();
}

Но инъекция зависимости открывает целую банку червей: открывая всю банку червей. Инъекция зависимостей требует, чтобы все детали личной реализации моих объектов были открыты. Потребитель моего класса 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();
}

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

См. также


Ради удовольствия я могу обрезать пример 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,
        ...
     }

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

4b9b3361

Ответ 1

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

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

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

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

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

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

public Car {
   private RealCar _car;
   public constructor(){ _car = new RealCar(new Engine) };
   public float getSpeed() { return _car.getSpeed(); }
}

Ответ 2

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

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

Еще один подход, который я использовал, который позволяет использовать более слабое соединение, заключается в создании объекта factory, который будет настраивать объект DI'ed с соответствующими зависимостями. Затем я вставляю этот factory в любой объект, который должен "обновлять" некоторые автомобили во время выполнения; это также позволяет вам вводить полностью фальшивые версии автомобилей во время тестов.

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


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

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

Я думаю, что вы нарушаете инкапсуляцию с помощью своего конструктора 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 не нарушает инкапсуляцию.

Ответ 4

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

http://components.symfony-project.org/dependency-injection/documentation

имеется раздел контейнеров для инъекций зависимостей.

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

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

Надеюсь, что это поможет вам.

Ответ 5

Заводы и интерфейсы.

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

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

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

Это, скорее всего, очень похоже на структуру инъекций зависимостей, но вам не нужно ее использовать.

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

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

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

Ответ 6

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

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

Ответ 7

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

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

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

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

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

Ответ 8

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

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

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

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

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

Ответ 9

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

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

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

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

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

Итак, что вам нужно?

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

var
  _EngineClass: TClass;

Каждый класс, который реализует движок, должен регистрироваться в переменной _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 предком фактического класса и не включая фактический класс в тестовом проекте; или путем создания класса mock потомком фактического класса и не считая макета в вашем обычном коде; или - еще проще - включив или в 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;

Удачи!