How to use Dependency Injection without breaking encapsulation?

前端 未结 9 593
囚心锁ツ
囚心锁ツ 2020-12-23 09:24

How can i perform dependency injection without breaking encapsulation?

Using a Dependency Injection example from Wikipedia:

public Car {
    public f         


        
相关标签:
9条回答
  • 2020-12-23 10:01

    You should break your code into two phases:

    1. Construction of the object graph for a particular lifetime via factory or DI solution
    2. Running these objects (which will involve input and output)

    At the car factory, they need to know how to build a car. They know what sort of engine it has, how the horn is wired in etc. This is phase 1 above. The car factory can build different cars.

    When you are driving the car, you can drive anything that meets the car interface you expect. e.g. pedals, steering wheel, horn. When you're driving you don't know the internal details when you press the brake. You can, however, see the result (change in speed).

    Encapsulation is maintained as no one driving a car needs to know how it was built. Therefore, you can use the same driver with many different cars. When the drive needs a car, they should be given one. If they build their own when they realise they need one, then encapsulation will be broken.

    0 讨论(0)
  • 2020-12-23 10:04

    Many of the other answers hint at it, but I'm going to more explicitly say that yes, naive implementations of dependency injection can break encapsulation.

    The key to avoiding this is that calling code should not directly instantiate the dependencies (if it doesn't care about them). This can be done in a number of ways.

    The simplest is simply have a default constructor that does the injecting with default values. As long as calling code is only using the default constructor you can change the dependencies behind the scenes without affecting calling code.

    This can start to get out of hand if your dependencies themselves have dependencies and so forth. At that point the Factory pattern could come into place (or you can use it from the get-go so that calling code is already using the factory). If you introduce the factory and don't want to break existing users of your code, you could always just call into the factory from your default constructor.

    Beyond that there's using Inversion of Control. I haven't used IoC enough to speak too much about it, but there's plenty of questions here on it as well as articles online that explain it much better than I could.

    If it should be truly encapsulated to where calling code cannot know about the dependencies then there's the option of either making the injecting (either the constructor with the dependency parameters or the setters) internal if the language supports it, or making them private and have your unit tests use something like Reflection if your language supports it. If you language supports neither then I suppose a possibility might be to have the class that calling code is instantiating a dummy class that just encapsulates the class the does the real work (I believe this is the Facade pattern, but I never remember the names correctly):

    public Car {
       private RealCar _car;
       public constructor(){ _car = new RealCar(new Engine) };
       public float getSpeed() { return _car.getSpeed(); }
    }
    
    0 讨论(0)
  • 2020-12-23 10:04

    If I understand your concerns correctly, you're trying to prevent any class that needs to instantiate a new Car object from having to inject all those dependencies manually.

    I've used a couple patterns to do this. In languages with constructor chaining, I've specified a default constructor that injects the concrete types into another, dependency-injected constructor. I think this is a pretty standard manual DI technique.

    Another approach I've used, which allows some looser coupling, is to create a factory object that will configure the DI'ed object with the appropriate dependencies. Then I inject this factory into any object that needs to "new" up some Cars at runtime; this allows you to inject completely faked Car implementations during your tests, too.

    And there's always the setter-injection approach. The object would have reasonable defaults for its properties, which could be replaced with test-doubles as needed. I do prefer constructor-injection, though.


    Edit to show a code example:

    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... }    
    }
    

    And then consumer classes just interact with it through the interface, completely hiding any constructors.

    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...
      }
    }
    
    0 讨论(0)
  • 2020-12-23 10:04

    I don't think a car is a particularly good example of the real world usefulness of dependency injection.

    I think in the case of your last code example, the purpose of the Car class is not clear. Is is a class that holds data/state? Is it a service to calculate things like speed? Or is it a mix, allowing you to construct its state and then call services on it to make calculations based on that state?

    The way I see it, the Car class itself would likely be a stateful object, whose purpose is to hold the details of its composition, and the service to calculate speed (which could be injected, if desired) would be a separate class (with a method like "getSpeed(ICar car)"). Many developers who use DI tend to separate stateful and service objects--although there are cases where an object will have both state and service, the majority tend to be separated. In addition, the vast majority of DI usage tends to be on the service side.

    The next question would be: how should the car class be composed? Is the intent that every specific car is just an instance of a Car class, or is there a separate class for each make and model that inherit from CarBase or ICar? If it's the former, then there must be some means of setting/injecting these values into the car--there is no way around this, even if you'd never heard of dependency inversion. If it's the latter, then these values are simply part of the car, and I would see no reason to ever want to make them settable/injectable. It comes down to whether things like Engine and Tires are specific to the implementation (hard dependencies) or if they are composable (loosely coupled dependencies).

    I understand the car is just an example, but in the real world you are going to be the one who knows whether inverting dependencies on your classes violates encapsulation. If it does, the question you should be asking is "why?" and not "how?" (which is what you are doing, of course).

    0 讨论(0)
  • 2020-12-23 10:11

    Factories and interfaces.

    You've got a couple of questions here.

    1. How can I have multiple implementations of the same operations?
    2. How can I hide construction details of an object from the consumer of an object?

    So, what you need is to hide the real code behind an ICar interface, create a separate EnginelessCar if you ever need one, and use an ICarFactory interface and a CarFactory class to hide the construction details from the consumer of the car.

    This will likely end up looking a lot like a dependency injection framework, but you do not have to use one.

    As per my answer in the other question, whether or not this breaks encapsulation depends entirely on how you define encapsulation. There are two common definitions of encapsulation that I've seen:

    1. All operations on a logical entity are exposed as class members, and a consumer of the class doesn't need to use anything else.
    2. A class has a single responsibility, and the code to manage that responsibility is contained within the class. That is, when coding the class, you can effectively isolate it from its environment and reduce the scope of the code you're working with.

    (Code like the first definition can exist in a codebase that works with the second condition - it just tends to be limited to facades, and those facades tend to have minimal or no logic).

    0 讨论(0)
  • 2020-12-23 10:12

    Now, for something completely different...

    You want the virtues of dependency injection without breaking encapsulation. A dependency injection framework will do that for you, but there is also a "poor man's dependency injection" available to you through some creative use of virtual constructors, meta class registration and selective inclusion of units in your projects.

    It does have a serious limitation though: you can only have a single specific Engine class in each project. There is no picking an choosing engine's, though come to think of it, you could probably mess with the value of the meta class variable to achieve just that. But I am getting ahead of myself.

    Another limitation is a single line of inheritance: just a trunk, no branches. At least with regard to the units included in a single project.

    You seem to be using Delphi and therefore the method below will work as it is something that we have been using since D5 in projects that need a single instance of class TBaseX, but different projects need different descendants of that base class and we want to be able to swap classes by simply chucking out one unit and adding another. The solution isn't restricted to Delphi though. It will work with any language that supports virtual constructors and meta classes.

    So what do you need?

    Well, every class that you want to be able to swap depending on units included per project, needs to have a variable somewhere in which you can store the class type to instantiate:

    var
      _EngineClass: TClass;
    

    Every class that implements an Engine should register itself in the _EngineClass variable using a method that prevents ancestors from taking the place of a descendant (so you can avoid dependence on unit initialisation order):

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

    Registration of the classes can be done in a common base class:

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

    Each descendant registers itself by calling the registration method in its unit's initialization section:

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

    Now all you need is something to instantiate the "descendent most" registered class instead of a hard coded specific class.

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

    Of course you should now always use this class function to instantiate engines and not the normal constructor.

    If you do that, your code will now always instantiate the "most descendant" engine class present in your project. And you can switch between classes by including and not including the specific units. For example you can ensure your test projects use the mock classes by making the mock class an ancestor of the actual class and not including the actual class in the test project; or by making the mock class a descendant of the actual class and not including the mock in your normal code; or - even simpler - by including either the mock or the actual class in your projects.


    Mock and actual classes have a parameter-less constructor in this implementation example. Doesn't need to be the case, but you will need to use a specific meta class (instead of TClass) and some casting in the call to the RegisterMetaClass procedure because of the var parameter.

    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;
    

    Have fun!

    0 讨论(0)
提交回复
热议问题