The Stable Abstractions Principle.

The Stable Abstractions Principle.

In the previous article we saw the Stable Dependency Principle and we talked a lot about the concept of "Event Driven Programming". Instead in this article we will see how to define the concept of abstraction also for the components and we will calculate a metric, the A-metric. With A-metric and I-metric (seen in the previous article) we will create a software quality mathematical model that gives us hints and guidance in order to have a more stable software. All these considerations are summed up again by a Robert Martin principle: the "Stable Abstraction Principle" (1.).

But, as usual, before talking about concepts and principles I would like to see with you a practical programming technique that helps us to minimize the coupling between objects, this is the?"Workflow Pattern". It is a higher order pattern, that can be found neither among the ones catalogued by the Gang of Four?(2.) nor among?Grag Larman?minimal patterns?(3.). Instead it is almost an architectural solution (like MVC Pattern). I will present to you a couple of realizations of Workflow Pattern. But what is this pattern useful for ? "Workflow Pattern" allows us to manage a particular type of coupling : the "Temporal Coupling".

Time is an often ignored aspect of software architectures. Instead we have to talk about the role of time as a design element of the software itself. There are two aspects of time that are important to us: concurrency (things happening at the same time) and ordering (the relative positions of things in time).?We don't usually approach programming with either of these aspects in mind. When we design an architecture or write a program, we always tend to think linearly and that is: "Method A must always be called before method B", "it is possible to execute only one task at a time", "it is necessary to wait for the screen to be redrawn first to receive the click of the button". This approach is not very flexible and not very realistic. We need to allow for concurrency and to think about decoupling any time or order dependencies. In doing so, we can gain flexibility and reduce any time-based dependencies.

On many projects, we need to model and analyze the users' workflows as part of requirements analysis. We would like to find out what can happen at the same time, and what must happen in a strict order. One way to do this is to capture their description of workflow using a notation such as the UML activity diagram (4.). An activity diagram consists of a set of actions drawn as rounded boxes. The arrow leaving an action leads to either another action (which can start once the first action completes) or to a thick line called a synchronization bar. Once all the actions leading into a synchronization bar are complete, we can then proceed along any arrows leaving the bar. We can use activity diagrams to maximize parallelism by identifying activities that could be performed in parallel. For instance look at the workflow represented in the activity diagram of figure 1.

Non è stato fornito nessun testo alternativo per questa immagine

Figure 1 : a complex activity diagram.

The one represented above is a rather complex workflow. After a first action ("Action 0") we have a parallel ("Parallel 0") of two other parallels ("Parallel 1" and "Parallel 2"). "Parallel 1" simultaneously executes three different sequences composed of synchronous, asynchronous and predicate actions ("Sequence 1", "Sequence 2" and "Sequence 3"). "Parallel 2", on the other hand, consists of two sequences of actions that are also composed differently ("Action 8" and simultaneously evaluating a predicate that leads to "Action 9" in the True branch and to "Action 10" in the False branch). When all the sequences of "Parallel 1" end then "Parallel 1" also ends. Same thing for "Parallel 2" which ends only when both of its two sequences end. Only when both "Parallel 1" and "Parallel 2" terminate is the last "Final Action" executed and then the workflow ends.

With the consultant Carlo Pescio (5.) we have created a framework written in Java language that allows us to create complex workflows in code such as the one just seen. The workflow scheduling is performed on a Thread ("WorkflowThread") where the actions are evaluated and their concurrency or sequencing is managed. Actions can be both synchronous and asynchronous, the framework provides the basic abstractions for the implementation of concrete actions.

Our goal was to create a sort of very vertical mini-language on the domain of Workflows. And with our framework, we have partially succeeded. Below is a code snippet that shows how the code of the complex workflow in figure 1 is very expressive.

Non è stato fornito nessun testo alternativo per questa immagine

Figure 2 : source code of workflow.

I share with you the link of the repository where you can download the framework sources and the test application related to the Activity Diagram in figure 1. I created the project with IntelliJ IDEA 2022.1.1 - Community Edition.

