NoUML with Levels of Abstraction

In this article we will discuss how to define and enforce Architectural Boundaries in Software Systems. You will also see the difference between Dependency Injection and Inversion of Control.

This is my second article in NoUML series. If you haven’t read the first one yet you should do it now as this article uses all the notations and intuition developed there.

Levels of Abstraction

Let me give you an example. Let’s say you are an architect of an accounting system. There will be abstractions representing Debit and Credit at the center of your Architecture. Most probably there also will be abstractions representing ORM (object-relational mapping) but these are just implementation details related to data persistence. One day if you decide to change relational database to NoSQL these ORM-related abstractions would be gone, but your system is going to be just fine. Contrarily, if the very concept of money goes out of humanity’s favor your system has no reason to exist any longer together with Debit and Credit abstractions.

Now let’s imagine you’re architecting your own ORM framework (I hope you aren’t though). ORM abstractions are going to play the key role in your architecture whereas Accounting, Debit and Credit abstractions could just be a showcase familiar to everyone, the dirtiest for all the details. Now it’s the other way around. Your system has no reason to exist without relational databases, but it is going to be just fine without accounting. You would easily be able to find another showcase without changing anything in your architecture.

Architectural Boundaries

In practice it means that we want to forbid any relations going from higher level to lower level abstractions. We want to split our abstractions into subgroups and control how these subgroups relate to each other. It doesn’t really matter how you call these subgroups of abstractions in your system: components, layers, services, microservices or whatever. These subgroups are architecturally significant only if they provide you the required level of isolation between abstractions [3].

You can also think of these subgroups of abstractions as of being abstractions themselves. You can pretend that you don’t know anything about their inner structure and draw them on higher-order NoUML diagrams.

In NoUML we draw Architectural Boundaries as solid lines separating abstractions. Here’s how Robert Martin’s Clean Architecture would look like:

higher-order NoUML diagram

As you can see on this diagram, all of the relations including transitive ones are crossing architectural boundaries in one direction.

Inversion of Control

Architectural Boundary is violated

In this example Execution Control flows from Service to Client and the relation between these two abstractions points in the same direction.

Control Flow is shown in blue on this diagram but it is not a relation in NoUML sense.

Control Flow is not transitive. If there are some scenarios where control flows from A to B and other scenarios where it flows from B to C it doesn’t necessarily mean that there exist any scenarios where control flows from A to C.

Control Flow is also not composable with any relations. If A uses B and control flows from B to C it doesn’t mean that control flows from A to C or that A uses C.

You can always reverse direction of any relation using Inversion of Control [4] trick without changing direction of Execution Control.

Architectural Boundary is restored

Architectural Boundary is restored on this diagram. Execution Control still flows from Service to Client but now Service doesn’t know anything about Client. It rather knows about some interface residing on the same side of Architectural Boundary so that notify relation doesn’t cross the Architectural Boundary. Client implements this interface and that’s how Service can pass Execution Control to it. Control flow crosses the Architectural Boundary in opposite direction from all the relations but that’s fine because control flow itself is not a relation.

Please note that there’s no good way to show “is” relation as a Venn diagram here, when the relation’s ends reside on opposite sides of Architectural Boundary. In this case we just draw an arrow and we label this arrow with the relation’s kind.

Dependency Injection

Dependency Injection vs Inversion of Control

First of all, Dependency Injection is not the only way to implement Inversion of Control. Another famous example would be Plugin Architecture [5]. Let’s say we have some Engine and its behavior could be extended by Plugins. All Plugins implement an interface exposed by Engine thus for every Plugin there exists “is” relation from that Plugin to Engine contents. Yet Execution Control flows in the opposite direction, from Engine to Plugins. Thus, Plugin Architecture is an example of Inversion of Control without Dependency Injection.

Also, Dependency Injection does not necessarily invert Execution Control. Consider the following example where Client uses some Singleton. The Singleton doesn’t have any interface so Client knows everything about it. Yet, the instance of that Singleton is created and managed by Dependency Injection framework. Client doesn’t have to call new or getInstance, Dependency Injection framework just takes that responsibility.

Dependency Injection without Inversion of Control

Both “uses” relation and control flow between Client and Singleton go in the same direction. Thus, on this diagram we have an example of Dependency Injection without Inversion of Control.

Use Case diagram

That’s another example of Architectural Boundary splitting Use Cases from Actors. In this case not only we restrict the direction of relations crossing this boundary but we also restrict kind of such relations.

Use Case diagram in NoUML

You can easily convince yourself that if all the explicit relations crossing this boundary are “uses” and going in the right direction then all of the transitive implicit relations crossing it are also “uses” going the same way.

This wouldn’t be possible if we wanted to have only “is” relations crossing the boundary as they are typically coming together with implicit “has” and “uses” relations.

Conclusions

References

Software Architect