Expressing architectural constraints for a Java system using Glamorous Toolkit

An example of molding custom source code analyses

Andrei Chis
feenk
15 min readJun 2, 2020

--

Glamorous Toolkit, the moldable development environment, ships with extensive software analysis support for various languages. These analyses can be combined and integrated with the rest of the environment to assist developers in assessing their own system.

In this tutorial we explore how we can take architectural constrains for our systems that often reside in wiki pages and other documents, and express them in a testable form through custom analyses.

The software modeling part is based on FAMIX, a family of meta-models for representing various facts about software systems. FAMIX is developed within the context of the Moose analysis platform. Glamorous Toolkit integrates FAMIX and the core of Moose into the overall development environment.

On checking architecture

Software architecture can mean different things for different people. In a broad sense, we can look at architecture as the set of design decisions that guide the implementation of a software system. They are especially helpful when we need to make decisions about how to evolve our systems.

However, if our assumptions about a system’s architecture are out of sync with the reality from the code, we can easily make wrong decisions about how to evolve the system. This has a higher chance of happening as long as the architecture of our systems remains only described in wikis, word documents or presentations. We can reduce this chance by taking our architectural decisions and expressing them as concrete analyses that we can run automatically to check that our assumptions still match the reality found in the code.

Running example

As an initial case study for expressing architectural constraints we use ArgoUML, an open-source Java applications for creating UML diagrams. We use version 0.34 of this application, which has around 2400 classes and ~200k lines of Java code.

For this system we create an architectural report containing three constraints:

  • No deprecated classes are used within the system;
  • UI widgets are placed within dedicated UI packages;
  • Modules implementing UML extensions for different programming languages have no inter-dependencies.

These constraints that are easy to write, and we use them as examples to explain the steps needed to create architectural constraints within Glamorous Toolkit.

Setup

To begin checking these constraints we need to load a model of ArgoUML into Glamorous Toolkit. We achieve this by means of an MSE file that must be exported by an external exporter. You can download the source code of ArgoUML, and an already created MSE model file using the snippet below. The MSE file was created using jdt2famix.

targetFolder := 'models' asFileReference ensureCreateDirectory.
archiveFileName := 'ArgoUML-0-34.zip'.
archiveUrl := 'https://dl.feenk.com/moose-tutorial/argouml/'. ZnClient new
url: archiveUrl, archiveFileName;
signalProgress: true;
downloadTo: targetFolder.
(ZipArchive new
readFrom: targetFolder / archiveFileName)
extractAllTo: targetFolder.
targetFolder

The snippet creates a new models folder in the Glamorous Toolkit directory, and downloads and extracts an already prepared archive for this tutorial that contains the code and the MSE model file (the archive is about 33 MB). We can inspect the models folder to check if the download went ok.

Downloading an archive with source code and a MSE model file for ArgoUML.

We can load that model by executing another code snippet.

modelFile := 'models' asFileReference
/ 'ArgoUML-0-34'
/ 'ArgoUML-0-34.mse'.
model := MooseModel new
importMSEFromFile: modelFile.
model

Now we have a loaded model of our system that we can inspect and explore.

Loading the MSE model file for ArgoUML.

Alternatively, if you want to also build the model file locally from source code, another article details the necessary steps.

The anatomy of an architectural constraint

What’s the best way to express an architectural constraint? There are many ways to approach it, but we suggest the following steps.

1. Describe the constraint

First, we need to describe the architectural constraint in plain words. The description can consist in at least a few sentences primarily focusing on the reason for which we want to verify the constraint. The code of the constraint describes what it does, and the plain text description should augment that with why it is important.

2. Ensure it is testable

Second, we need to ensure that we can actually test the constraint. For this we need to make sure that we can clearly identify the types of entities and relations involved in the constraint, and that we have all necessary data. For example, before testing that the database layer does not access code from the business layer we first need to identify which code belongs in the database layer and which code belongs in the business layer.

3. Attach a stakeholder

Every constraint should have a stakeholder. This is the person, group of persons, or team, for whom the constraint has value. If a constraint fails anyone can challenge the validity of a constraint. The stakeholder’s role is to defend it, or at least provide extra context for why it is relevant. If there’s nobody to defend it, the constraint should be removed as it does not add value anymore.

4. Formulate the analysis

Once we have a description of a constraint and a way to identify all relevant entities, we can proceed to specify an analysis that expresses that constraint. We should phrase the analysis so that it returns a list of entities (for example methods, classes, packages) that do not respect the constraint. This way, if the analysis returns an empty list the constraint passes. The constraint is considered as failing if the analysis returns one or more entities.

Implementing architectural constraints

Let’s look at defining a few concrete constraints.

Constraints in Glamorous Toolkit are subclasses of GtLeafConstraint; these kinds of constraints capture one single aspect about our system. To better structure all constraints relevant to ArgoUML we create a subclass named ArgoUMLConstraint. This class also stores the model on which we apply the constraint.

GtLeafConstraint subclass: #ArgoUMLConstraint
instanceVariableNames: 'model'
classVariableNames: ''
package: 'Argouml-ArchitectureRules'
ArgoUMLConstraint>>#model
^ model
ArgoUMLConstraint>>#model: aModel
model := aModel

To create the actual constraint we need to override the following methods:

  • name: Indicates the name of the constraint;
  • description: Provides a description for the constraint;
  • stakeholder: Indicates the stakeholder associated with the constraint;
  • issues: Computes the list of entities that do not satisfy the constraint;
  • status: Optional method indicating how to handle the case of a failing constraint when running constraints as tests. For example, we might not want to mark the build as broken due to a failing constraint, but it can still be valuable to know the list of entities that do not respect that constraint.

Checking for deprecated classes

Our first constraint checks that no deprecated classes are being used within the system. This is not the most useful constraint, however, it is a very simple one to implement and illustrates how to apply the steps:

  1. Describe the constraint. To make deprecated classes easy to remove, they should not be used from classes that are not themselves deprecated.
  2. Ensure it is testable. To test this we need to obtain the list of classes from our system and be able to detect if a class is annotated with @Deprecated. For each deprecated class we further need to access the list of classes using it. This data is readily available in the model.
  3. Attach a stakeholder. We make the Development Team the stakeholder for this constraint.
  4. Formulate an analysis. We first formulate the analysis as a sentence and then prototype an implementation in the playground.

From the model get the list of all classes that are deprecated. From this list select all classes that have at least a client that is not deprecated.

allDeprecatedClasses := model allModelClasses select: [ :each |
each isAnnotatedWith: 'Deprecated' ].
allDeprecatedClasses select: [ :each |
each clientTypes anySatisfy: [ :aClient |
(aClient isAnnotatedWith: 'Deprecated') not ] ]

By prototyping the implementation in the Playground we can directly see the list of entities returned by the constraint and make sure it is ok.

Prototyping the analysis for detecting usages of deprecated classes in the playground.

To implement the constraint we first create a subclass of ArgoUMLConstraint named ArgoUMLDeprecatedClassesWithNoDeprecatedClients. The name and description methods are trivial to implement:

ArgoUMLConstraint 
subclass: #ArgoUMLDeprecatedClassesWithNoDeprecatedClients
instanceVariableNames: ''
classVariableNames: ''
package: 'Argouml-ArchitectureRules'
ArgoUMLDeprecatedClassesWithNoDeprecatedClients>>#name
^ 'Deprecated classes with no deprecated clients'
ArgoUMLDeprecatedClassesWithNoDeprecatedClients>>#description
^ 'To make deprecated classes easy to remove, they should not be used from classes that are not themselves deprecated.'

To attach a stakeholder we create an instance of GtConstraintStakeholder and return it from the stakeholder method.

ArgoUMLDeprecatedClassesWithNoDeprecatedClients>>#stakeholder
^ GtConstraintStakeholder new
name: 'Development Team'

Now the most complicated method is issues that implements the analysis. In this case we can just reuse the code from the Playground where we prototyped the constraint.

ArgoUMLDeprecatedClassesWithNoDeprecatedClients>>#issues
| allDeprecatedClasses |
allDeprecatedClasses := self model allModelClasses select: [:each|
each isAnnotatedWith: 'Deprecated' ].
^ allDeprecatedClasses select: [ :each |
each clientTypes anySatisfy: [ :aClient |
(aClient isAnnotatedWith: 'Deprecated') not ]

We stop here with the implementation and check the constraint. We can instantiate and inspect the constraint directly in the Playground. Constraint objects have an inspector view showing the list of entities that do not respect that constraint. In this case we see in that view the 10 classes that are not deprecated and use deprecated classes.

ArgoUMLDeprecatedClassesWithNoDeprecatedClients new
model: model
Instantiating and inspecting the constraint that checks the usage of deprecated classes.

Ensuring correct placement of classes

For the second constraint let us implement a less generic one that is more specific to our system.

ArgoUML is an application with a graphical user interface. If we inspect its list of packages we observe that UI related classes are placed in UI packages that contain the ui component in their name, for example org.argouml.ui, org.argouml.notation.ui, or org.argouml.ui.explorer .

Inspecting the list of packages from the system.

To make it easier to identify UI related classes and better structure the code, we decide that all classes that represent graphical widgets should be placed in packages having the ui component in their name. This rule also allows those packages to contain sub-packages. Depending on our context we can also have a more strict rule. For now we go with the less strict version. We consider as graphical widgets those classes that inherit from java.awt.Component (here we can later decide to extend this definition).

We begin by creating a class for the constraint:

ArgoUMLConstraint subclass: #ArgoUMLWidgetsInTheWrongPackages
instanceVariableNames: ''
classVariableNames: ''
package: 'Argouml-ArchitectureRules'

Next we go through the four steps for creating a constraint.

1. Describe the constraint. Classes representing widgets (inherit from java.awt.Component) should be placed in packages that contain the ui component in their name to improve code modularity.

ArgoUMLWidgetsInTheWrongPackages>>#name
^ 'Widgets placed in the wrong package'
ArgoUMLWidgetsInTheWrongPackages>>#description
^ 'Classes representing widgets (inherit from ''java.awt.Component'') should be placed in packages that contain the ''ui'' component in their name to improve code modularity.'

2. Ensure it is testable. To test this constraint we need to access the list of classes from our system and for each class access all its superclasses including the ones from external dependencies like the graphical library AWT. For each superclass we need to access the package containing it. We can ensure we get all these information in the model, if during the generation of the model, we provide the importer with access to all relevant dependencies for the graphical libraries Swing and AWT.

3. Attach a stakeholder. As with the previous constraint the Development Team is the stakeholder for this constraint. Just in this case let us create a more specialized way of specifying the stakeholder. In the previous constraint, we used directly an instance of type GtConstraintStakeholder. We can create a subclass, named ArgoUMLConstraintStakeholder , and add a dedicated API for creating stakeholders.

GtConstraintStakeholder subclass: #ArgoUMLConstraintStakeholder
instanceVariableNames: ''
classVariableNames: ''
package: 'Argouml-ArchitectureRules'
ArgoUMLConstraintStakeholder class
instanceVariableNames: 'developmentTeam'
ArgoUMLConstraintStakeholder class>>#developmentTeam
^ developmentTeam ifNil: [
developmentTeam := self new name: 'Development Team' ]

Now we can specify the stakeholder using the method developmentTeam.

ArgoUMLWidgetsInTheWrongPackages>>#stakeholder
^ ArgoUMLConstraintStakeholder developmentTeam

We can also refactor the stakeholder method in the previous constraint.

ArgoUMLDeprecatedClassesWithNoDeprecatedClients>>#stakeholder
^ ArgoUMLConstraintStakeholder developmentTeam

4. Formulate an analysis. Last but not least we should create the analysis that checks the constraint. We start by formulating it in plain text:

From the model, get the list of all classes representing widgets. From this list, select all classes that are not placed in a package containing the ‘ui’ component.

We can now prototype the analysis in the Playground. As the first step we extract all widget classes from the model.

allWidgetClasses := model allModelClasses select: [ :aClass |
aClass superclassHierarchyGroup anySatisfy: [ :aSuperclass |
aSuperclass mooseName = 'java::awt::Component'] ].
Extracting all model classes that inherit directly or indirectly from the class java::awt::Component.

As a second step, we reject all widget classes that contain the ui component in their package name. There could be multiple ways to implement this but for now let us simply check if the qualified name includes a component whose name begins with ui.

allWidgetClasses reject: [ :aClass |
aClass namespaceScope mooseName includesSubstring: '::ui' ].
Extending our analysis to remove classes that have the ui component in the package name,

Now that we have the entire analysis we can place it in the issues method.

ArgoUMLWidgetsInTheWrongPackages>>#issues
| allWidgetClasses |
allWidgetClasses := self model allModelClasses select: [ :aClass |
aClass superclassHierarchyGroup anySatisfy: [ :aSuperclass |
aSuperclass mooseName = 'java::awt::Component' ] ].
^ allWidgetClasses reject: [ :aClass |
aClass namespaceScope mooseName includesSubstring: '::ui' ].

Next we can also inspect this constraint in the Playground to ensure it works.

ArgoUMLWidgetsInTheWrongPackages new
model: model
Inspecting the constraint that checks the placement of widget classes.

Verifying dependencies between modules

While the previous constraint is more contextual, it still can be applied to many systems that rely on a user interface and follow the same naming convention. As a last example, let us look at a constraint only relevant for ArgoUML.

ArgoUML can generate UML diagrams for systems written in various programming languages. Each language can have its own particularities. To support this ArgoUML has a core module and UML modules for different programming languages. We can decide that in our design these modules should not have any dependencies between them; they should just depend on a core module. We would like to create a constraint to ensure this.

Before going into the actual implementation, we can consider what should this constraint return. One option would be to return the list of modules that have those unwanted dependencies. The disadvantage of this is that when we get a failure we need to manually go and find the invalid dependencies.

Instead, we can structure the constraint to return the list of invalid dependencies between modules, and if we get invalid dependencies we know the exact places in the code where the invalid calls happen.

ArgoUMLConstraint subclass: #ArgoUMLInvalidUMLModulesDependencies
instanceVariableNames: ''
classVariableNames: ''
package: 'Argouml-ArchitectureRules'

Let’s go through the four steps for creating a constraint.

1. Describe the constraint. UML modules for different programming languages should not depend on one another.

ArgoUMLInvalidUMLModulesDependencies>>#name
^ 'Invalid dependencies between UML modules'
ArgoUMLInvalidUMLModulesDependencies>>#description
^ 'UML modules for different programming languages should not depend on one another.'

2. Ensure it is testable. To test this we need to obtain the list of UML modules, and the classes in each module. In this case we consider a module all classes from certain packages. For each class we further need to access all of its dependencies. We can get all these data from the model.

3. Attach a stakeholder. We make the Architecture Team the stakeholder for this constraint.

ArgoUMLConstraintStakeholder class>>#architectureTeam
^ architectureTeam ifNil: [
architectureTeam := self new name: 'Architecture Team' ]
ArgoUMLInvalidUMLModulesDependencies>>#stakeholder
^ ArgoUMLConstraintStakeholder architectureTeam

4. Formulate an analysis. There are more ways in which we could create this analysis. One possible way is as below:

From the model get the list of all UML modules. For each module collect the list of outgoing dependencies. Select from each list dependencies where the target of the dependency is another UML module.

Let’s prototype the analysis in the Playground. We do not go into details here about the implementation of the analysis. We start by getting for each UML module the list of packages that we are interested in. These are the packages that begin with org.argouml.language followed by the name of a language (there are also other packages that begin with org.argouml.language that are not UML modules)

moduleNames := #('cpp' 'csharp' 'java' 'php' 'sql').
modulePackages := model allModelNamespaces select: [ :aNamespace |
| fullName |
fullName := aNamespace mooseName.
moduleNames anySatisfy: [ :moduleName |
fullName beginsWith: 'org::argouml::language::', moduleName ] ].
modulesByName := modulePackages groupedBy: [ :aNamespace |
aNamespace withAllParentScopes reversed fourth mooseName ].
Extracting the list of packages for each UML module.

The second step is to get for every module all its dependencies. We can obtain this using the method queryAllOutgoing, that returns all types of dependencies an entity has in a model. We then select as invalid those dependencies that are to other UML modules.

allInvalidDependencies := OrderedCollection new.
modulesByName keysAndValuesDo: [ :moduleName :packages |
| moduleDependencies invalidDependencies otherModuleNames |
moduleDependencies := packages flatCollect: #queryAllOutgoing.
otherModuleNames := modulesByName keys copyWithout: moduleName.

invalidDependencies := moduleDependencies select: [ :aDependency |
aDependency to asCollection anySatisfy: [ :aCandidate |
otherModuleNames anySatisfy: [ :anotherModuleName |
aCandidate mooseName beginsWith: anotherModuleName ] ] ].

allInvalidDependencies addAll: invalidDependencies ].
allInvalidDependencies
Extracting dependencies to other UML modules

We notice that there are no modules that do not respect the constraint. To double check that the constraint works we could also go and introduce some invalid dependencies in the code and see if the constraint detects them.

We can take now the previous snippets and put them into the constraint method:

ArgoUMLInvalidUMLModulesDependencies>>#issues
|moduleNames modulePackages modulesByName allInvalidDependencies|
moduleNames := #('cpp' 'csharp' 'java' 'php' 'sql').
modulePackages := model allModelNamespaces select: [:aNamespace |
| fullName |
fullName := aNamespace mooseName.
moduleNames anySatisfy: [ :moduleName |
fullName beginsWith: 'org::argouml::language::', moduleName]].
modulesByName := modulePackages groupedBy: [ :aNamespace |
aNamespace withAllParentScopes reversed fourth mooseName ].
allInvalidDependencies := OrderedCollection new.
modulesByName keysAndValuesDo: [ :moduleName :packages |
| moduleDependencies invalidDependencies otherModuleNames |
moduleDependencies := packages flatCollect: #queryAllOutgoing.
otherModuleNames := modulesByName keys copyWithout: moduleName.

invalidDependencies := moduleDependencies select: [:aDependency |
aDependency to asCollection anySatisfy: [ :aCandidate |
otherModuleNames anySatisfy: [ :anotherModuleName |
aCandidate mooseName beginsWith: anotherModuleName ] ] ].

allInvalidDependencies addAll: invalidDependencies ].
^ allInvalidDependencies

This constraint requires more code than the previous two. Part of the reason is that this constraint is more complicated. Another is the way in which we chose to implement the constraint. However, the more important reason is that we just used “raw” operations over a model to perform all the necessary checks.

We will look in a future tutorial at how to extend a model with domain-specific queries that will make it possible to write analyses at a level of abstraction closer to the domain of our applications. Then we could, for example, express the logic for getting the packages in an UML module as below, instead of checking names of packages in the analysis.

modulePackages := model allModelNamespaces 
select: #isArgoUMLUmlModulePackage.
modulesByName := modulePackages groupedBy: #argoUMlUmlModuleName.

Creating an architectural report

Up to this point we implemented three architectural constraints. To finish, we put all of them into an architectural report. An architectural report is nothing more than a composite constraint.

We created the individual constraints by subclassing GtLeafConstraint. To create an architectural report we subclass GtConstrainerReport, which is a subclass of GtCompositeConstraint with some helper methods. As we need to pass the model to the individual constraints we store it in the report.

GtConstrainerReport subclass: #ArgoUMLReport
instanceVariableNames: 'model'
classVariableNames: ''
package: 'Argouml-ArchitectureRules'
ArgoUMLReport>>#model
^ model
ArgoUMLReport>>#model: aModel
model := aModel
ArgoUMLReport class>>#onModel: aModel
^ self basicNew
model: aModel;
initialize

Next we override the build: method and use addConstraint: to add individual constraints to the report.

build: aComposite
aComposite name: 'ArgoUML Report'.
aComposite
addConstraint:(ArgoUMLDeprecatedClassesWithNoDeprecatedClients new
model: self model);
addConstraint: (ArgoUMLWidgetsInTheWrongPackages new
model: self model);
addConstraint: (ArgoUMLInvalidUMLModulesDependencies new
model: self model)

Now we can instantiate the report and inspect it.

report := ArgoUMLReport onModel: model.
Inspecting the architectural report for ArgoUML.

We can decide that in our system using deprecated classes is ok for the moment. We could remove that constraint, however, then we cannot now just by looking at the report how many deprecated classes are currently in use. Instead we can mark the constraint about deprecated classes as neutral by overriding the status method in the constraint.

ArgoUMLInvalidUMLModulesDependencies>>#status
^ GtNeutralConstraintStatus new

Now when running the report, the first constraint is not marked as failed, and we can still get the list of deprecated classes.

We can mark constraint as neutral in a report to avoid having a failing build and still see the entities that do not respect those constraints.

Running the architectural report as part of continuous integration

Above we were looking at the report in the inspector. However, it can be useful to also run the report as part of our CI process. We can write a simple script that creates a model from an MSE file, instantiates our architectural report, and exports the results of running the report using the same format used by JUnit to return results.

$./glamoroustoolkit GlamorousToolkit.image eval "\
| modelFileName model|\
modelFileName := '.models/ArgoUML-0-34/ArgoUML-0-34.mse'\
model := MooseModel new \
importMSEFromFile: modelFileName asFileReference. \
ArgoUMLReport onModel: model.\
GtConstrainerHudsonReporter runOn: (ArgoUMLReport onModel: model)."

Below we see the results of running our architectural report for ArgoUML on a Jenkins server using the script above. Only the constraints about the placement of widgets is marked as failing.

Running the architectural report for ArgoUML using Jenkins.

Wrap-up

In this tutorial we explored the creation of an architectural report for a Java system. An architectural report consists of a set of constraints expressed through custom analyses. We can then automatically run those analyses and check them against the code. This is a first step in making sure that our assumptions about a system’s architecture do not become out of sync with the reality from the code.

--

--