ClassA relies upon the interface only, delegating this responsibility of passing the
classB object elsewhere
This is the idea. If you are separating
ClassB by the use of an interface
ClassA) doesn't know
Then you do not want
ClassA to instantiate
ClassB. Instead it must recieve an object typed by the interface (for example, it can be received in the constructor).
How does this help encapsulation? You do not get encapsulation for free. However, given that
ClassA will only use
ClassB via its interface, you should define interfaces which are sufficient for that use. Ideally you would also remove any part of the interface that is not used. Then you will know exactly what
ClassB needs to expose. You still need to design a good interface.
however all it does is "pass the buck" elsewhere since someone, somewhere, must instantiate an implementation of ISomeInterface (such as
ClassB) and pass it as an object to
It has to happen somewhere. And is the thing that does this now coupling itself to both
Coupling is unavoidable. You are not trying to make it zero. You are trying to keep it low. Think of it as a cost you have to pay... too much coupling means that maintenance is hard. However, maintenance will never be a zero effort task.
I will continue on the assumption that we do dependency injection on the constructor.
You want to have a single source of truth of how to get a
ClassA object. That is, you do not want to call the constructor passing an object of
ClassB every time. You want to encapsulate that and reuse it.
If in the future you need to replace
ClassC, then there will be a single place where to change that. If there is some logic that picks
ClassC, there is a single place where that logic will be. If the instances need to be pooled, you can do that there too.
Thus, you will have a piece of code that has as responsibility to give you an object of
ClassA bound with the correct implementation of
ISomeInterface. And that is the only responsibility of that code. We could refer to that piece of code as a factory.
Note: Creating a factory for each class should not be a goal. You only need the ones you need.
Depending on what you are doing, it could make sense that in some places you want
ClassA created with
ClassB and in others you want
ClassA created with
ClassC. In fact, this could depend on external input.
For another example, let us say we have a video game, where there are some characters. Sometimes they are controlled by the local user, sometimes they are controlled by a player connected through the network, sometimes they are controlled by AI. In this case, we would have three valid implementations, and they could all co-exist.
For another example, it could be that we have a video game that can save progress to local storage, or to a cloud service that allows you to continue playing on a different machine. That is, we have two implementations to save progress, and which one we use depends on user input.
Where to put factories?
Yes, you can apply complex patterns to handle your factories. In fact, I'm saying the factory is a piece of code to keep it vague. However, let me point this out: If you are making a library... unless your goal is to make a library dedicated to factory patterns, let the developer of the application decide what factory patterns to use, by not providing any.
Exactly where you want to put your factory code will depend on what kind of application you have. For simple console and desktop projects, you can do all the dependency injecting in
main. If you are doing something more complex (a network server, for example), you might want some factory solution. Usually, you want to use it after deciding what code will handle input, and after the dependency injection is done, the code does not need to know what factory solution you used.
By factory solution I mean service locators, IoC containers… factory methods, abstract factories… all that stuff. Whatever patterns you pick. If you pick a library that does it… guess what? It is a dependency! Muahahaha. So, yeah, after the dependency injection is done, you do not want any code to be aware of it.
Abstracting things away is good™. In fact, it happens naturally by the single responsibility principle. By the way, I think you mean “single responsibility principle” instead of separation of concerns.
I believe that when they tell you that a layer can only interact with lower layers via an interface, they not abstract interface types, but an interface as in API.
However, we could be mixing all the layer talk with the concept of abstracting external systems…
The software engineer needs to make an executive decision about the scope of the project, which decides what is part of the system and what is external.
Draw a line. An architectural line. On the left of the line you code that deals with external systems. Which includes writing adapters which abstract the external system. That code must be interchangeable, allowing you to easily swap it in the future if needs be.
Note: I am not suggesting to wrap every dependency in adapters. Only external systems. So, no, I'm saying that you need to make an adapter for every library you use, nor the ridiculous idea of isolating from the runtime. While it is true that those would have their own reasons to change, you follow the single responsibility principle and that is enough. This is about external systems as in IO. Also, I'm saying that you decide what is external.
Therefore, you do not want the code on the right to depend on code on the left (which could have to change because of external reasons). However, the code on the left can depend on code on the right (which is under your control).
Thus, you will have the adapters (which are on the left) implement an interface (that exists on the right), and the rest of the code on the right of the architectural line will depend on the interface only…
However, somebody has to instantiate the adapter. Where does that code go? It goes to the left of the architectural line. Because, remember, the code to the right cannot depend on the code on the left. Thus the code that instantiates the adapter (which is to the left), must be to the left.
Now, surprise: the operating system is an external system.
main is a method that handles an event from the operating system (namely the start of the program). And since the operating system is external,
main goes on the left of the line, and thus it can instantiate the adapters, and it can call code on the right. Well, actually it would call the factory.
Similarly, if you have a form and event handlers the event handlers are on the left. Or if you have web server, the router is on the left. You want that code (
main, event handlers, router) to call the factory, inject the dependencies, and let the execution flow to the right.
On the flip side. Your code on the right might want to call the external systems. Remember that this is possible via the interfaces. I want to point out that there is another way to do this. Since the flow of execution starts on the left, it is often possible to let the right return, then the left can execute more code.
What I’m describing here? The idea of the architectural line comes from Clean Code.
I am taking inspiration from the idea of “functional core, imperative shell”: pure functions can only call pure functions. However impure functions can call pure and impure functions (pure to the right, impure to the left).
Bonus chatter: You might be aware that
async code has a tendency to propagate. Because
async methods can call synchronous methods and
async methods. However synchronous cannot await
async methods. However, you can use the architectural line to keep it on the left (synchronous to the right,
async to the left).
Why do I say left and right instead of talking of higher and lower layers? Because I would treat both the database and the user interface as external systems. However, classic tier architecture will say that the database is a lower layer and the UI is a higher layer.
Also, the communication between internal layers does not have to be held to the same standards. Providing a concrete facade or allowing a “higher” layer to instantiate types of the lower one is acceptable inside the system. Although, sometimes you want to draw more lines.