Examples of defining rules up-front—the preferred scenario

Namespace dependency rules

Here is an example scenario for dependency checking. Assume you are responsible for the design of a new GUI library which uses the MVC pattern. Let's call that new library "Yet Another GUI Library" (or "Your Advanced GUI Library"), which is abbreviated as "YAGL." The architect (you) has a vision of an architecture which decouples the implementations of the three pattern roles as cleanly as possible—preferably by namespaces. To be concrete, you start with the design and implementation of buttons. You decide that you want the following namespaces:
  • a namespace for base and standard models—Yagl.Buttons.Models
  • a namespace for interfaces of models and MVC-internal Listeners on models—Yagl.Buttons.IModels
  • a namespace for base button controllers—Yagl.Buttons.Controllers
  • sub-namespaces for each supported GUI System—Yagl.Buttons.Controllers.WPF, Yagl.Buttons.Controllers.Forms etc.
  • a namespace for base button views—Yagl.Buttons.Views
  • sub-namespaces for each supported GUI System—Yagl.Buttons.Views.WPF, Yagl.Buttons.Views.Forms etc.
and, moreover
  • namespaces that provide access to the underlying GUI system (typically through native calls)—System.GUISupport.WPF, System.GUISupport.Forms (for System.Windows.Forms) etc.

Your next step is to decide on the dependencies that you want to allow. This will lead to the intended decoupled architecture, where e.g. the Models namespace can be replaced (or even removed) without disturbing the rest of the design (except that, at runtime, there must of course be some concrete models around—but they could be provided by other modules, e.g. an application or a data binding layer). Here are the rules that might be necessary with the namespace division above:

    // MODELS:
      // Abstract and standard models use the model interfaces
    Yagl.Buttons.Models.**          ---> Yagl.Buttons.IModels.**
    
    // CONTROLLERS:
      // Controllers call View changing methods - on a general level ...
    Yagl.Buttons.Controllers.*      ---> Yagl.Buttons.Views.*
      // ... and for each GUI system (but not crossing
      // GUI systems!)
    Yagl.Buttons.Controllers.(*).** ---> Yagl.Buttons.Views.\1.**
    
      // GUI-system dependent Controllers use common Controller code
    Yagl.Buttons.Controllers.*.**   ---> Yagl.Buttons.Controllers.*
    
      // Controllers for one GUI system use methods and events from the 
      // corresponding native library.
    Yagl.Buttons.Controllers.(*).** ---> System.GUISupport.\1.**
    
      // Controllers influence Models; and hook themselves as listeners to Models
    Yagl.Buttons.Controllers.*      ---> Yagl.Buttons.IModels.**
    
    // VIEWS:
      // Views hook themselves as listeners to Models
    Yagl.Buttons.Views.*            ---> Yagl.Buttons.IModels.**
    
      // GUI-system dependent Views use common View code
    Yagl.Buttons.Views.*.**         ---> Yagl.Buttons.Views.*
    
      // Views for one GUI system use methods and events from the 
      // corresponding native library.
    Yagl.Buttons.View.(*).**        ---> System.GUISupport.\1.**


