Learning Through Example

Mars Rover – Modeling Concepts

In the last post, we took a look at the problem description for Mars Rover and developed a set of concepts for the problem. From these concepts, we were able to develop common terminology and determine the relationships between the concepts. In this post, I’m going to show how I think about software design in general and how to apply them when modeling in code.

As a note, I’ll be showing code in both C# (for Object-Oriented approaches) and F# (for Functional Programming approaches). Once again, these concepts are fundamentals, but depending on your technology stack, the implementations will vary.

Design Guidelines

When I’m designing my models, my end goal is to produce software that captures the problem at hand using the same terms that the business uses. By striving for this goal, I can have richer conversations with my stakeholders when I run into interesting interactions of various business rules and can speak to them using the right terminology. In addition to capturing the problem, I will focus on designing my models in such a way that a developer can’t violate a business rule because the code won’t compile. At this point, I would have made illegal states unrepresentable in my code.

I first came across this term while reading Scott Wlashcin‘s work on the terrific F# for Fun and Profit website and it immediately resonated with me. I’ve definitely been bitten before working in a codebase where I wrote some code that compiled but blew up in my face during runtime because the parameter I passed in wasn’t valid for the method I was calling. Wouldn’t it be nice if the compiler told me while I was writing the code that what I was doing wouldn’t work? By thinking a bit more about the models being used and what some of their properties are, we can make this goal achievable.

Modeling Types

With these goals in mind, when it comes to modeling concepts, I naturally gravitate to types and find that types will fall in one of three categories.

The Type Has a Finite Number of Values

If the type has a finite number of valid values, then we can remove error conditions by defining the type to only be one of those possible options. For those from an Object-Oriented background, enums are a great example of modeling these types as you can explicitly set a label for the different values. For those from a Functional background, sum types are a great way to model these choices. 

Some examples of such a type include the states in the U.S., the suits for a deck of playing cards, or the months in a year.

public enum State
{
Alabama, Alaska, California, Delaware,
Florida, Georgia, Tennessee, Wyoming
}
public enum Suit
{
Hearts, Clubs, Spades, Diamonds
}
public enum Months
{
January, February, March, April, May, June,
July, August, September, October, November, December
}
view raw finiteValues.cs hosted with ❤ by GitHub
type State = Alabama | Alaska | California
| Delaware | Florida | Georgia
| Tennesee | Wyoming
view raw finiteValues.fs hosted with ❤ by GitHub

The Type Has an Infinite Number of Values

For other types, however, there are so many possible valid values that it’s impossible to list all of them. For example, if we were looking at valid house numbers for an address, any positive integer would be valid so good luck on defining every positive number as an enum or sum type.

In these cases, I will leverage built-in primitives to model the concept at first. So in the case of HouseNumber, an integer might be a good enough spot to start. However, if I then find myself writing code that can work on integers, but shouldn’t work on HouseNumbers, then I might wrap a stronger type around the integer (see below).

// If the value isn't a major component of the design, we can use a primitive type
int houseNumber;
// However, if the type is a major concept to the domain at hand,
// it makes sense to lift it to its own type
public class HouseNumber
{
public int Value {get;}
public StreetNumber(int input)
{
// validation logic
Value = input;
}
}
// The difference between the two approaches is that in the first case, this would work
int houseNumber = 400;
Math.Sqrt(houseNumber);
// But this wouldn't
var houseNumber = new HouseNumber(400);
Math.Sqrt(houseNumber); // fails to compile with "cannot convert from HouseNumber to double"
view raw infiniteValues.cs hosted with ❤ by GitHub
// If the value isn't a major component of the design, we can use a primitive type
let houseNumber:int;
// However, if the type is a major concept to the domain at hand,
// it makes sense to lift it to its own type (single case sum type)
type HouseNumber = HouseNumber of int
// The difference between the two approaches is that in the first case, this would work
let houseNumber = 400;
Math.Sqrt(houseNumber);
// But this wouldn't
let houseNumber = HouseNumber 400
Math.Sqrt(houseNumber); // fails to compile with
// "This expression was expected to have type 'float' but here has type 'HouseNumber'"
view raw infiniteValues.fs hosted with ❤ by GitHub

The Type Is a Composition of Other Types

As the saying goes, large programs are built by composing a bunch of smaller programs, and types are no different. As we begin to model more complicated types, it’s natural to start thinking about types being composed of other types. For these types, we’ll leverage either objects (if following OO) or records (if following FP).

One way you can determine if you’re needing a composite type like this is if you find yourself using the word and or has when describing the type, then it’s a composition. For example:

An Address has a HouseNumber, it has a StreetName, it has a State.

An Address consists of a HouseNumber and a StreeName and a State

 

