Evolving Sonargraph’s Architecture DSL

Sonargraph’s architecture DSL is now about 18 months old and we received a lot of positive feedback from customers bundled with ideas for improving the language. There are now several projects with more than one million LOC that use this language to define and enforce their architectural blueprint. Of course this feedback is most valuable for us and we did our best to implement a good share of the ideas brought to us. This article requires some basic knowledge of our architecture DSL. An introduction can be found here. To use all the features described below you need Sonargraph-Architect version 9.3 or higher.

Expressing Architectural Patterns as Artifact Stereotypes

There are some basic patterns that are used in almost every architectural model. Those patterns describe the relationships between sibling artifacts, i.e. artifacts that have the same parent.

  • Layered architecture – here dependencies are allowed to flow top-down within an ordered list of sibling artifacts. If we use strict layering, an artifact can only access ist next sibling artifact. In the case of relaxed layering, artifacts have access to all artifacts defined beneath them.
  • Independent – here sibling artifacts are independent from each other, i.e. there should be no dependencies between them.
  • Unrestricted – here siblings artifacts have no restrictions in accessing each other. This is not very desirable because it will allow cyclic dependencies between artifacts, but can be really useful when working on a model for a legacy software system.

Those patterns can now directly be expressed in the DSL using the keywords strict, relaxed and unrestricted:

strict artifact Service
{
    include "**/service/**"
}
 
strict artifact Controller
{
    include "**/controller/**"
}
 
require "JDBC"
 
artifact DataAccess
{
    include "**/data/**"
    connect to JDBC
}
 
public artifact Model
{
    include "**/model/**"
}

Please note how we combined strict with public. The artifact “Service” can access “Controller”, because we made it a strict layer. But it also can access “Model” because that has been defined as a public artifact. “Controller” can access “DataAccess” and “Model” for the same reasons. Using the strict stereotype saves us two explicit connect statements. “DataAccess” has no stereotype and is therefore an independent artifact. But since “Model” is public, it can be accessed by “DataAccess”.

When we talk about access we always mean a connection between the default connector and the default interfaces of the respective artifacts. Keep in mind that it is always possible to customize the default connector and the default interface of any artifact.

Now lets us look at a different version of the above example:

relaxed artifact Service
{
    include "**/service/**"
}
 
relaxed artifact Controller
{
    include "**/controller/**"
}
 
require "JDBC"
 
relaxed artifact DataAccess
{
    include "**/data/**"
    connect to JDBC
}
 
unrestricted artifact Model
{
    include "**/model/**"
}

Here we use relaxed layering. Now “Service” has access to all the layers defined beneath it. The same is true for “Controller” and “DataAccess”. We also declared “Model” to be unrestricted. That means that Model has unrestricted access to all of its siblings. Of course this is not really desirable, but can be helpful if we assume that the “Model” layer contains badly structured legacy code that we do not want to address at this point in time.

Introducing Artifact Classes

Artifact classes have been added as an optional and advanced feature that can be really useful in larger projects or in conjunction with connection schemes. An artifact class basically names the connectors and interfaces an artifact is supposed to have. If an artifact is declared to have a specific class Sonargraph will verify that it defines all the interfaces and connectors required by the class. Moreover connection schemes can now also define source and target classes which allows immediate checking of correctness.

Another benefit is that artifact classes make it a lot easier to organize artifacts into a tree so that the number of top-level artifacts stays manageable.

Let us introduce a real example:

// File "layering.arc"
strict artifact Service
{
    // ...
}  
strict artifact Controller
{
    // ...
}
artifact DataAccess
{
    // ...
}
public exposed artifact Model
{
    // ...
}
interface IService
{
    export Service, Model
}
 
// Main file "business.arc"
class BusinessComponent
{
    interface IService, Model
    connector Controller, Model 
}
 
connection-scheme BC2BC : BusinessComponent to BusinessComponent
{
    connect Controller to target.IService
    connect Model to target.Model 
}
 
artifact Customer : BusinessComponent
{
    apply "layering"
}
 
artifact Order : BusinessComponent
{
    apply "layering"
 
    connect to Customer using BC2BC
}

The artifacts “Customer” and “Product” are specifying “BusinessComponent” as their artifact class. Therefore they must have “IService” and “Model” either as an interface or as an exposed artifact. They also must have connectors or artifacts named “Controller” and “Model”. In our example the artifacts conform to the class. Otherwise Sonargraph would report an error.

The advantage of using artifact classes together with connection schemes is that we now can check the connection scheme for correctness at the point of definition. Without the use of classes we can only do checks at the point of use.

Another aspect of artifact classes is that they help grouping components together in an elegant way. Let’s look at another example:

