The problem of dependencies between software components or classes is a well known problem and also several well known solutions exist to solve this problem. Some popular approaches to solve the dependency problems are Designing by Contracts
, Dependency Injection
, Service Locator Pattern
, Autowiring(dependency injection by a third party service locator)
etc. But each of these approaches has its own pros and cons. And sometimes, the cons are responsible for adding new complexities to the program(of course, for bad decisions) that, the approach itself becomes more of a problem than solution.
Instantiator pattern tries to solve the dependency problem in such a way that it can overcome the limitation of each of the other approaches.
Let's dive into details:
- First I'll try to discuss a problem
- Then I'll try to explain what the limitations of each approaches
- After that, I'll explain what Instantiator pattern is and how it solves the problem
When designing softwares, we often break softwares into components or classes to wrap up similiar kinds or tightly coupled functionalities. Let's say, in our imaginary project, we have several such components or classes. Let's say, there is such a component which has the same functionlity as a third party component. As we want to develop our software as fast as possible, we decided to go with third party component for now, but later we'll replace it with our own implementation of that component. For discussions sake, let's define a consumer class for the thrid party component as follows(note: I'll be using php, but if you're familiar with any oop language, you should be fine):
// note: It's the easiest possible code, we could write to use the ThirdPartyComponent class. This is the first solution come to our mind, cause it's natural, just creating dependency as we go forward. No extra thinking!
// the following line is equivalent to java or python import
use Path\To\ComponentByThirdParty;
// class that dependents on component
class ComponentConsumer
{
public function usesComponent()
{
// ...
// ...
// other necessary codes
// here, $a is third party component's dependency
$component = new ComponentByThirdParty($a);
// ...
// ...
// some other codes
}
}
I am going to discuss about what is the problems with the above code and each existing solutions step by step. And also I'll show you, how each of the existing solutions tries to solve the problems and fails in one aspect or another.
The first problem with the above(natural) solution is the compatibility problem. If we want to replace ComponentByThirdParty
class with our own implementation, then both must have same public methods and also, the corresponding methods signatures must be same and perform same tasks.
To solve the issues with the first problem, we must enforce a contract that must be carried out by both ComponentByThirdParty
and ComponentByUs(let it be the name of our own implementation)
classes. But it's possible that, third party component has already gone out of the way. So, we can introduce an adapter class CompatibleComponentByThirdParty
, that conforms ComponentByThirdParty
to implement ComponentInterface(let it be the name of the interface that both should implement)
. So,
ComponentInterface sample implementation:
interface ComponentInterface
{
public function methodA(): void;
public function methodB(): RetClass;
// ...
// ...
// other methods
}
CompatibleComponentByThirdParty sample implementation:
use Path\To\ComponentByThirdParty;
class CompatibleComponentByThirdParty implements ComponentInterface
{
private $thirdPartyComponent;
// constructor
public function __construct($a)
{
$this->thirdPartyComponent = new ComponentByThirdParty($a);
}
public function methodA(): void
{
$this->thirdPartyComponent->someMethod();
}
public function methodB(): RetClass
{
// ...
$ret = $this->thirdPartyComponent->someOtherMethod();
// ...
return $ret;
}
// ...
// ...
// other methods
}
So, design by contract along with adapter pattern solves the first problem for us. We can now just replace CompatibleComponentByThirdParty
with ComponentByUs
to use our own implementation of component. But this is not the end. New problem arises.
Now, if we want to swap CompatibleComponentByThirdParty
with ComponentByUs
, then we've to go into the code and manually replace or edit CompatibleComponentByThirdParty
to ComponentByUs
. And also, we've to add import statement for ComponentByUs
. Now, this is a tedious task, and leads to several problems:
- Hardcoding dependencies into code
- We've to change all the places where
ComponentImplementedByThirdParty
has been instantiated. And this could be a lot of places distributed in a lot of files - Changing already written codes often introduces bugs
- (language specific) fixing imports
As, the first solution leads us to perform very tedius task to get our job done, it's not very useful for a large project where we've to deal with a lot of files. So, to solve the issue with the second problem, we can instantiate the objects using a service locator. So, its get very easy to replace one component with another. We can rewrite ComponentConsumer
as follows:
class ComponentConsumer
{
public function usesComponent()
{
// ...
$component = ServiceLocator::get(ComponentInterface::class, [ $a ]);
// ...
}
}
and service locator interface which is implemented by service locator could be as follows:
interface ServiceLocatorInterface
{
public static function add(string $className, Closure $closure): void;
public static function get(string $className, ...$args): mixed;
}
And one could map one interface to a concrete implementation as follows:
ServiceLocator::add(ComponentInterface::class, function($a) {
return new \Path\To\ComponentByThirdParty($a);
});
So, we can see that, ServiceLocator
gives us the flexibility we needed. We have totally isolated the object creation from our code. And, now we've only one place where the changes occurs. So, to replace ComponentByThirdParty
with ComponentByUs
can be done just by mapping ComponentInterface::class
to ComponentByUs
:
ServiceLocator::add(ComponentInterface::class, function($a) {
return new \Path\To\ComponentByUs($a); // note the changes
});
Pretty easy and fun huh! Now, just wait a bit. This also has problems. Let's discover them.
Service locator leads us to a path where testing becomes harder. When testing we often need an environment where we could easily inject dependencies(specially fake external dependencies like a fake database or fake file system) easily. But service locator does not provide the facility for us. We can't just inject a fake facility without changing service locator. And changing service locator back and forth for testing purposes, is not the appropriate solution.
Service locator often leads to runtime errors, cause object not intantiated directly, rather it is instantiated inside a method or closure later at runtime, which may lead to runtime error.
Let's look at folllowing code properly:
// here $a is a dependency
$component = ServiceLocator::get(ComponentInterface::class, [ $a ]);
We've to pass dependency $a
to properly construct the ComponentByUs
object, but we don't know the ComponentByUs
's constructor signature by looking at the above line. Many one uses IDE
for this purpose, so that they can instantly see what are the dependencies to construct an object or call a method. To construct an object that implements ComponentInterface
either we've to consult ComponentInterface
's documentation or go see it's implementation or we just have to remind all the dependencies. This problem may create distraction or cognitive load.
Now, dependency injection is really an elegant solution to solve the above problems. Rather then resolving dependencies inside the method, dependency injection says, let's just push the dependencies into the method. In this way, testing would be easier, and there would be no cognitive load(at least, for the writer of the method). So, we could write ComponentConsumer
as follows:
class ComponentConsumer
{
public function usesComponent(ComponentInterface $component)
{
// ...
// do you stuff with component
// ...
}
}
That's just pretty neat, huh! No worry about which component class we're using, third party or our, no worry about how to construct it, just great, the writer of the method is pretty happy! But hold on, there still should be someone who has to intantiate component object. So, who would do it? Let's go to our next problem.
So, someone has to be responsible for creating object that implements ComponentInterface
. Naturally, it becomes the responsibility of the consumer of the usesComponent
's method. So, let's say there is some code as bellow:
// $b, $c are dependencies for creating an object of class A
$a = new A($b, $c);
$component = new \Path\To\ComponentByUs($a);
$componentConsumer->usesComponent($component);
Now, let's examine the three lines of codes. For writing the three lines how much knowledge one should have. Well, (s)he has to know about dependency of A, know about dependency of ComponentByUs
and also (s)he has to know about dependency of the methods (s)he is calling of $componentConsumer
object. Now, if you ask me, I think that's a lot of knowledge one need just to write three lines of code. And I don't want that. The much more you have to know to write some code, the much more burden will be on you and the much more possibility of that error would occur.
Let's just assume, you have faced a very odd case, where class A
dependends of class B
and C
. B
depends on D
and E
. C
depends on F
. D
depends on G
. All dependencies are constructor dependencies. It's a mess right. Let's see it in a picture:
A
/ \
B C
/ \ \
D E F
/
G
So, to construct an object of A, we have to do as follows:
$c = new C(new F());
$b = new B(
new D(new G()),
new E()
);
$a = new A($b, $c);
Very bad design, huh! What is the odd that this could happen? Well, it's not about the odds that we should be worried about, rather, we should be worried about that, we're keeping a hole in our design procedure, that this could happen. In reality, it could lead to a much worse dependency hell.
Autowiring is just a hybrid of Service Locator
and Dependency Injection
pattern. Autowiring checks all the argument types in a method's signature and if the type is registered in Service Locator
, then if just creates the object and return. So, you can just assume that, if the above classes A, B, C, D, E, F, G
are just registered in the service locator, you don't have to worry about dependency handling, autowiring would do that for you. So, you just call ComponentConsumer::usesComponent
method as follows:
$componentConsumer->usesComponent();
You don't even have to be worried about creating any object that implements ComponentInterface
. Autowiring does that job for you.
Well, that's very charming. I think, it just solved all the problems. But does it? Well, try to remember Autowiring needs a Service Locator
. So, it comes with the problems of Service Locator
. And, we're doomed again.
We just observe that, we've actually found a lot of solutions for dependency handling, but each of them tries to solve the problem partially. And among all of them Dependency Injection
is the most usable solution, but one must be experienced to design Dependency Injection
properly, so that, (s)he may not create a dependency hell.
Okay, let's move forward and try to find a new solution.
Before going on describing Instantiator pattern, let's list all problems first to see what type of solution we need:
- The compatibility problem
- The replacement/edit problem
- The testing problem
- The runtime error
- The cognitive load
- The responsilibity and the diameter of knowledge complexity
- The dependency hell
So, what we've to do is just propose a way that could solve those problem provided above and we're done.
To solve the above described problems, I have proposed some simple rules, that would help us either avoid the problems or solve the problems. The rules are:
- Whenever possible, one should resolve it's own dependency rather than injected by others. This helps us eliminate dependency problem. And also keeps the diameter of knowledge as small as possible.
- The second idea depends on observation:
How often do we need to switch between implementation? 1. for testing, and 2. for switching production grade components
. So, one should be able to switch between components based on modes. This solves replacement/edit problem and testing problem. - Each instantiator will provide a client interface which will be used to instantiate object. This will help solve cognitive load.
- For solving compatibility, we'll use our first solution, a common contract.
- The only problem that remains is
runtime error
. As Instantiator is a lazy object generator pattern, we can not solve it directly, but we can easily solve it using asimple unit test
.
I am not great at generating diagrams, so I'll try to explain by words:
- Instantiator class would be an abstract class
- Instantiator class would be extended by other class for use
- Children of Instantiator class would provide two method: one is
register method
, other one is of their own choice for getting instance.register method
is an abstract method of Instantiator class, which the user must implement, it would provideclosures or factories that returns desired objects
. - Intantiator would contain an static variable
private static $container
to point to a container which would be used tocontain the closures or factories that produces proper objects
. - Instantiator class will have two states. A global state and a local state. Global state targets to "default" mode by default and local state would be set by
Child of Instantiator class
. If global state is missing, then loacl state would point to global state at that time. Instantiator would also have a fallback variable, which states that if a mode is missing, then the program should fallback to "default" mode or not. Generally, one should often let fallback to true.
And that's it.
Let's view a look at how a simple child of Instantiator class would look like:
class DatabaseInstantiator extends Instantiator
{
protected function register()
{
$this->instance([
"default" => function($p, $q, $r) {
return new \Path\To\Database($p, $q, $r);
},
"test" => function($p, $q, $r) {
return new \Path\To\FakeDatabase($p, $q, $r);
}
]);
}
// you can choose any name you want get, create etc..., but getInstance
// cause, getInstance method is provided by Instantiator and can't be overridden
public function get(P $p, Q $q, R $r): DatabaseInterface
{
return $this->getInstance($p, $q, $r);
}
}
When mode is set to "test", DatabaseInstantiator::get($p, $q, $r)
would return FakeDatabase
object and when in "default" mode, it would return Database
object.
If you want to define your DatabaseInstantiator
have a constructor, then you must call parent constructor as following:
class DatabaseInstantiator extends Instantiator
{
public function __construct($mode=null, $fallback=null)
{
parent::__construct($mode, $fallback);
// ...
// do other stuffs
}
// ...
// ...
// register and other methods you need
}
And the structure of Instantiator could be like:
abstract class Instantiator implements InstantiatorInterface
{
private static $globalMode;
private static $globalFallback;
private static $container;
// various setter, getter and access checker for
// global variables
// ...
// ...
// local properties and methods of instantiator
// local state variables
private $childMode;
private $childFallback;
public function __construct(?string $mode=null, ?bool $fallback=null)
{
$this->childMode = ($mode === null) ? self::globalMode() : $mode;
$this->childFallback = ($fallback === null) ? self::globalFallback() : $fallback;
}
// instance and singleton method implementation
protected function instance(array $factories): void
{
// ...
}
protected function singleton(array $factories): void
{
// ...
}
// `register` is an abstract method which must be implemented by
// user. It should be used to provide closures for instantiating
// objects for various modes
abstract protected function register();
// method to get and set local mode and fallback
public function setMode(string $mode): void
{
$this->childMode = $mode;
}
public function getMode(): string
{
return $this->childMode;
}
public function setFallback(bool $fallback): void
{
$this->fallback = $fallback;
}
public function getFallback(): bool
{
return $this->childFallback;
}
final protected function getInstance(...$args)
{
// method that accesses $container and return generated object
// this can not be overridden
}
}
One can use the given DatabaseInstantiator as follows:
// default mode
$dbInstantiator = new DatabaseInstantiator();
$db = $dbInstantiator->get($p, $q, $r);
// here is instance of Database, can be checked as follows
var_dump($db instanceof Database); // prints true
// and if one want to use "test", then
// mode="test", fallback=true
// cause, if "test" mode is not found, it'll use
// the "default" mode factory or closure
$dbInstantiator = new DatabaseInstantiator("test", true);
$db = $dbInstantiator->get($p, $q, $r);
// here is instance of Database, can be checked as follows
var_dump($db instanceof DatabaseFake); // prints true
I hope this gives an idea how Instantiator pattern works. For a full version of Instantiator implementation visit this repository.
If you know how to improve Intantiator pattern or make it much more easier to use then send a pull request. You can also contact me at [email protected] with your ideas. I would love to have a talk.