Stability for software components: the Stable Dependency Principle
In the previous article we saw the Acyclic Dependency Principle and we talked a lot about the concept of "Shy Programming". Instead, in this article we will extend the concept of stability to the software components domain and make some considerations on it that will take us to the Stable Dependency Principle (SDP) enounced for the first time by Robert Martin (1.). But before talking about concepts and principles I would like to see with you a practical programming technique (as the Shy Programming seen in the last article) that helps us to minimize the coupling between objects, this is the "Event Driven Programming"(2.).
Let's try to get to the concept of event in the software domain.
The first thing we are taught is not to write a program as a single big chunk, but to separate a program into modules. Each module has its own responsibilities. In fact, a good definition of a module (or class) is that it has only one well-defined responsibility (SRP)(3.). But once we separate a program into different modules based on responsibility, we have a new bunch of problems: how do the objects talk to each other at runtime ?
How do we manage the logical dependencies between them? That is, how do we synchronize changes in state (or updates to data values) in these different objects? It has to be done in a clean and flexible way so that we don't want them to know too much about each other.
We will start with the concept of an event. An event is simply a special message that says "something interesting has just happened". We can use events to signal an object changes that another object might be interested in.
Using events in this way minimizes coupling between those objects, the sender of the event does not need to have any explicit knowledge of the receiver.
However, we must be careful with the use of events. For example, we don't have to make a single procedure that receives and manages all the events for a particular application, like the old “Window Procedure” of many frameworks (MFC, QT, etc..). Not exactly the way for easy maintenance or evolution. Why is it bad to push all the events through a single routine?
When one routine is to have intimate knowledge of the interactions among many objects, encapsulation is being violated. Coupling is increased too, while we should decrease it.
You have probably seen this type of code, it is usually dominated by a huge case statement or multiway if-then.?We can do better. Objects should be able to register in order to receive only the events they need, and they should never be sent events they don't need. We don't want to spam our objects! Instead, we can use a publish/subscribe pattern, if we are interested in certain events generated by a Publisher, all we have to do is register ourselves. The Publisher keeps track of all ?interested Subscriber objects; when the Publisher generates an event of interest, it will call each Subscriber in turn and notify them that the event has occurred.
In order to explain this technique, I want to show you a real implementation in C ++ of the Publish-Subscribe pattern that I created with the consultant Carlo Pescio (4.) for a software system in a Linux environment that managed a line of equipments for cardiovascular exercise. You will see a variation to the theme, we have replaced the "Publisher" and "Subscriber" concepts with those of "EventSource" and "EventSink", terms more suitable for cardiovascular equipment domain, but it is the same thing. Our implementation is also totally thread-safe. The implementation goes back a few years ago and you will see that "auto_ptr" had been used. Today they are deprecated in the face of "unique_ptr". The replacement does not involve large changes, in Figure 1 we see the class diagram of the "EventHandling" pattern and in Figure 2 we see the sequence diagram relating to the task of subscribing to events by an "eventsink" and notification of an event asserted by an "eventsource" and managed by the "eventsink".
Figure 1: EventHandling class diagram
Figure 2: EventHandling sequence diagram
Event is the base class for all the concrete events. Each event is distinguished by a tag (a number). Event is an abstract class: Clone is pure virtual. Clone() as in the Prototype pattern creates (and returns) a clone of the object on which it is called. Must be redefined in each derived class, at each inheritance level.
SimpleEvent is a simple event class, having no data other than the tag. Useful whenever the event has no need to carry additional data. Clone() creates and returns a clone of the object.
EventSink is the base class for all the event receivers (and handlers). When the EventSink is notified of an event, it stores a clone of the incoming event into a private queue. Later, derived classes can extract the event using the GetEvent member function. The queue allows the notification of the event and the subsequent handling to happen in different threads. Also, the class maintains a dispatching map for events. Derived classes can register event handling member functions (using SubscribeEvent). In that way, the HandleFirstEvent() function will call the proper event handler, without any need for switch/case statements and explicit casts. So derived classes writers are encouraged to use the SubscribeEvent/HandleFirstEvent idiom, instead of registering events directly with the EventSource and popping them from the event queue manually. Please note that the two approaches are, within the same derived class, incompatible. Different derived classes, each one using one of the approaches, can coexist without problems on the same system. Notify(Event) clones the parameter and stores it in the EventQueue. Please note that access to the queue is protected by a critical section. GetEvent() : auto_ptr<Event> returns the first event in the EventQueue (FIFO access). If no events are present, a NULL pointer is returned. Access to the queue is protected by a critical section. The caller is ultimately responsible for the destruction of the received event. Therefore, this function returns an auto_ptr to the event, that will take care of destroying the event when it goes out of scope. No manual destruction is therefore necessary. HandleFirstEvent() to be called from derived classes, within their own thread. Extracts the first event (if any) from the event queue, and invokes (through the DispMap) the appropriate handling method. SubscribeEvent<T1,T2>(EventSource, Tag, void (T1::*f)(const T2*)) this function registers the sink it has been invoked on as a listener for the specified event from the specified source (this is no different than calling the Subscribe function of the source). However, the function has also another important responsibility: a new TypeSafeInvoker is created and stored in the DispMap of the sink, to allow for type-safe, switch/case-free handling of events. Since the type of the concrete class (derived from EventSink) that will handle the event, as well as the type of the concrete event expected from the handling member function, can assume any value, this member function is a template with two parameters.
EventQueue a simple FIFO queue containing pointers to Events. EventQueue does not provide synchronization. Access from different thread must be dealt with from the calling site.
DispMap an instance of a standard library class, maintains an association between event tags and the corresponding Invoker (actually a TypeSafeInvoker) that has been demanded to (asynchronously) call the handling function for that event.
Invoker <<Interface>> a polymorphic base class for the TypeSafeInvoker instances. Defines the common interface through the (pure virtual) InvokeHandler method, and provides a common class to store objects of derived classes into a single queue.
TypeSafeInvoker provides type-safe handling of an event (previously stored into an EventQueue). Stores a pointer to an handling object (usually of class derived from EventSink) and a pointer to a member function to be called on that object to handle the request. Since both the class of the handler and the class of the parameter of the handling member function can have any type, a template class with two parameters is needed. InvokeHandler calls the member function of the handler that has been designated as the handler of the event (identified by the tag). In order to accomplish the call in a type-safe way, the InvokeHandler function casts (with type checking if supported by the platform) the passed event from Event* to T2*, since the?member function to be called wants a pointer to a T2 object. Also InvokeHandler function calls the "handlingFunction" member of the "handler" object, passing the obtained value as parameter.
EventSource is the base class for all the sources of Events. The EventSource maintains a map of all the subscribed events, from all the event sinks. The sinks subscribe to an event using the Subscribe function. Before destruction, they must also unsubscribe using the UnSubscribe function. To send an event to all the subscribers, the EventSource (or an object of derived class, or in some cases another calling class) uses the Dispatch function. Subscribe(Tag, EventSink) add the pair (tag, EventSink) to the (multi)map of the subscribed events. Whenever an event with the specified tag is dispatched, the specified sink will be notified (see EventSink). Dispatch(Event) send the event to all the event sinks that have subscribed its tag. The sending happens through a call to EventSink::Notify (see EventSink for details). UnSubscribe(EventSink) removes the event sink from the dispatching map. This function must be called before the event sink is destroyed. Otherwise, an invalid pointer will remain in the map, with undefined run-time behaviour (most likely a crash).
Tag a tag is, in the current release, just an unsigned int (32 bit). For example in our implementation the range has been partitioned as follows:
0 - 800,000,000 are reserved for the infrastructure
800,000,001 - 1,000,000,000 are reserved for the Portable Memory
1,000,000,001 - 2,000,000,000 are reserved for the Equipment
2,000,000,001 - 3,000,000,000 are reserved for the Training Program
3,000,000,001 - 4,000,000,000 are reserved for the GUI
4,000,000,001 - MAX_UINT are reserved for the infrastructure
Here is the link to all sources of this framework. I also add an interesting unit test. The unit-test is related to an eventsource that simulates being the GUI of a cardiovascular equipment. Then there are two active EventSink (i.e. they are also threads) that simulate a Training Program and a VirtualTrainer. The unit test is called BetterEvent_Test. The project was built in Visual Studio.
Here are some code snippets related to this framework for "Event Handling Programming".
Figure 3 : Tag, Event, SimpleEvent.
Figure 4 : EventSink.
Figure 5 : EventSource, Invoker.
It also exists a IPC version of this framework made by my former colleague Alessandro Antenucci (IPC means Inter Process Communication). It is a very interesting extension of EventHandling framework because Alessandro used very elegantly the "Linux Eventbus System" in order to allow it to work also with EventSource and EventSink located in different processes. I spoke to Alessandro who told me that he will soon publish an article about it. When the article will be ready I will put the link on my newsletter.
A few years ago, again with Antenucci we also created an Android version of the EventHandler framework written in Java.
But now is the time to talk about principles. The Stable Dependency Principle (DSP) states:
"The dependencies between components in a design should be in the direction of stability of the components. A package should only depend upon packages that are more stable than it is".
But what is meant by “Stability”?
领英推荐
A component is STABLE if it is difficult to modify because many other components depend on it. A component that has many dependencies is very stable because it takes a lot of work to adapt a change with all the components that depend on it. Instead a component that nobody else depends on it is very unstable -> X is stable, Y is instable (see Fig 6).
Fig 6: component stability -> X is stable, Y is instable.
Every system has?stable?components and?volatile?components. Stable components are components that aren't expected to change that often. They either:
Because of these reasons, we'll more frequently write code that depends on stable components. Volatile components are those that are most likely to change. This is why:
There's nothing wrong with volatile components, every system has them and that's perfectly fine. But it's important to know when a component is volatile and ensure that we don't make stable components depend on them.
Any component that we expect to be volatile should not be depended on by a component that is difficult to change. Otherwise, the volatile component will also be difficult to change. It is the perversity of software that a module that you have designed to be easy to change can be made difficult to change by someone else who simply has a dependency on it. Not a line of source code in this module need change, yet this module will suddenly become more challenging to change. By conforming to the Stable Dependencies Principle (SDP), we ensure that modules that are intended to be easy to change are not depended on by modules that are harder to change.
We can make an analogy with Adults and Adolescents. A component on which many other components depend is as an Adults. Responsibility implies stability, not free to change instead "Irresponsibility" implies instability, so free to change. A component that depends on many other components is "Dependent", so is as a "Adolescent". "Independence" implies stability instead "Dependence" implies instability.
How can we measure the stability of a component? One way is to count the number of dependencies that enter and leave that?component.?These counts will allow us to calculate the stability of the component.
? Fan-in: Incoming dependencies. This metric identifies the number of classes outside this component that depend on classes within the component.
? Fan-out: Outgoing dependencies. This metric identifies the number of classes inside this component that depend on classes outside the component.
? I: Instability:
This metric has the range [0, 1].
I = 0 indicates a maximally stable component. I = 1 indicates a maximally unstable component.
The Fan-in and Fan-out metrics1 are calculated by counting the number of classes outside the component in question that have dependencies with the classes inside the component in question.
Let's consider the component diagram of Figure 7. We want to calculate the stability of the component Cc.
Fig 7: component diagram
We find that there are three classes outside Cc that depend on classes in Cc. Thus, Fan-in = 3. Moreover, there is one class outside Cc that classes in Cc depend on. Thus, Fanout = 1 and I = 1/4. When the I metric is equal to 1, it means that no other component depends on this component (Fan-in = 0), and this component depends on other components (Fan-out > 0). This situation is as unstable as a component can get; it is irresponsible and dependent. Its lack of dependents gives the component no reason not to change, and the components that it depends on may give it ample reason to change. In contrast, when the I metric is equal to 0, it means that the component is depended on by other components (Fan-in > 0), but does not itself depend on any other components (Fan-out = 0). Such a component is responsible and independent. It is as stable as it can get. Its dependents make it hard to change the component, and its has no dependencies that might force it to change. The SDP says that the I metric of a component should be larger than the I metrics of the components that it depends on. That is, I metrics should decrease in the direction of dependency.
If all components of a system were maximally stable, the system would be unchangeable. This is not a desirable situation. We want to design our component structure so that some components are unstable and others are stable. The modifiable components are at the top and depend on the stable components at the bottom. Let's consider the component diagram of Figure 8.
Fig 8: component diagram with stable and instable components.
Let us now see a situation of violation of the SDP Principle. For example let's consider the case that we are working in the component named Stable, we added a module that we have designed so that it is easy to change (FLEXIBLE). The U class of Stable use the C class of Flexible. This violates the SDP because the I metric for Stable is much smaller than the I metric for Flexible. As a result, Flexible will no longer be easy to change. A change to Flexible will force us to deal with Stable and all its dependents. To fix this problem, we somehow have to break the dependence of Stable on Flexible.
We can fix this by using DIP (Dependency Inversion Principle)(5.). We create an interface class called UC and put it in a component named NewComponent. We make sure that this interface declares all the methods that U needs to use. We then make C implement this interface as shown in Figure 9. This breaks the dependency of Stable on Flexible, and forces both components to depend on NewComponent. NewComponent is very stable (I = 0), and Flexible retains its necessary instability (I = 1). All the dependencies now flow in the direction of decreasing I.
Fig 9: SDP violation and solution.
Conclusion
Software quality has always been challenging to measure. However, using I metric is a huge help toward that. In the next article we will see a mathematical model of software quality that give us hints and guidance in order to have a more stable software : we will see the metric called "D-metric".?
I remind you my previous article:
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.?Andrew Hunt, David Thomas, “Pragmatic Programmer” Addison Wesley (25 November 2019) p 120-122.
3. S.Santilli:"https://www.dhirubhai.net/pulse/single-responsibility-principle-stefano-santilli/".?
4. C. Pescio: "https://eptacom.net/".
5. S.Santilli:"https://www.dhirubhai.net/pulse/dependency-inversion-principle-stefano-santilli/".?
Software Engineer, ML | ex-VMware
9 个月I feel like the following sentence towards the end of the article is misleading: "This violates the SDP because the I metric for Stable is much smaller than the I metric for Flexible". According to my calculations for Fig. 9 the 'I' metric of Stable is '1/3' and the 'I' metric for Flexible is '0' which contradicts the statement in question. I would appreciate any clarifications if I've missed something.