In modern object-oriented languages, inheritance is massively used with its pros and cons. Moreover, languages such as Java offer simple inheritance but also allow classes to implement an arbitrary number of interfaces. With inheritance and interface implementation comes one additional ingredient that is naturally expected: method overriding. When a software evolves, you end up with hierarchies involving multiple classes and interfaces with methods definitions and implementations and then, the classes that are part of this hierarchy will be used by some other classes. In this context, it is difficult if not impossible to have control by hand over the usages or overriding classes of methods we would be interested in. Hereafter, I will present this problem in more detail with a very concrete and yet complex enough example, as well as some tools that can empower software architects and developers to gain more control over their code.
Let’s start by introducing the following class diagram of a tax system which describes the situation mentioned above on a very small scale:
This class diagram shows a very simplistic domain of taxable elements within a system including a “getTaxDiscountRate” operation for IResident which is overridden by some of the classes in the hierarchy and used from some of the controllers, one calling the method defined in IResident, and other two using instances of the Student and TemporaryResident where there are no explicit implementations of the method. But even with such a simple model, there are some questions that are not easy to answer unless you use the right tool, e.g:
- How many direct usages does the getTaxDiscountRate() method in IResident have?
- How many usages from implementing classes does the getTaxDiscountRate() method in IResident have?
- Which are the potential usages of the getTaxDiscountRate() method overridden in the Worker class?
- Which classes are providing implementations for the getTaxDiscountRate() method in IResident and overriding it from Resident?
At this point, you may have realized that you might not need a tool to answer these questions in this particular problem, but to put things in perspective, imagine a similar scenario with a hierarchy of more than 20 classes/interfaces involved and multiple classes whose methods make heavy use of the method of interest. It will require a great deal of patience and mostly a great deal of time to answer these questions accurately.
To overcome these difficulties, Sonargraph introduced some special dependencies in version 9.3.0 to achieve the following.
Follow the exact path from a method called using a subtype reference to the top-most implementation of the method:
As you can see in this Exploration view screenshot, there is a usage from StudentsController.calculateTaxToPay to Student.getTaxDiscountRate() (green arc), which refers to the implementation of this method in the Resident class. Nevertheless, Sonargraph creates a “Virtual Method Call [Via Subtype]” (gray arcs) for every step in the hierarchy until reaching the actual implementation of the method that is being called.
It is worth to mention that the reason why the “Via Subtype” dependencies are grayed is because they are not taken into account in the architecture check since they are not user-defined.
Find out where the definition of an inherited, but not overridden, method actually exists:
The student class does not override getTaxDiscountRate() method, which is used from the StudentsController class, however, Sonargraph creates a “Member Definition Provided By” dependency from the inherited method to the actual implementation in the Resident class. These dependencies are not taken into account by the architecture check either.
Find out all classes overriding a method:
If a class overrides a method from a supertype, Sonargraph will create an “Overrides” dependency from the method overriding to the method overridden. These dependencies are not taken into account by the architecture check either.
Now that the theoretical aspect has been treated, is time to explore how these newly introduced dependencies in the Sonargraph model can provide valuable information for software architects and developers by answering the questions that were raised at the beginning of these post. For that purpose, I have developed two Groovy scripts using the Sonargraph Groovy Script API; one that calculates the usages of a method and another one that calculates the overriding hierarchy of a method. At this point, one may wonder why to dedicate a blog post and two scripts to provide a functionality that many IDEs offer out of the box? Well, you will be surprised to know (or may have experienced) that some of the major IDEs out there fail to complete these tasks accurately, meaning that you often miss a piece of information or get false positives in the results.
FindUsages Script (direct usages, through subclasses, through superclasses):
The idea behind this script is simple and can be implemented in three steps:
- Collect the direct usages of the method. This is achieved by collecting the calls done by using a reference of the class or interface containing the method implementation or declaration. E.g. Given A.method(), and ‘a’ an instance of A, a.method() is a direct usage.
- Collect the usages of the method done by using references to subclasses, implementing classes or subinterfaces. E.g. Given method A.method(), class B extends A and ‘b’ an instance of B, b.method() is a usage of A.method() using a reference to a subclass of A.
- Collect the usages of the method done by using a reference to superclasses or implemented interfaces. This is a special case, since a call to an overridden method by using a reference of a class that is higher in the hierarchy than the object of interest can only be labeled as a potential usage. E.g. Given I.method(), A implements I and ‘i’ an instance of an object that implements I, i.method() is a potential usage of A.method().
Overrides script (all subclasses overriding a method in a superclass/superinterface):
This is a much simpler script counting on the “Overrides” dependency that Sonargraph 9.3.0 now calculates. For any given method, calculating all incoming “Overrides” dependencies recursively will show all implementations of the method.
It is worth mentioning that the newly added dependencies open a lot of possibilities for developers and architects to gain more control over their code. For example, it would be possible to write a Groovy script that detects precisely methods that do not have any usages at all and creates issues for them to be later removed.
In the following link, SampleTaxSystemV02, you will find a directory containing two directories:
- sampleTaxSystem: Java project ready to be imported to Eclipse or IntelliJ. It contains the source code created from the UML class diagram shown at the beginning of this post.
- SampleTaxSystem.sonargraph: Sonargraph software system for the java code. This folder contains the two Groovy scripts that were explained in the content of this post under the Scripts directory.
- The sample source code is not production-ready and is not intended ever to be. It is just a small piece of sample code for demonstration purposes.
- You must use Sonargraph version 18.104.22.1685 or above in order to make use of the explained dependency functionality.