The last rule (but not only that one) shows how the right side of a dependency rule can reference the left side by using "back references" (this is due to an idea of Ralf Kretzschmar, a colleague on a project I'm currently working on). As in standard regular expressions, \1 references the first parenthesized group on the left side, \2 the second etc.

Your team will now start designing the classes and algorithms and then code and test along. As you have .NET Architecture Checker in your continuous build, there is no chance that someone inadvertently introduces unwanted dependencies. At some point (which might be quite early if you practice TDD) you want to assemble a small button with a controller, a view, and a model. At this time, you will probably notice that you can build this aggregate component with your defined dependencies in any of the existing namespaces, as you need access to the class constructors of controllers and views as well as models. You should now not weaken the dependencies: This will, if done a few more times, lead to a tangled architecture, where almost everything depends on everything.

One possibility of a correct design which keeps the dependencies clean is that you define an additional namespace
  • Yagl.Buttons.Standard
which provides typical facade classes connecting elements from all namespaces above. The dependencies of this namespace would (or could) be like this:

      // needed to create standard model via constructor
    Yagl.Buttons.Standard.* ---> Yagl.Buttons.Models.**
      // needed to define instance vars to models
    Yagl.Buttons.Standard.* ---> Yagl.Buttons.IModels.**
      // needed to create GUI-dependent controller via constructor
    Yagl.Buttons.Standard.* ---> Yagl.Buttons.Controllers.*.**
      // needed to create GUI-dependent view via constructor
    Yagl.Buttons.Standard.* ---> Yagl.Buttons.Views.*.**


—and now you are happy: Facade classes RadioButton, ToggleButton, StandardButton, MenuButton and more can be defined in the Standard namespace by instantiating objects from the Models, Views.*, and Controllers.* namespaces.

A different concept is that instead of providing a "connecting namespace," you use dependency injection: The creation is externalized into a container (e.g. PicoContainer), which is suitably configured. In some sense, that configuration takes the role of the connecting namespace.

In later steps of designing YAGL, you might generalize the patterns above for all sorts of GUI controls, so that you end up with dependencies like

    Yagl.(*).Models.**          ---> Yagl.\1.IModels.**
    Yagl.(*).Controllers.*      ---> Yagl.\1.Views.*
    Yagl.(*).Controllers.(*).** ---> Yagl.\1.Views.\2.**
    Yagl.(*).Controllers.*.**   ---> Yagl.\1.Controllers.* 

etc.

Macros

In the previous example, you might have defined namespaces Yagl.Buttons, Yagl.Labels, Yagl.Grids etc. with the corresponding sub-namespaces (Models, IModels, Controllers, Views). Between such "modules", you might also have dependencies:
  • Buttons depends on Labels (to place a label on the Button)
  • Grids depends on Labels (for row headers) and Buttons (for column headers which allow sorting)
  • etc.

However, the dependencies between these module should follow a consistent pattern:
  • The left side's Models sub-namespace may only depend on the right side's Models and IModels sub-namespaces.
  • The left side's Controllers sub-namespace may only depend on the right side's Controllers and Views sub-namespaces.
  • etc.

In larger systems, such compound rules introduce an additional level of abstraction, which helps to define the constraints more concisely. Here is an example of definitions for Yagl, which uses a macro called ===>:

    ===> :=
        Yagl.\L.(*).**             ---> Yagl.\R.\1.**
        Yagl.\L.Models.**          ---> Yagl.\R.IModels.**
        Yagl.\L.Controllers.*      ---> Yagl.\R.Views.*
        Yagl.\L.Controllers.(*).** ---> Yagl.\R.Views.\1.**
        Yagl.\L.Controllers.*.**   ---> Yagl.\R.Controllers.*
    =:
    
    Buttons ===> Labels
    Grids   ===> Buttons
    Grids   ===> Labels

Method dependency rules

Here is a short explanation of a quite different dependency scenario, this time on the level of methods: An object-relational mapping I wrote does its work for searches in multiple phases (this is a sort of pipeline architecture):
  1. First, search conditions are converted to an internal tree representation (c2t—"condition to tree")
  2. Then, the trees are enhanced with SQL statements, depending on lazy/eager load and other factors (t2s—"tree to SQL")
  3. Then, the SQL statements are executed (s2r—"SQL to result sets")
  4. Finally, the result sets are converted to objects (r2o—"result sets to objects")

Many internal classes contribute to more than one of these phases: There is state, and there are methods for each phase. For a clean architecture, methods of one phase should only call methods and access fields provided by the same phase; this is important so that methods of earlier phases (e.g. a t2s method) do not call methods of later phases (e.g. an r2o method), when the state necessary for the later phase has not yet been computed by an earlier phase! In addition, there are some common methods which may be called in each phase (e.g. property getters)—these are to be suffixed with com. To ascertain these constraints, the methods' names are to be suffixed with the phase, e.g. ComputeSQLt2s() or CreateObject_r2o(...). Here are possible dependency rules that help to maintain the calling architecture:

    ORMapping.**::*_c2t  --->   ORMapping.**::*_c2t 
    ORMapping.**::*_t2s  --->   ORMapping.**::*_t2s
    ORMapping.**::*_s2r  --->   ORMapping.**::*_s2r 
    ORMapping.**::*_r2o  --->   ORMapping.**::*_r2o
    ORMapping.**::*      --->   ORMapping.**::*_com
      // Methods may access private state (prefixed with _).
      // A separate getter is provided for each phase that
      // may legitimally access some field.
    ORMapping.**::*      --->   ORMapping.**::_*
    
      // ORMapping may use System except Windows.Forms
    ORMapping.**         --->   System.**
    ORMapping.**         ---!   System.Windows.Forms.**
      // Every class may use <PrivateImplementationDetails>
    ORMapping.** ---> <PrivateImplementationDetails>**


By the way, a shorter way of defining the first 4 rules would be:
    ORMapping.**::*_(c2t|t2s|s2r|r2o)  --->   ORMapping.**::*_\1


However, this is probably more difficult to understand than the expanded version above.

Also, in practice, the rules need to include methods for writing; and they will be more elaborate, as there will also be dependency rules between namespaces inside the ORMapping. E.g., some classes responsible for writing to the database—INSERT, UPDATE, DELETE—probably must not call, and not be called by, classes for querying the database.


The two scenarios just described are typical and intended uses for dependency checking: You (as the architect) define the dependencies up-front and have them checked during the life-time of the project. Of course, new developments and experiences might make it necessary to add new dependencies (as new modules with new namespaces or new method types are designed), and, sometimes, also the addition and modification of dependencies for existing namespaces. However, the latter can be a quite disruptive act as it might fundamentally shake the architectural foundations of the software system. When you become (or if you already are) a seasoned software architect, you will create more and more stable architectures up-front—or (more important) learn when to delay architectural decisions so that they can be arrived at when the necessary knowledge and understanding is available.

Last edited Jun 21, 2010 at 4:42 PM by thoemmi, version 1

Comments

No comments yet.