Dependency Inversion Principle (SOLID) vs Encapsul

2020-05-11 12:02发布

问题:

I was recently having a debate about the Dependency Inversion Principle, Inversion of Control and Dependency Injection. In relation to this topic we were debating whether these principles violate one of the pillars of OOP, namely Encapsulation.

My understanding of these things is:

  • The Dependency Inversion Principle implies that objects should depend upon abstractions, not concretions - this is the fundamental principle upon which the Inversion of Control pattern and Dependency Injection are implemented.
  • Inversion of Control is a pattern implementation of the Dependency Inversion Principle, where abstract dependencies replace concrete dependencies, allowing concretions of the dependency to be specified outside of the object.
  • Dependency Injection is a design pattern that implements Inversion of Control and provides dependency resolution. Injection occurs when a dependency is passed to a dependent component. In essence, the Dependency Injection pattern provides a mechanism for coupling dependency abstractions with concrete implementations.
  • Encapsulation is the process whereby data and functionality that is required by a higher level object is insulated away and inaccessible, thus, the programmer is unaware of how an object is implemented.

The debate got to a sticking point with the following statement:

IoC isn't OOP because it breaks Encapsulation

Personally, I think that the Dependency Inversion Principle and the Inversion of Control pattern should be observed religiously by all OOP developers - and I live by the following quote:

If there is (potentially) more than one way to skin a cat, then do not behave like there is only one.

Example 1:

class Program {
    void Main() {
        SkinCatWithKnife skinner = new SkinCatWithKnife ();
        skinner.SkinTheCat();
    }
}

Here we see an example of encapsulation. The programmer only has to call Main() and the cat will be skinned, but what if he wanted to skin the cat with, say a set of razor sharp teeth?

Example 2:

class Program {
    // Encapsulation
    ICatSkinner skinner;

    public Program(ICatSkinner skinner) {
        // Inversion of control
        this.skinner = skinner;
    }

    void Main() {
        this.skinner.SkinTheCat();
    }
}

... new Program(new SkinCatWithTeeth());
    // Dependency Injection

Here we observe the Dependency Inversion Principle and Inversion of Control since an abstract (ICatSkinner) is provided in order to allow concrete dependencies to be passed in by the programmer. At last, there is more than one way to skin a cat!

The quarrel here is; does this break encapsulation? technically one could argue that .SkinTheCat(); is still encapsulated away within the Main() method call, so the programmer is unaware of the behavior of this method, so I do not think this breaks encapsulation.

Delving a little deeper, I think that IoC containers break OOP because they use reflection, but I am not convinced that IoC breaks OOP, nor am I convinced that IoC breaks encapsulation. In fact I'd go as far as to say that:

Encapsulation and Inversion of Control coincide with each other happily, allowing programmers to pass in only the concretions of a dependency, whilst hiding away the overall implementation via encapsulation.

Questions:

  • Is IoC a direct implementation of the Dependency Inversion Principle?
  • Does IoC always break encapsulation, and therefore OOP?
  • Should IoC be used sparingly, religiously or appropriately?
  • What is the difference between IoC and an IoC container?

回答1:

Does IoC always break encapsulation, and therefore OOP?

No, these are hierarchically related concerns. Encapsulation is one of the most misunderstood concepts in OOP, but I think the relationship is best described via Abstract Data Types (ADTs). Essentially, an ADT is a general description of data and associated behaviour. This description is abstract; it omits implementation details. Instead, it describes an ADT in terms of pre- and post-conditions.

This is what Bertrand Meyer calls design by contract. You can read more about this seminal description of OOD in Object-Oriented Software Construction.

Objects are often described as data with behaviour. This means that an object without data isn't really an object. Thus, you have to get data into the object in some way.

You could, for example, pass data into an object via its constructor:

public class Foo
{
    private readonly int bar;

    public Foo(int bar)
    {
        this.bar = bar;
    }

    // Other members may use this.bar in various ways.
}

Another option is to use a setter function or property. I hope we can agree that so far, encapsulation is not violated.

What happens if we change bar from an integer to another concrete class?

public class Foo
{
    private readonly Bar bar;

    public Foo(Bar bar)
    {
        this.bar = bar;
    }

    // Other members may use this.bar in various ways.
}

The only difference compared to before is that bar is now an object, instead of a primitive. However, that's a false distinction, because in object-oriented design, an integer is also an object. It's only because of performance optimisations in various programming languages (Java, C#, etc.) that there's an actual difference between primitives (strings, integers, bools, etc.) and 'real' objects. From an OOD perspective, they're all alike. Strings have behaviours as well: you can turn them into all-upper-case, reverse them, etc.

Is encapsulation violated if Bar is a sealed/final, concrete class with only non-virtual members?

bar is only data with behaviour, just like an integer, but apart from that, there's no difference. So far, encapsulation isn't violated.

What happens if we allow Bar to have a single virtual member?

Is encapsulation broken by that?

Can we still express pre- and post-conditions about Foo, given that Bar has a single virtual member?

If Bar adheres to the Liskov Substitution Principle (LSP), it wouldn't make a difference. The LSP explicitly states that changing the behaviour mustn't change the correctness of the system. As long as that contract is fulfilled, encapsulation is still intact.

Thus, the LSP (one of the SOLID principles, of which the Dependency Inversion Principle is another) doesn't violate encapsulation; it describes a principle for maintaining encapsulation in the presence of polymorphism.

Does the conclusion change if Bar is an abstract base class? An interface?

No, it doesn't: those are just different degrees of polymorphism. Thus we could rename Bar to IBar (in order to suggest that it's an interface) and pass it into Foo as its data:

public class Foo
{
    private readonly IBar bar;

    public Foo(IBar bar)
    {
        this.bar = bar;
    }

    // Other members may use this.bar in various ways.
}

bar is just another polymorphic object, and as long as the LSP holds, encapsulation holds.

TL; DR

There's a reason SOLID is also known as the Principles of OOD. Encapsulation (i.e. design-by-contract) defines the ground rules. SOLID describes guidelines for following those rules.



回答2:

Is IoC a direct implementation of the Dependency Inversion Principle?

The two are related in a way that they talk about abstractions, but that's about it. Inversion of Control is:

a design in which custom-written portions of a computer program receive the flow of control from a generic, reusable library (source)

Inversion of control is allowing us to hook our custom code into the pipeline of a reusable library. In other words, Inversion control is about frameworks. A reusable library that does not apply Inversion of Control is merely a library. A framework is a reusable library that does apply Inversion of Control.

Do note that we as developers can only apply Inversion of Control if we are writing a framework ourselves; you can't apply inversion of control as an application developer. We can (and should) however apply Dependency Inversion Principle and the Dependency Injection pattern.

Does IoC always break encapsulation, and therefore OOP?

Since IoC is just about hooking into the pipeline of a framework, there is nothing that's leaking here. So the real question is: does Dependency Injection break encapsulation.

The answer to that question is: no, it does not. It doesn't break encapsulation because of two reasons:

  • Since the Dependency Inversion Principles states that we should program against an abstraction, a consumer will not be able to access the internals of the used implementation and that implementation will therefore not be breaking encapsulation to the client. The implementation might not even be known or accessible at compile time (because it lives in an unreferenced assembly) and the implementation can in that case not leak implementation details and break encapsulation.
  • Although the implementation accepts the dependencies it requires throughout its constructor, those dependencies will typically be stored in private fields and can't be accessed by anyone (even if a consumer depends directly on the concrete type) and it will therefore not break encapsulation.

Should IoC be used sparingly, religiously or appropriately?

Again, the question is "Should DIP and DI be used sparingly". In my opinion, the answer is: NO, you should actually use it throughout the application. Obviously, you should never apply things religiously. You should apply SOLID principles and the DIP is a crucial part of those principles. They will make your application more flexible and more maintainable and in most scenarios it is very appropriate to apply the SOLID principles.

What is the difference between IoC and an IoC container?

Dependency Injection is a pattern that can be applied either with or without a IoC container. An IoC container is merely a tool that can help building your object graph in a more convenient way, in case you have an application that applies the SOLID principles correctly. If you have an application that doesn't apply the SOLID principles, you will have a hard time using a IoC container. You will have a hard time applying Dependency Injection. Or let me put it more broadly, you will have a hard time maintaining your application anyway. But in no way an IoC container is a required tool. I'm developing and maintaining an IoC container for .NET, but I don't always use a container for all my applications. For the big BLOBAs (boring line of business applications) I often use a container, but for smaller apps (or windows services) I don't always use a container. But I do almost always use Dependency Injection as a pattern, because this is the most effective way to adhere to DIP.

Note: Since an IoC container helps us in applying the Dependency Injection pattern, "IoC container" is a terrible name for such library.

But despite of anything I said above, please note that:

in the real world of the software developer, usefulness trumps theory [from Robert C. Martin's Agile Principle, Patterns and Practices]

In other words, even if DI would break encapsulation, it wouldn't matter, because these techniques and patterns have proven to be very valuable, because it results in very flexible and maintainable systems. Practice trumps theory.



回答3:

Summing up the question:

We have the ability for a Service to instantiate its own dependencies.

Yet, we also have the ability for a Service to simply define abstractions, and require an application to know about the dependent abstractions, create concrete implementations, and pass them in.

And the question is not, "Why we do it?" (Because we know there is a huge list of why). But the question is, "Doesn't option 2 break encapsulation?"

My "pragmatic" answer

I think Mark is the best bet for any such answers, and as he says: No, encapsulation isn't what people think it is.

Encapsulation is hiding away implementation details of a service or abstraction. A Dependency isn't an implementation detail. If you think of a service as a contract, and its subsequent sub-service dependencies as sub-contracts (etc etc chained along), then you really just end up with one huge contract with addendums.

Imagine I'm a caller and I want to use a legal service to sue my boss. My application would have to know about a service that does so. That alone breaks the theory that knowing about the services/contracts required to accomplish my goal is false.

The argument there is... yeah, but I just want to hire a lawyer, I don't care about what books or services he uses. I'll get some random dood off the interwebz and not care about his implementation details... like so:

sub main() {
    LegalService legalService = new LegalService();

    legalService.SueMyManagerForBeingMean();
}

public class LegalService {
    public void SueMyManagerForBeingMean(){
        // Implementation Details.
    }
}

But it turns out, other services are required to get the job done, such as understanding workplace law. And also as it turns out... I am VERY Interested in the contracts that lawyer is signing under my name and the other stuff he's doing to steal my money. For example... Why the hell is this internet lawyer based in South Korea? How will THAT help me!?!? That isn't an implementation detail, that's part of a dependency chain of requirements I'm happy to manage.

sub main() {
    IWorkLawService understandWorkplaceLaw = new CaliforniaWorkplaceLawService();
    //IWorkLawService understandWorkplaceLaw = new NewYorkWorkplaceLawService();
    LegalService legalService = new LegalService(understandWorkplaceLaw);

    legalService.SueMyManagerForBeingMean();
}

public interface ILegalContract {
    void SueMyManagerForBeingMean();
}

public class LegalService : ILegalContract {
    private readonly IWorkLawService _workLawService;

    public LegalService(IWorkLawService workLawService) {
        this._workLawService = workLawService;
    }

    public void SueMyManagerForBeingMean() {
        //Implementation Detail
        _workLawService.DoSomething; // { implementation detail in there too }
    }
}

Now, all I know is that I have a contract which has other contracts which might have other contracts. I am very well responsible for those contracts, and not their implementation details. Though I am more than happy to sign those contracts with concretions that are relevant to my requirements. And again, I don't care about how those concretions do their jobs, as long as I know I have a binding contract that says we exchange information in some defined way.



回答4:

I will try to answer your questions, According to my understanding:

  • Is IoC a direct implementation of the Dependency Inversion Principle?

    we can't label IoC as the direct implementation of DIP , as DIP focuses on making higher level modules depending on the abstraction and not on the concretion of lower level modules. But rather IoC is an implementation of Dependency Injection.

  • Does IoC always break encapsulation, and therefore OOP?

    I don't think the mechanism of IoC will violate Encapsulation. But can make the system become Tightly coupled.

  • Should IoC be used sparingly, religiously or appropriately?

    IoC can be used as a in many patterns like Bridge Pattern, where seperating Concretion from Abstraction improves the code. Thus can be used in order to achieve DIP.

  • What is the difference between IoC and an IoC container?

    IoC is a mechanism of Dependency Inversion but containers are those which uses IoC.



回答5:

Encapsulation does not contradict with Dependency Inversion Principles in Object-Oriented Programming world. For example in a car design, you will have an 'internal engine' which will be encapsulated from outside world, and also 'wheels' that can be replaced easily, and considered as outside component of the car. The car has specification (interface) to rotate the shaft of the wheels, and the wheels component implements part that interact with the shaft.

Here, The internal engine represents the encapsulation process, while the wheel components represent the Dependency Inversion Principles (DIP) in the car design. With DIP, basically we prevent building a monolithic object, and instead we make our object composable. Can you image you build a car, where you cannot replace the wheels because they are built-in into the car.

Also you can read more about Dependency Inversion Principles in more details in my blog Here.



回答6:

I'm only going to answer one question as many other people have answered everything else. And keep in mind, there is no right or wrong answer, just user preferences.

Should IoC be used sparingly, religiously or appropriately? My experience leads me to believe that Dependency Injection should only be used on classes that are general and might need changing in the future. Using it religiously will lead to some classes needing 15 interfaces in the constructor which can get really time consuming. That tends to lead to 20% development and 80% house keeping.

Someone brought up an example of a car, and how the builder of the car will want to change the tires. Dependency injection allows one to change the tires without caring of the specific implementation details. But if we take dependency injection religiously... Then we need to start building interfaces to the constituents of the tires as well... Well, what about the threads of the tire? What about the stitching in those tires? What about the chemical in those threads? What about the atoms in those chemical? Etc... Okay! Hey! At some point you're going to have to say "enough is enough"! Let's not turn every little thing into an interface... Because that can get too time consuming. It's okay to have some classes be self contained in and instantiated in the class itself! It's faster to develop, and instantiating the class is a ton easier.

Just my 2 cents.



回答7:

I have found a case when ioc and dependency injection breaks encapsulation . Lets assume we have a ListUtil class . In that class there is a method called remove duplicates . This method accepts a List . There is an interface ISortAlgorith with a sort method . There is a class called QuickSort which implements this interface. When we write the alogorithm to remove duplicates we have to sort the list inside . Now if RemoveDuplicates allow an interface ISortAlgorithm as a parameter(IOC/Dependency Injection) to allow extensibility for others to choose an another algorithm for remove duplicate we are exposing the complexity of remove duplicate feature of the ListUtil class . Thus violating the foundation stone of Oops.