Designing a DSL to Describe Software Architecture (Part 2)

Now that we have covered the basic building blocks in the first part of this article we can progress to more advanced aspects. In this post I will focus on how to factor out reusable parts of an architecture into separate files that can best be described as architectural templates. We will also cover the restriction of dependencies by dependency types.

Creating Architectural Templates

Let us assume we want to use a predefined layering for several modules of our software system. Without a template mechanism we would have to write something like that:

artifact Module1
{
    include "Module1/**"
 
    artifact UI
    {
        include "**/ui/**"
        connect to Business
    }
    artifact Business
    {
        include "**/business/**"
        connect to Persistence
    }
    artifact Persistence
    {
        include "**/persistence/**" 
    }
    public artifact Model
    {
        include "**/model/**"
    }
}
 
artifact Module2
{
    include "Module2/**"
 
    artifact UI
    {
        include "**/ui/**"
        connect to Business
    }
    artifact Business
    {
        include "**/business/**"
        connect to Persistence
    }
    artifact Persistence
    {
        include "**/persistence/**" 
    }
    public artifact Model
    {
        include "**/model/**"
    }
}

As you can see the inner structure of both modules is completely identical. Now imagine having dozens of modules. We clearly need a better way to model that. That is where architectural templates come into the game. They are separate architecture DSL files that are instantiated with an apply directive (see below).

We also introduced a new artifact modifier on the fly: public. All artifacts marked as public can be used by all non-public artifacts on the same level (siblings in the artifact tree). “UI”, “Business” and “Persistence” therefore have an implicit connection to “Model” (from default connector to default interface).

// File layering.arc
artifact UI 
{ 
    include "**/ui/**"
    connect to Business 
} 
artifact Business 
{ 
    include "**/business/**"
    connect to Persistence 
} 
artifact Persistence 
{ 
    include "**/persistence/**" 
}
public artifact Model
{
    include "**/model/**"
}
 
// New file modules.arc 
artifact Module1
{
    include "Module1/**"
 
    apply "layering"
}
 
artifact Module2
{
    include "Module2/**"
 
    apply "layering"
}

Now we only have to describe the inner structure of modules in one separate file and apply this structure to them using the apply directive. That is a very powerful construct that will enable you to define reusable architectural patterns.

Extending Template Based Artifacts

Now let us assume we want to refactor one of our modules to have an extra layer. We cannot do this change in the template because this would apply to all modules. If we still want to be able to use the template for this module we need some way to extend or modify the elements in the template:

artifact Module2
{
    include "Module2/**"
 
    apply "layering"
 
    // New layer
    artifact BusinessInterface
    {
        include "**/businessinterface/**"
    }
    // Now Business and UI need access to BusinessInterface
    extend Business
    {
        connect to BusinessInterface
    }
    extend UI
    {
        connect to BusinessInterface
        // UI should not use Business directly
        disconnect from Business
    }
}

Extending an artifact only makes sense in the context of apply directives. It allows us to add nested elements to an artifact and/or modify its connections to other artifacts. Within an extended artifact you can also use the keyword override to override the definitions of interfaces or connectors defined in the the original version of the artifact:

artifact Module2
{
    // ...
    extend Business
    {
        // This assumes that the template version of Business has an interface named "X"
        override interface X
        {
            // Use other patterns or other exports
            include "**/x/*"
        }
        connect to BusinessInterface
    }
    // ...
}

This allows you to adapt the architecture elements derived from a template when needed.

Restricting Dependency Types

Sometimes you are in a situation, where you allow one artifact to use another one, but would like to restrict the usage to dependencies of a certain type. For example let us assume you do not want the UI layer to create new instances of classes defined i the “Model” layer. Only “Business” and “Persistence” would be allowed to create “Model” instances. You can solve this by creating a new interface that restricts the usage of certain dependency types:

artifact UI 
{ 
    include "**/ui/**"
    connect to Business, Model.UI
} 
artifact Business 
{ 
    include "**/business/**"
    connect to Persistence, Model
} 
artifact Persistence 
{ 
    include "**/persistence/**" 
    connect to Model
}
artifact Model
{
    include "**/model/**"
    interface UI
    {
        include all // everything in "Model"
        exclude dependency-types NEW
    }
}

Now it would be marked as an architecture violation if a class from the UI layer would create a new instance of an object from the model layer. Please note that we had to remove the public modifier from “Model”. If we had kept it there would have been an implicit connection from UI to the default interface of Model bypassing our special restriction.

Currently the language supports the following list of language agnostic abstract dependency types:

CALL          // all non-virtual function or method calls
VIRTUAL_CALL  // call of a virtual method
EXTENDS       // inheritance
IMPLEMENTS    // interface implementation
NEW           // instance creation
READ          // reading a field or variable
WRITE         // writing to a field or variable
USES          // all other uses

This concludes the second article of the series. In the next part we will look at another advanced concept called “connection schemes”. Again, please let me know what you think. Comments are welcome.

Leave a Reply

Your email address will not be published. Required fields are marked *