Then, in agreement with the consultant Carlo Pescio, we decided to extend the functionality of this framework so that it could also be allowed to create workflows nested on different processes. In particular, we made an implementation for the Android world. The reason for this decision was motivated by the difficulty we found in managing the cooperation of the various processes of a software system at the bootup of the operating system (it was the software of cardiovascular training equipment).

In addition to the clarity and simplicity of the workflow code, our framework also allows a high level of observability on the execution of action flows and their cooperation between different processes, making it easier to analyze and debug the system.

In figure 3 we have an activity diagram shared between two different processes. We can also see it as two different workflows (on two different processes) of which the second is nested to the first. The "StartNestedWorkflowAction" action of the first workflow in process A launches the second wokflow in process B. When the workflow of process B ends, ?an event is sent to "WaitNestedWorkflowTermination" action of the first workflow which is then unlocked.

Non è stato fornito nessun testo alternativo per questa immagine

Figure 3 : activity diagram shared between two different processes.

Figures 4 and 5 show the workflow code of process A and the workflow code of process B, respectively.

Non è stato fornito nessun testo alternativo per questa immagine

Figure 4 : source code of workflow related to Process A.

Non è stato fornito nessun testo alternativo per questa immagine

Figure 5 : source code of workflow related to Process B.

I share the "Workflow Nested Framework" repository with you. This is a project that I made with Android Studio and consists of three modules:

-) "workflowlib" is a module which contains the framework with the workflow engine and with the basic abstractions for realization of different types of concrete actions.

-) "app" is the application that realize the main workflow (figure 4)

-) "app2" is the application that realize the nested workflow (figure 5)

I finish the discussion on this "mini workflow language" by telling you that there is also a more complete version of this framework where we added some new statements to the "workflow engine", such the cycle ("SequenceForever") . Then with the statements "sequence", "parallel", "condition" and "loop", defined on the "Actions Flow", you could model the structure of the whole application. I can't share this extension for now, but I invite you to try to develop it yourself starting from the source code of the repository below.

And now it's finally time to talk about the last, but not least, principle of component stability. The Stable Abstraction Principle (SAP).

In the previous article we saw the SDP principle which already allowed us to define the degree of stability of a component. So where does the need for a further principle on component stability come from? In this regard, let's try to answer the following question : "Where do we put the Business Rules?".

Some components in the software system should not change very often. This component represents high-level architecture and policy decisions (Business Rules). We don’t want these business and architectural decisions to be volatile. Thus the software that encapsulates the high-level policies of the system should be placed into stable components (I = 0). Instead unstable components (I = 1) should contain only the software that is volatile, the software that we want to be able to quickly and easily change.

However, if the high-level policies are placed into stable components, then the source code that represents those policies will be difficult to change. This could make the architecture inflexible. How can a component that is maximally stable (I = 0) be flexible enough to resist to change? The answer is found in the "Open Closed Principle" (6.).?

This principle tells us that it is possible and desirable to create classes that are flexible enough to be extended without requiring modification. Which kind of classes conform to this principle? "The abstract classes".

So here is what Martin states in his Stable Abstraction Principle:

"A component should be as abstract as it is stable".

"Packages that are maximally stable should be maximally abstract. Instable packages should be concrete. The abstraction of a package should be in proportion to its stability".

But what do we mean by the abstractness of a component and how can we measure it?

The abstractness of a package is?calculated?by the number of abstract entities (interface and abstract classes) it contains versus the number of concrete entities (classes). This metric is called the "A-metric" and its value is simply the ratio of interfaces and abstract classes in a component to the total number of classes in the component.

? Nc: The number of classes in the component.

? Na: The number of abstract classes and interfaces in the component.

? A: Abstractness. A = Na ÷ Nc.

The A-metric ranges from 0 to 1. A value of 0 implies that the component has no abstract classes at all. A value of 1 implies that the component contains only abstract classes.

The reason that a stable package should be abstract is so that it can be extended. If a package is stable, then it shouldn’t change often, but if we want to introduce new behavior without changing it, how is that done? By creating abstract packages, we can extend the abstractions to create new behaviors. Conforming to SAP leads to stable components containing many abstract classes, and instable components containing many concrete classes. So where do we put the high-level policy?