class BusinessComponent
{
    interface IService, Model
    connector Controller, Model 
}
 
artifact OrderProcessing : BusinessComponent
{
    local artifact Customer : BusinessComponent
    {
        apply "layering" // see above
    }
    artifact Order : BusinessComponent
    {
        apply "layering"
        connect to Customer using BC2BC // defined above
    }
    connect to ProductManagement using BC2BC 
}
 
artifact ProductManagement : BusinessComponent
{
    artifact Product : BusinessComponent
    {
        apply "layering"
        connect to Part using BC2BC
    }
    hidden artifact Part : BusinessComponent
    {
        apply "layering"
    }
}

The first thing you should notice is that neither “OrderProcessing” nor “ProductManagement” define the interfaces and connectors required by “BusinessComponent”. They don’t have to, because their nested artifacts do provide those connectors and interfaces. If an artifact belongs to a class and does not explicitly define a required interface or connector Sonargraph will check if it has nested artifacts that do.

In the case of interfaces Sonargraph will implicitly create a missing interface by exporting the matching interfaces of nested artifacts that are not hidden. In the case of connectors Sonargraph will implicitly create a missing connector by including the matching connectors of nested artifacts that are not local.

Here is the same example without using classes and all those implicitly defined interfaces and connectors explicitly defined:

artifact OrderProcessing
{
    local artifact Customer
    {
        apply "layering" // see above
    }
    artifact Order
    {
        apply "layering"
        connect to Customer using BC2BC // defined above
    }
    // Implicitly defined when using artifact classes
    connector Controller
    {
        include any.Controller  // will not include Customer.Controller because Customer is local
    }
    connector Model
    {
        include any.Model       // will not include Customer.Model because Customer is local
    }
    interface IService
    {
        export any.IService
    }
    interface Model
    {
        export any.Model
    }
    // end of implicit definitions
    connect to ProductManagement using BC2BC 
}
 
artifact ProductManagement
{
    artifact Product
    {
        apply "layering"
        connect to Part using BC2BC
    }
    hidden artifact Part
    {
        apply "layering"
    }
    // Implicitly defined when using artifact classes
    connector Controller
    {
        include any.Controller 
    }
    connector Model
    {
        include any.Model
    }
    interface IService
    {
        export any.IService // will not include Part.IService because Part is hidden
    }
    interface Model
    {
        export any.Model    // will not include Part.Model because Part is hidden
    }
    // end of implicit definitions
}

As you can see the use of artifact classes saves us a lot of boilerplate wiring code and also makes the architecture description easier to read and understand. The implicit definitions only occur when you do not make an explicit definition. So you can always override those definitions although this should hardly ever be necessary. Using artifact classes can become a very powerful pattern especially for the design of larger systems with many components that have a similar internal structure.

Optional and Deprecated Artifacts

We added two more stereotypes for artifacts that will help you to make your architectural templates more concise. An optional artifact can be empty without generating a warning message while a deprecated artifact is supposed to be empty and will generate a warning if anything is assigned to it. Here is an example:

// template file layering.arc
strict artifact Service
{
    include "**/service/**"
}
 
strict artifact Controller
{
    include "**/controller/**"
}
 
require "JDBC"
 
artifact DataAccess
{
    include "**/data/**"
    connect to JDBC
}
 
public artifact Model
{
    include "**/model/**"
}
 
public optional artifact Util
{
    include "**/util/**"
}
 
deprecated hidden artifact Leftovers
{
    include "**"
}

In this template we created an artifact for utility classes that can be used by all the other artifacts. We declared it to be optional because the template might be used where no utility classes are present. Since this is perfectly acceptable we do not want Sonargraph to create a warning if no components are assigned to the “Util” artifact. Now we also want to ensure that all components consumed by the template fit into one of the five legitimate layers. Anything that does not fit will be consumed by the “Leftover” artifact. Therefore it is fair to say that all components assigned to “Leftovers” do not follow our component naming strategy. Since “Leftovers” is declared to be deprecated this will generate a Sonargraph warning. We also declared “Leftover” to be hidden, so that the “Leftovers” artifact is not part of the default interface of those artifacts that apply this template. Therefore we will also get architecture violations for all incoming dependencies into the “Leftovers” artifact.

Be aware that we slightly changed the semantics of “public”.  Before 9.3 every non-public sibling of a public artifact could access it. Now only sibling artifacts (public or not) that are defined above the public artifact have access to it. So in the example above “Model” can access “Foundation” because “Foundation” is public and “Model” is defined above it. Before 9.3 you would have to add an explicit connection from “Model” to “Foundation”.

If you have another idea how to improve our architecture DSL please let us know in the comment section below. All feedback is appreciated.

Leave a Reply

Your email address will not be published.