11.3.  Creating Architectural Aspects

Let us assume we want to use a predefined layering for several modules of our software system. Without a mechanism to factor our architectural aspects 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/**"
    }
    interface Service
    {
        export Business, 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/**"
    }
    interface Service
    {
        export Business, 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 aspects 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/**"
}
// Top level interfaces only make sense, when used together with "apply" (see below)
interface Service
{
    export Business, 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 the them using the apply directive. That is a very powerful construct that will enable you to define reusable architectural patterns.

Let us introduce two additional artifact modifiers that can be useful in certain situations: "optional" is used for artifacts defined within an aspect that could potentially be empty. Using "optional" will suppress the warning marker that is attached to artifacts that have no components assigned to them.

"deprecated" works the other way around. Artifacts declared as "deprecated" will get a warning marker if they have components assigned. That features is very useful to catch components that are not named correctly. The next example will show both modifiers in action:

// 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/**"
    connect to Util // since Model is public this is required
}
optional public artifact Util
{
     include "**/util/**"
}
deprecated artifact Deplorables
{
    include "**"
}
// Top level interfaces only make sense, when used together with "apply" (see below)
interface Service
{
    export Business, Model
}
    

We added two more artifacts. "Util" is for utility classes that might or might not be present. That is why we added the "optional" modifier. "Util" is also "public"" so that all non-public sibling artifact can use the utility classes implicitly. Since "Model" is also declared to be "public" we need to make an explicit connection to "Util" if we want "Model" to have access to "Util".

The artifact "Deplorables" catches all remaining components that are assigned to the surrounding artifact. Note that the order of artifacts is critical in this case. "**" matches everything, so if we would move "Deplorables" to the top of the artifact list it would get all available components assigned. At the end of the list it will only get those components that have not been assigned to the artifacts above. If we did not have the "Deplorables" artifact those would usually stay assigned to the parent artifact or stay unassigned if there is no parent artifact.

So having an unconnected deprecated artifact like "Deplorabes" is useful for several reasons:

  • It catches all components that are not properly named.

  • Usually it is desirable that parent artifacts distribute all their components among their children and do not keep components to themselves. This is achieved by using the "**" pattern in "Deplorables".

  • If there are components that are not properly named the artifact will get a warning marker and all dependencies to those components are marked as architecture violations.