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 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:
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.
The Dependency Inversion Principle (DIP) states that high level modules should not depend on low level modules; both should depend on abstractions. Abstractions should not depend on details.
In our example, CustomerBusinessLogic depends on the DataAccess class, so CustomerBusinessLogic is a high-level module and DataAccess is a low-level module. So, as per the first rule of DIP, CustomerBusinessLogic should not depend on the concrete DataAccess class, instead both classes should depend on abstraction.
Following two statements are core of the Dependency Inversion Principle(DIP): "High-level modules should not depend on low-level modules. Both should depend on abstractions." "Abstractions should not depend on details. Details should depend on abstractions."
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With