NoUML with Levels of Abstraction

Volodymyr Frolov
7 min readJan 9, 2019

--

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

Have you ever heard the phrase “Levels of Abstraction” [1] meaning that not all abstractions are created equal? In my previous article we discussed that abstractions have no inner structure, they are indivisible atoms, dots on a bigger diagram. So how is that possible that some of these dots could be more abstract than the others? Well, it’s not about abstractions per se. It’s all about domain knowledge and the context in which these abstractions are organized into bigger picture that we call Software Architecture. In one context an abstraction could be at the very heart of the Architecture and in the other context the same abstraction is just a dirty detail nobody cares about.

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

The concept of Levels of Abstraction is important because we want to protect high level abstractions from being disrupted by changes in lower levels [2]. So, we want volatile abstractions to depend on stable ones but not the other way around. And we know that the highest-level abstractions are immensely stable whereas the lowest-level abstractions are notoriously volatile.

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

Let’s say you have an architectural boundary but one of the relations is crossing it in the wrong direction:

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

The easiest way to implement Inversion of Control is to use some Dependency Injection framework. In that case Client is going to be autowired to Service and there’s going to be no abstraction knowing about them both. But strictly speaking it is not necessary as Client could just manually wire itself to Service. In that case uses relation from Client to Server is going to cross Architectural Boundary in the allowed direction.

Dependency Injection vs Inversion of Control

Dependency Injection frameworks are often called Inversion of Control Containers but I think this name is misleading.

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

Levels of Abstraction aren’t the only case where Architectural Boundary is useful. Think for example how to translate Use Case diagram [6] from UML to NoUML. You can split all abstractions there into two non-overlapping subgroups: Actors and Use Cases. Inside each of these subgroups relations are not restricted in any way and could be of different kinds, but between the subgroups we could have only “uses” relations going only in one direction from Actors to Use Cases.

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

Architectural Boundary is an important concept defining Software Architecture which deserves its own place in NoUML. In this article we have seen how Architectural Boundaries shape architecture of Software Systems.

References

[1] Kent Beck. Smalltalk Best Practice Patterns, 1st ed., 1996.
[2] Matthias Noback. Principles of Package Design: Creating Reusable Software Components, 1st ed., 2018, p. 217–249.
[3] Robert Martin. The Clean Architecture, 1st ed., 2012, p. 239.
[4] Martin Fowler. Inversion of Control, 2005.
[5] Martin Fowler, David Rice, Matt Foemmel. Patterns of Enterprise Application Architecture, 1st ed., 2003, p. 499.
[6] Grady Booch, James Rumbaugh, Ivar Jacobson. The Unified Modeling Language User Guide, 1st ed., 1998, p 225–238.

--

--