To describe architecture in a formal way we first need to think about the basic building blocks that we could use to describe the architecture of a system. The smallest unit of design is what we call a component. What is represented by a component depends on the base model you choose for you architecture.
Since version 9.7 Sonargraph supports two different base models. The "physical" model - which is the default model and the only model that was supported prior to 9.7 - is based on the model in the "Navigation View". Components are based on the physical layout of your project. In Java a component is a single source files. In C# a component is a single C# source file or a top level type in an external assembly. In C/C++ components are created dynamically out of combining associated header and source files.
The "logical" model is based on the model in the module based namespace view. Here our components are top level programming elements, which for Java or C# is always some type, usually a class or an interface. The logical model organizes these types only by their namespaces/packages. The directory structure of the project is not reflected in the model. In C/C++ components can also be functions or other top level programming elements. For Java there is almost no difference between the physical and the logical model. Only in the rare case that a Java file has more than one top level type the logical model would create one component for each top level type, while the physical model only generates one component per source file.
So logical models are more interesting for languages like C++ and C# where the namespace structure is not related to the physical organization of your project. For these languages it makes sense to use the logical model if your namespaces are in some way reflecting your architecture.
To define an architecture you would group associated components into artifacts. Then you could group several of those artifacts together into higher level artifacts and so on. For each artifact you would also define which other artifacts can be used by them.
Each component has a name which we call the architecture filter name. In the physical model the filter name starts with the module name or "External [language]". Then follows the path of the component relative to a module specific root directory. The filter name ends with the name of the source file without an extension, All name parts are separated by slashes.
TIP
To determine the architecture filter name of a component just click on the component in the navigation or namespace view and check the "Properties View". There you should be able to see the architecture filter name and other properties of the selected item.
When using a logical model the filter name again starts with the module name followed by the namespace followed by the name of the programming element. Each name part is again separated by slashes.
In most cases assignment of components to artifacts is based on their architecture filter name. But it is also possible to assign components based on other attributes like annotations or implemented interfaces. This will be explained in more detail later in this chapter.
// Main.java in package com.hello2morrow: "Core/com/hello2morrow/Main" // The Method class from java.lang.reflection: "External [Java]/[Unknown]/java/lang/reflect/Method" // SimpleAction.cs in subfolder of NHibernate: "NHibernate/Action/SimpleAction" // An external class from System.dll: "External [C#]/System/System/Uri"
For internal components (components that actually belong to your project) we use the following naming strategy:
module/rel-path-to-project-root-dir/source-name (physical)
module/namespace-or-package/element-name (logical)
For external components (third party components used by your project) we use a slightly different strategy. Here we might not have access to any source files:
External [language]/jar-or-dll-if-present/rel-path-or-namespace/typename (physical)
External [language]/jar-or-dll-or-header/namespace-or-package/element-name (logical)
Now we can use patterns to describe groups of components:
// All components from the Core module with "business" in their name: "Core/**/business/**" // All components in java.lang.reflect: "External*/*/java/lang/reflect/*"
As you can see a single '*' matches everything except a slash, '**' matches over slash boundaries. You can also use '?' as a wildcard for a single character.
Now we can build our first artifacts:
model "physical" // or "logical" artifact Business { include "Core/**/business/**" exclude "**/api/**" } artifact Reflection { include "External*/*/java/lang/reflect/*" }
In the first line you specify which model you would like to use. If you omit the model specification we assume "physical".
We grouped all components from module "Core" with "business" in their name into an artifact named "Business". As you can see the include patterns determine which components should belong to an artifact. You can also use "exclude" patterns to specify exceptions. The reflection classes from the Java runtime are now in their own artifact called "Reflection". Artifacts can also have "exclude" filters. They help you to describe the content of an artifact with an "everything except" strategy. Exclude filters will always be applied after all include filters.
TIP
More than one "include" statement can be used to assign components to an artifact. It is also possible to use "exclude" statements to specify exceptions from the elements included above.
The assignment of components to artifacts is usually determined by the order of artifacts in the DSL file. The principle is "first come. first served". If two patterns would match the same artifact the first pattern wins. It is however possible to assign priorities to artifacts which changes the order in which artifacts are assigned. The default priority of an artifact is 0. You can change the priority of an artifact with a priority definition, which always must be the first definition in an artifact. You can also negate patterns with the keyword 'not' or combine them with 'and'.
model "physical" // or "logical" artifact Business { include "Core/**/business/**" } artifact Reflection { priority 1 include "External*/*/java/lang/reflect/*" } artifact NotApiController { include "**/controller/**" and not "**/api/**" }
In the example above the artifact 'Reflection' would get the first pick at components since it has a higher priority than 'Business', even though it is defined after 'Business'. You can also assign negative priorities. That can be useful when you want to ensure that an artifact gets a lower priority than the default of 0. Just to be clear, if two artifacts have the same priority the order in the file determines which one picks first. The last artifact will match everything with 'controller' in the artifact name, unless it also contains 'api' in the name.
artifact Parent { artifact FirstChild { include "**/c1/**" } artifact SecondChild { include "**/c2/**" } } artifact OtherParent { include "**/other/**" artifact FirstChild { include "**/c1/**" } artifact SecondChild { include "**/c2/**" } }
Artifacts can be nested arbitrarily. If a parent artifact does not have any include patterns of its own it becomes a 'transparent' artifact, i.e. it passes all components offered to it to its children for matching. 'Parent' is an example for a transparent artifact. 'OtherParent', on the other hand, defines its own include pattern. Now its children are only offered artifacts that are matched by the parent artifact, i.e. artifacts that contain 'other' somewhere in their component name. You can overrule this behavior by using 'strong' include patterns in the children artifacts. They are introduced in the next section.
TIP
Transparent artifacts can still have 'exclude' statements to limit the elements passed on to their children.
If a pattern has no matches, Sonargraph will put a warning marker on that pattern. You can suppress that warning by either making the artifact 'optional' or mark the pattern as 'optional'