A Software Design Pattern which brings the principles of Inheritance, Encapsulation and Polymorphism one level up from the Class level to the Subsystem level, and offers a way of realizing relationships between classes so as to achieve dependency inversion by means of propagation instead of injection.
Part 1: Dependency Inversion
The software that we write often invokes other software to get parts of the job done. These are known as Services or Dependencies. If Class A is making use of some Class B, then Class A depends on Class B, so Class B is a dependency of Class A.
The principle of Dependency Inversion (⬀) says that a class should not contain any direct calls to specific instances of any of its dependencies. Instead, it should receive these instances as parameters during initialization.
That's all very nice, but passing dependencies around can become quite a complicated business, and in large systems it can become a nightmare.
Service Locators
- A service locator is a mandatory global dependency. That's a bad thing to have. At some point you will want to reuse a module in a different system, and that service locator will not be available there, and you will have to start rewriting stuff. Trust me, you will sooner or later regret having it.
- A service locator may defer compile-time errors to run-time errors. These errors occur when a system is being wired together, but tests are usually wired up differently, so these errors cannot be detected with unit testing or integration testing, you have to do end-to-end testing in order to discover them.
Dependency Injection Frameworks
- They work by magic, and I don't like magic.
- They tend to embrace silent failure, while I mandate hard failure.
- They don't have an API that you can call, so you cannot use code completion, you have to know stuff by heart.
- They tend to make application startup time slow as molasses, while I like application startup to be snappy.
- They are also a mandatory global dependency.
- The individual class is a much too fine-grained unit to be applying dependency injection onto.
In my 35 years of programming I have encountered the problem of dependency injection a lot, and in the last decade or so I have started solving it with a paradigm that I call Domain Oriented Programming.
Note that Domain Oriented Programming does not have any direct relation to Domain Driven Design (⬀), although it may be a suitable pattern to use when implementing systems designed using the Domain Driven Design paradigm.
Note that Domain Oriented Programming does not have any direct relation to Domain Driven Design (⬀), although it may be a suitable pattern to use when implementing systems designed using the Domain Driven Design paradigm.
Introducing Domain Oriented Programming (DOP)
The Domain Oriented Programming Design Pattern can be roughly described as follows:
- Classes do not exist in a vacuum; instead, every class has a special relationship with another class by which it is instantiated and from which it obtains its dependencies. The class doing the instantiation and providing dependencies is called Domain, the instantiated class is called Subject. Sometimes a class can be Subject to multiple Domains, more on that later.
- Every Subject has specific knowledge of its Domain.
- In some cases Domains also have specific knowledge of their Subjects, and in some cases they do not, more on that later.
- A Domain contains references to all services that are used by itself and by all of its Subjects; so, when a Subject needs to use some service, it obtains the service from its Domain. Therefore, dependencies do not need to be injected into Subjects.
- The Domain-Subject relation is hierarchical, so a Subject of one Domain may in turn be Domain to other Subjects. This way, dependencies are propagated from the root of a system all the way down to the leaf nodes without the need to use any special framework to achieve this.
The domain-subject relation can exist in two forms: Closed (a.k.a. Realm) and Open (a.k.a. Free).
- Closed (a.k.a. Realm) Domains
- The Domain is the one and only Domain for its Subjects. It is passed to each Subject as its first constructor parameter.
- The Domain has complete control over the lifetime of its Subjects. This means that the Domain is the exclusive factory of its Subjects, and can also decide when and if a Subject is destroyed.
- The Domain and its Subjects are Closely Coupled. This means that not only the Subjects have specific knowledge of their Domain, but also the Domain has specific knowledge of its Subjects. (Close coupling is perfectly okay as long as the Domain limits itself to acting as a factory and the Realm is kept small.)
- Subjects are usually exposed to the outside world as interfaces rather than as objects.
- The Realm forms a coherent, closed group which cannot be extended without modifying the Domain class.
- Open (a.k.a. Free) Domains
- The Domain does not have specific knowledge of any Subjects, it only exists for the purpose of making dependencies available to other Domains.
- Open Domains are usually provided as interfaces rather than as actual objects.
- A Subject of Open Domains can be freely instantiated as long as all the domains necessary for its instantiation are available. It can also be freely disposed.
There are a few interesting things to notice here:
The Domain is to a Subject what the Object is to a Method. Hopefully a DOP oriented language will be introduced one day which realizes the DOP construct in its grammar, making the Domain reference implicit, just as in Object Oriented Programming the Object reference is always the implicit first parameter to every Method.
(Incidentally, Java and other languages are already doing something along these lines with non-static inner classes (⬀), but we do not want to have to nest the source code of each Subject within the source code of its Domain, especially since a Domain may in turn be Subject of another Domain.)
Domain Oriented Programming does not require any platform or library: it is just a way of structuring code. So, with DOP, no omnipresent framework is needed for injecting dependencies, and no magic is involved in their propagation; nobody needs to query any service locators for services, (the availability of services is practically guaranteed by the compiler,) and no huge lists of dependencies are passed to constructors, either. Still, at various places where domains are constructed and wired together, all necessary services are supplied, so any one of them can be replaced with a Test Double (⬀).
Furthermore, Domain Oriented Programming is interoperable with non-DOP systems: A group of classes making use of DOP among themselves can be introduced into a system which is already using some other mechanism of Dependency Inversion.
At first glance, Domain Oriented Programming can be thought of as employing something like Half-Way Dependency Injection, or Subsystem-level Dependency Injection as opposed to Class-level Dependency Injection. Dependencies are injected into the Domain, and from that moment on Subjects of the Domain can go ahead and fetch their dependencies from the Domain as needed, instead of having their dependencies injected into them.
Things become even more interesting when we consider Domains that are in turn Subjects of other Domains, forming a hierarchy of Domains, where at each level we have SuperDomains and SubDomains. In this scenario, we do not exactly have Dependency Injection going on anymore, because at each level dependencies are obtained from the level above; however, we still have Dependency Inversion, because dependencies are still not hard-coded in any way, and each Domain has control over each service that it makes available to its subjects, and may, if needed, decide which particular implementation will offer it.
The lesson to learn from this is that Dependency Injection was never a goal in and of itself; the goal has been Dependency Inversion, (avoiding hard-coded dependencies, Dependency Independence if you will permit the pun,) and Dependency Injection has been a mechanism for achieving it, but the same goal can be achieved by other means, such as Dependency Propagation, which is what Domain Oriented Programming offers.
The lesson to learn from this is that Dependency Injection was never a goal in and of itself; the goal has been Dependency Inversion, (avoiding hard-coded dependencies, Dependency Independence if you will permit the pun,) and Dependency Injection has been a mechanism for achieving it, but the same goal can be achieved by other means, such as Dependency Propagation, which is what Domain Oriented Programming offers.
Part 2: Object Orientation at the Subsystem Level
Domain Oriented Programming is not only about Dependency Propagation. It reflects the realization that Software being created today is immensely more complex than what it used to be almost half a century ago, when Object Oriented Programming was invented and the first Object Oriented languages were laid down.
It used to be that all we needed was a means of coupling groups of functions with the data that they operate on, and that Inheritance, Encapsulation and Polymorphism were only necessary at the class-and-method level; however, as we build more elaborate software, we find ourselves more and more thinking not so much in terms of classes and methods, but in terms of subsystems and classes, or systems and subsystems. Therefore, there appears to be a need for terminology which brings Inheritance, Encapsulation and Polymorphism one level up, to the subsystem level, and by recursive application, to the entire system.
Domain Oriented Programming offers the Domain as the unit upon which to apply the principles of Object Oriented Programming.
- In DOP the Domain is the principal polymorphic unit, providing an implementation for a complex interface, and instantiating subjects to polymorphically implement smaller scope interfaces.
- In DOP the Domain encapsulates its subjects, hiding from the outside world their nature and lifetime.
- In DOP inheritance is only utilized (if needed) among Subjects, while the Domain hides this fact from the outside world, thus perfectly realizing Josh Bloch's maxim "Design for inheritance or else prohibit it".
My first public mention of this concept was in this answer of mine on Software Engineering Stack Exchange.
No comments:
Post a Comment