Since NDepend version 2018.1, the tool proposes a default rule to check Domain Driven Design (DDD) Ubiquitous Language validity.
DDD Ubiquitous Language
Let’s quote Martin Fowler on Ubiquitous Language:
Ubiquitous Language is the term Eric Evans uses in Domain Driven Design for the practice of building up a common, rigorous language between developers and users. This language should be based on the Domain Model used in the software – hence the need for it to be rigorous, since software doesn’t cope well with ambiguity.
Evans makes clear that using the ubiquitous language between in conversations with domain experts is an important part of testing it, and hence the domain model. He also stresses that the language (and model) should evolve as the team’s understanding of the domain grows.
–Martin Fowler
Eric Evans coined the term DDD, let’s quote him:
By using the model-based language pervasively and not being satisfied until it flows, we approach a model that is complete and comprehensible, made up of simple elements that combine to express complex ideas.
Domain experts should object to terms or structures that are awkward or inadequate to convey domain understanding; developers should watch for ambiguity or inconsistency that will trip up design.
–Eric Evans
See below a sample of ubiquitous language usage in the real-world. We end up with clean and readable code:
The TrainTrain Code Base
To explain and demonstrate the rule, we’ll conduct our experiment on the TrainTrain code base.
This OSS code base has been developed by Bruno Boucard and Thomas Pierrain from 42Skillz, a French consultancy company specialized in DDD and developers coaching. TrainTrain has been developed in order to illustrate concretely most of DDD concepts (including Ubiquitous Language) in a session named How To Distill The Core Domain From Your Legacy App (Live Coding). In this session, a legacy version of the code is live-refactored to a DDD-compliant version. It has been performed both at Explore DDD 2017 (Denver, Sept 2017) and DDD Europe 2018 (Amsterdam, Jan 2018).
We worked with Bruno and Thomas to develop this first rule related to DDD and we expect that more rules will follow from this collaboration.
The Rule
See below the full source code of the new rule named DDD ubiquitous language check that can be found in the rule group Naming Convention. This rule is disabled by default because before using it, the user must customize both:
- The core domain namespace name (by default set to “TrainTrain.Domain”)
- The vocabulary list
The idea is to centralize in this rule source code the vocabulary. The rule then checks that all code elements defined in the core domain namespace are named with one or several terms found in the vocabulary list. Code elements checked include classes, enumerations, structures, interfaces, methods, properties and fields. If a term needs to be used both with singular and plural forms, both forms need to be mentioned, like Seat and Seats for example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// <Name>DDD ubiquitous language check</Name> warnif count > 0 // Update to your core domain namespace(s) let coreDomainNamespaces = Application.Namespaces.WithNameLike("TrainTrain.Domain") // Update your vocabulary list let vocabulary = new [] { "Train", "Coach", "Coaches", "Seat", "Seats", "Reservation", "Fulfilled", "Booking", "Book", "Reserve", "Confirm" }.ToHashSet() let technicalWords = new [] { "get", "set", "Get", "Set", "Add" }.ToHashSet() let multiWordsVocabulary = vocabulary.Where(w => w.GetWords().Length >= 2) // Append multi-words words in vocabulary let multiWordsWords = multiWordsVocabulary.SelectMany(w => w.GetWords()) let vocabulary2 = vocabulary.Concat(multiWordsWords).ToHashSet() let vocabulary3 = vocabulary2.Concat(technicalWords).ToHashSet() from ce in coreDomainNamespaces.ChildTypesAndMembers() let tokens = ce.SimpleName.GetWords().Select(w => w.FirstCharToUpper()).ToArray() where !(ce.IsMethod && ce.AsMethod.IsConstructor) && // No vocabulary in ctor !(ce.IsField && ce.AsField.IsGeneratedByCompiler) && // Remove compiler generated backing fields, their vocabulary is in property !(vocabulary3.Any(vocable => tokens.Contains(vocable))) let wordsNotInVocabulary = tokens.Where(w => !(vocabulary2.Contains(w) && string.IsNullOrEmpty(w))).ToArray() select new { ce, wordsNotInVocabulary = string.Join(", ",wordsNotInVocabulary) } //<Description> // The language used in identifiers of classes, methods and fields of the **core domain**, // should be based on the **Domain Model**. // This constraint is known as **ubiquitous language** in **Domain Driven Design (DDD)** // and it reflects the need to be rigorous with naming, // since software doesn't cope well with ambiguity. // // This rule is disabled per default // because its source code needs to be customized to work, // both with the **core domain** namespace name // (that contains classes and types to be checked), // and with the list of domain language terms. // // If a term needs to be used both with singular and plural forms, // both forms need to be mentioned, // like **Seat** and **Seats** for example. Notice that this default rule is related with // the other default rule // *Properties and fields that represent a collection of items should be named Items* // defined in the *Naming Convention* group. // // This rule implementation relies on the NDepend API // **ExtensionMethodsString.GetWords(this string identifier)** // extension method that extracts terms // from classes, methods and fields identifiers in a smart way. //</Description> //<HowToFix> // For each violation, this rule provides the list of **words not in vocabulary**. // // To fix a violation of this rule, either rename the concerned code element // or update the domain language terms list defined in this rule source code, // with the missing term(s). //</HowToFix> |
The NDepend rule system makes easy to modify the source code of an existing rule. There is no Visual Studio project to create and store, no NuGet package to reference, no assembly to compile, version and maintain, no integration. Just textual edition with code completion, API documentation and live result while editing, and then Ctrl+S, that’s it. As a consequence, the NDepend rule system is well suited to implement such rule that must be customized with some user data before usage.
Notice that this rule relies on the new NDepend API method ExtensionMethodsString.GetWord(this string identifier). This method extracts words from code identifiers. For example from the field identifier _seatsRequestedCount it extracts the 3 words seats, Requested, Count. To be compliant with the vocabulary list, we then set the first char to upper, for example seats becomes Seats.
Running the Rule
See below a screenshot on running this rule on a TrainTrain version. An issue is spotted on a core domain class named TreasholdCapacity. Both words are reported in the column wordsNotInVocabulary because both words are not in the vocabulary list. Moreover the word Treashold has a typo. At this point, to fix this issue:
- either this class should be renamed with existing core domain vocabulary words
- either these words should be added to the vocabulary list (with the typo fix)
DDD is nowadays a popular concept. We are proud to innovate with a static analysis code rule related to DDD. We have plans for more DDD related rules and we would like to hear both your feedback on using this rule, and your needs for more DDD related rules.