We can define the relationship between stability (I) and abstractness (A). To do so, we create a graph with "A" on the vertical axis and "I" on the horizontal axis (Figure 6).

Non è stato fornito nessun testo alternativo per questa immagine

Figure 6 : the Stability (I) and Abstractness (A) graph.

The components that are maximally stable and abstract are at the upper left at (0, 1). The components that are maximally unstable and concrete are at the lower right at (1, 0).

Obviously not all components fall into these two categories because components often have degrees of abstraction and stability. We can identify the following areas of interest :

Zone of pain : contains very concrete components. These components are undesirables because are rigids. They cannot be extended and are very difficult to modify. They are not a well designed components (eg. database schemas, concrete service library : what happens if we have to change the String class?). If a component that could change fall in this area is a??trouble.

Uselessness Zone : contains abstract classes but which have no dependencies. They are useless classes. This is usually rubble code, unused code.

Main sequence : most volatile components should be located as far as possible from the pain and uselessness zone. This area is the main sequence. A component that sits on the Main Sequence is not “too abstract” for its stability, nor is it “too unstable” for its abstractness. It is neither useless nor particularly painful. The most desirable position for a component is at one of the two endpoints of the Main Sequence. We have to try to place most of the components in those end points. However, some fractions of the components in a large system are neither perfectly abstract nor perfectly stable. These components have the best characteristics when they are close to the main sequence. We can create a metric that measures how far away a component is from this ideal.

? D: Distance. D = |A+I–1| . The range of this metric is [0, 1]. A value of 0 indicates that the component is directly on the Main Sequence. A value of 1 indicates that the component is as far away as possible from the Main Sequence. if we calculate the value of D for each component of our software system we obtain a scatterplot like the one in figure 7A.

Non è stato fornito nessun testo alternativo per questa immagine

Figure 7 : A) Scatterplot of the components B) Plot of D for a component over time.

Any component that has a D value that is not near zero can be reexamined and restructured. We can also calculate the mean and variance of all the D metrics for the components within a design. We would expect a conforming design to have a mean and variance that are close to zero. The variance can be used to establish “control limits” so as to identify components that are “exceptional” in comparison to all the others.?Another way to use the metrics is to plot the D metric of each component over time, as can be seen from the graph of figure 7B. We can see that some strange dependencies have been introduced into the component in the latest releases. The graph shows a control threshold at D = 0.1. The release R2.1 has exceeded this control limit, so we should find out why this component is so far from the main sequence.

In conclusion, we can say that the metric D measures the conformity of a design to a good model of dependence and abstraction. However, let us always remember that a metric is never infallible; it is simply a measure against an arbitrary standard. Therefore the evaluation of the metric must always be accompanied by careful considerations driven by our experience.

With this article we have concluded the series dedicated to Martin's Principles on Components, from the next article we will start talking about patterns, considered as good solutions to problems that commonly occur in software design.

I remind you my newsletter "Sw Design & Clean Architecture" : https://lnkd.in/eUzYBuEX where you can find my previous articles and where you can register, if you have not already done so, to be notified every time I publish new articles.


thanks for reading my article, and I hope you have found the topic useful,

Feel free to leave any feedback.

Your feedback is very appreciated.

?Thanks again.

Stefano

?

?References:

1.?C. Martin, “Clean Architecture - a craftsman's guide to software structure and design” Prentice-Hall (November 2018) p 133-139.

2. Gamma, Helm, Johnson, Vlissides, “Design Patterns” Addison Wesley (2° Edition October 2002).

3. Craig Larman, “Applying UML and Patterns” Prentice-Hall ( November 2004).

4. Carlo Pescio, "UML manuale di stile": "https://www.eptacom.net/pubblicazioni/umlstile/umlstile.pdf"

5.?Carlo Pescio: "https://eptacom.net/".

6.?S. Santilli: "https://www.dhirubhai.net/pulse/open-closed-principle-stefano-santilli/".?

要查看或添加评论,请登录

社区洞察

其他会员也浏览了