When the Triad (Customer, Developer, Tester) have a conversation about a requirement such as a story, domain terms often come up in the conversation. These terms could represent the elements in a requirement, such as a loan amount or an interest rate, or a type applied to the elements, such as Dollar or Percentage. They could also represent actions, such as charge interest.
Eric Evans describes these terms as a Ubiquitous Language. Understanding these terms is crucial to creating a shared understanding of the requirement. But do you just leave the terms in Jira or Sharepoint? If so, then you’re missing an opportunity for creating a good design.
Kent Beck has four rules for what is a simple design. One of them is often worded “Reveals Intention” or “Clear, Expressive, and Consistent”. Al Shalloway and Ron Jeffries express that one facet of good design is to use appropriate names, so you don’t have to explain method, member, or class names with additional documentation. Ward Cunningham talks about Whole Value objects that quantify the domain model and can be used as arguments. . Primitive Obsession refers to the overuse of primitives in code. Abstract Data Types (ADTs) can give an implementation-independent view of objects. My book Prefactoring: Extreme Abstraction, Extreme Separation, Extreme Readability has a guideline “When You’re Abstract, Be Abstract All the Way”. The guideline suggests that you should avoid passing primitive values as parameters to public methods, except to the constructors.
The ideas behind these references suggest that there should be classes in the implementation that represent the types and variables of these types that follow the domain terms. Then the code gets tied back to the requirements and the impedance between the two is decreased. For the remainder of this article, I’ll use abstract data type (ADT) as the term for these type classes.
Here’s an example of a method that computes an interest, using primitives:
double computeInterest(double principal, double interestRate);
Notice that everything is declared using primitives. One might call this from a method that had:
double principal = 1.0; double interestRate = .05; double interest = computeInterest(principal, interestRate);
Let’s add abstract data types to this code:
Dollar computeInterest(Dollar principal, Percentage interestRate); Dollar principal = new Dollar(1.0); Percentage interestRate = new Percentage(.05); Dollar interest = computeInterest(principal, interestRate);
Take a moment to examine the two versions of the code – with primitives and with ADTs. Which do you think will be easier to read? If you needed rounding on the resulting interest, which way would be more maintainable?
Both the Dollar class and the Percentage class should have constructors or methods that take Strings and convert into the respective objects. These methods should respond with an error if the string represents an invalid value (pass back an invalid indication, throw an exception, etc.).
Dollar Dollar.parseString(String value) // or Dollar(String value) Percentage Percentage.parseString(String value)
The Dollar class will have other operations that the requirements suggest:
Dollar add(Dollar other) Percentage divideBy(Dollar denominator)
Dollar, Percentage, or a third class will have the multiply operation needed to compute the interest. In which class you put it is a developer decision.
Dollar multiplyBy(Percentage other)
If you’re using a language that supports operator overloading, you might use symbols instead of named methods. However, unless that operations truly represent mathematical ones, symbols can be more confusing than words. Calculations with ADTs usually involve only one or two operations, so the lines of code with calculations will not be long.
Notice that the Dollar and Percentage classes only support the operations that are needed. This is related to the Interface Segregation Principle. For example, what can you do with two primitive doubles, such as these?
double principal = 1.0; double interestRate = .05; principal * interestRate principal + interestRate principal * principal
The operations that a double provides is larger than the operations needed by a Dollar. Having the ADTs eliminates some errors such as the second and third operations above that do not make domain sense. ADTs also prevents other errors such as switching the parameters:
double interest = computeInterest(interestRate, principal);
The ADT’s can be used to valid input coming from the user, files, databases, messages, and other external sources of information. If an incoming value is invalid, then you can prevent it from entering the rest of the application.
Almost any data which has formatted input (e.g. Phone Number) or is some type of measure (e.g. Temperature, Weight) or has a restriction on allowable values (e.g. State, Country) is a candidate for becoming an ADT.
ADTs represent the domain term types in your ubiquitous language. Creating the classes for them first helps you understand the domain terms. You can add operations to them as needed to fulfill each requirement. When you create public methods for other classes, you can use the ADTs as the types for the parameters. Your code will be better related to the domain of the requirements.