public class Address
{
public int HouseNumber {get; set;}
public string StreetName {get; set;}
public State State {get; set;}
}
view raw composition.cs hosted with ❤ by GitHub
type Address = {
houseNumber:int,
streetName:string,
state:State
}
view raw composition.fs hosted with ❤ by GitHub

Modeling Types

Now that we’ve talked about some different modeling techniques, let’s see how we can apply those rules as we start to model Mars Rover. From the previous post, we were able to derive the following concepts and relationships:

  • A Rover has a Location and an Orientation
  • Orientation is the Direction that a Rover is facing
  • Location is the coordinates that the Rover is located at
  • A Command is something that a Rover receives from the User
  • Direction can be North, East, South, or West
  • Command can be Move Forward, Move Backward, Turn Left,  Turn Right, or Quit

Yielding the following graph

Domain model for Mars Rover
Domain model relationships where Rover has a Location and an Orientation. Orientation is a Direction and Command is not related to anything.

Given the above rules, we can start taking a look at how to model these in code! We’ll first start with the models that don’t have a dependency, and then build up from there

Modeling Direction

From the above requirements, Direction can only be one of four possible values (North, East, South West). So based on that, it looks like we can leverage the first rule and model Direction like so:

public enum Direction
{
North, South, East, West
}
view raw direction.cs hosted with ❤ by GitHub
type Direction = North | East | South | West
view raw direction.fs hosted with ❤ by GitHub

Modeling Command

From the above requirements, Command can only be one of five possible values (MoveForward, MoveBackward, TurnLeft, TurnRight, and Quit). Based on that, we can once again leverage the first rule and model Command like so:

public enum Command
{
MoveForward, MoveBackward,
TurnLeft, TurnRight,
Quit
}
view raw command.cs hosted with ❤ by GitHub
type Command = MoveForward | MoveBackward
| TurnLeft | TurnRight | Quit
view raw command.fs hosted with ❤ by GitHub

Modeling Location

After talking more with our Subject Matter Expert, a Location is the Coordinate where the Rover is located.

Aha! A new concept!

When we ask additional questions, we find out that a Coordinate refers to the Cartesian Coordinate System and for the problem we’re solving, we can assume that a Coordinate represents two numbers where the first number represents the location from the x-axis and the second number represents the location from the y-axis.

With this new information, our mental model has changed to be the following

Domain Model Relationships
Domain model relationships where Rover has a Location and an Orientation. Location is a Coordinate where Coordinate has an X and Y value. Orientation is a Direction and Command is not related to anything.

 

Going into further discussion, we find out that both X and Y will be whole numbers for our emulation and that they can be negative. Based on these properties, it sounds like X and Y can be modeled as integers and therefore fall under the second rule.

Given that a Coordinate has to have both an X and Y value, it sounds like Coordinate falls under the third rule and that this concept is a composition of X and Y.

public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
view raw coordinate.cs hosted with ❤ by GitHub
type Coordinate = {x:int; y:int}
view raw coordinate.fs hosted with ❤ by GitHub

Modeling Orientation

From the above requirements, it seems like Orientation is what we call the Direction that the Rover is facing. Based on that, this sounds like a property that Rover would have.

Modeling Rover

Now that we have both the Direction and Coordinate concepts designed, we can start designing Rover. From the requirements, it looks like Rover is a combination of Direction (known as Orientation) and a Coordinate (known as a Location). Based on that, Rover falls under the third rule and looks like the following.

public class Rover
{
public Direction Orientation {get; set;}
public Coordinate Location {get; set;}
}
view raw rover.cs hosted with ❤ by GitHub
type Rover = {
orientation:Direction;
location:Coordinate;
}
view raw rover.fs hosted with ❤ by GitHub

Wrapping Up

In this post, we implemented the basic types needed to solve the Mars Rover kata! We first started by taking a look at the concepts identified earlier and thought about the characteristics of the type which helped guide us to build software that both uses the terms of the problem domain and also prevents us from creating errors by making illegal states unrepresentable. In the next post, we’ll start adding functionality to our application.

Additional Reading


Series

  • Part 0 – Introduction
  • Part 1 – Defining the Problem
  • Part 2 – Modeling Concepts
  • Part 3 – Intro to Testing
  • Part 4 – Implementing Rover Part 1: Move Forward and Move Backward
  • Part 5 – Implementing Rover Part 2: Turn Left and Turn Right
  • Part 6 – Implementing Rover Part 3: There seems to be a pattern
  • Part 7 – Implementing Logger
  • Part 8 – Bringing Rover and Logger together
  • Part 9 – Wrapping Up and Reflection

Leave a Reply

%d bloggers like this: