Coding Better: Programming From the Outside In. Part I

Modeling an API for a domain is a difficult task. The trick is to get the correct level of encapsulation/abstraction while making the API easy to understand and consume. If we can model our domains at the same level that we understand them linguistically then we are 90% of the way there. This article discusses a programming methodology by which we can build a concise interface that will provide a consistent level of abstraction and is easy to code against.

Intro:

In this article we'll look at a technique for building an API from the outside in.  I'll just be going over defining the surface areas (the outer part) of the API and will not build out the implementation (the inner part) which is something that would be done later. 

Modeling a domain using this process has some significant benefits most of which are derived from the fact that we are coding from the outside-in rather than coding inside-out.  Just like when we wear our cloths inside out and see some of the seams and how the piece was constructed, code constructed from the inside-out usually has poor encapsulation/abstraction and some of the plumbing (or seams) leak into the consumer's layer.  Using this technique we'll be focusing on building the surface area of our classes first and later worry about the implementation.  This technique really helps code bloat because we will only be coding what we need and not what we think we might need at some point in the future.  We also have all the benefits of a TDD (Test Driven Development) coding process.

The resulting surface area of our library should be relatively simple and other programmers consuming our classes should find it very intuitive because it should make sense linguistically.  Sometimes this means some additional complexity will end up inside our library in order to present a simpler surface are.   Code written against our API will end up being much more readable than if we were to code the guts first.  We can take advantage of intellesense to provide developers consuming our library with a logical "next step" when they are coding.  All of this will be accomplished while we use a TDD approach so we end up with a nice suite of tests for our library.  Even if we don't exclusively use this approach for your development some of the techniques are useful in certain places.

Defining Our Domain:

This is the most difficult part of the process and often I find myself needing a couple tries to get it right.  The idea is to write down how we would think of a domain using common language and try to convert it to code using method and properties.  The trick is figuring out exactly what we are modeling and how it would be consumed in a manner that correctly abstracts the complexity of the domain.   This is the most significant change from the inside-out approach to coding where a junior programmer would start with a general idea of the functionality required and just can't wait to start putting code on the page.  By investing more time thinking about the surface are of the functionality we want to expose before we start implementation we end up with a much better product in the end and have less wasted work due to surface are changes later.

Let's say we have to model a marathon and its participants. Using the outside-in approach we'll always define how we want our code to read first, before we write a single line of code.  We would begin by writing some statements about how the domain should be consumed with the goal of making the API as close to spoken or written language as possible.  The following is our first sketch of how we would like our code to read. Because this code is readable by non-technical people, we would potentially go through many discussions with domain experts to make sure we have the structure correct.  There are many details hidden in the nuances of language that we often miss in code and being able to discuss code with a domain expert in this way before we set anything in stone will make our library much more accurate and complete.

Racer Joe, Frank, Fred;

Race race1 = Joe.Races(Frank);

race1. IsJoinedBy(Fred);

race1. IsJoinedBy(Bill);

 

This first overly simplified sketch of the API would probably go though a few iterations of change and become much more complex before really being solidified.  For the purposes of this article you can get the general idea of what we're shooting for so we'll leave it as is. In a real project we would probably go through a few iterations by ourselves or with a domain expert on this sketch alone to really flesh out the domain before moving forward.  Let's say we have gone through this process and are now ready for building an object model that behaves as close to our sketch as possible.

Building the Surface Area:

First we'll build the Racer class and some basic functionality.  We figure that we'll need a way to distinguish our racers so we'll make the first slight alteration to our sketch by adding a "Name" property.  On a real project it would be a good idea to keep our sketch up-to-date and modify it every time we make a change like this in order to ensure everything is consistent and all the changes make sense together with our sketch.  For this article we'll wing it for the sake of brevity and to give ourselves some elbow room.

public class Racer
{
    public Racer(string name)
    {
        m_Name = name;
    }
 
    private readonly string
        m_Name;
 
    public string Name
    {
        get { return m_Name; }
    }  
}

Next, we'll build a unit test in a separate test project to pin down our functionality. 

[TestMethod()]
public void RacerConstructorTest()
{
    string name = "Joe";
    Racer target = new Racer(name);
    Assert.AreEqual(name, target.Name);
}

I know we are not strictly following the TDD methodology, but we get the same benefits in the end as long as we are patient enough to ensure we have a test before we move on.  For those of you who this just rubs the wrong way… not to worry: after this we'll be better about following TDD a bit more strictly.

We now will define our next entity, the "Race" class with no implementation.

public class Race
{
}

And we'll write a test for construction of Race using our Racer class.  Take note that there is currently no "Racer.Races()" method.  Using this technique we will first be writing code in the way we want to consume it and then write the implementation.  This is similar to TDD where we write the test first but taken to the next level.  We will write the consuming code first, before the method stubs even exist.

[TestMethod()]
public void RaceConstructorTest()
{
    Racer joe = new Racer("Joe");
    Racer frank = new Racer("Frank");
 
    Race race1 = joe.Races(frank);
 
    Assert.IsNotNull(race1);
}

Now we'll take advantage of the IDE (In this case, Visual Studio 08) in order to stub out the method we want.  "Races()" is not implemented, but using Studio08 we can right click on the unimplemented method and generate a method stub.  There are other tools available as well to help with this style of development.  The one I currently use is from DevExpress and is a great addition to VisualStudio.  Because everyone might not have this product, we'll just go with the VisualStudio functionality for this article.  We right click on the method that has not been defined and we see a "Generate Method Stub" in the menu and select it.

image001.gif

And now the IDE writes the method for us in our Racer class with the NotImplementedException in place.

public class Racer
{
    public Racer(string name)
    {
        m_Name = name;
    }
 
    private readonly string
        m_Name;
 
    public string Name
    {
        get { return m_Name; }
    }
 
    public Race Races(Racer frank)
    {
        throw new NotImplementedException();
    }
}

We can now use the IDE again to generate a test against our new method in order to define how it should behave:

image002.gif

And put in the code we expect.  At this point we may need to add to the surface are of our classes in order to test them.  We'll try and write new methods that are more like natural language so that they make sense in the context of the domain.  Again, if this were a real project we should update our original code sketch to include this new method. 

In the domain of a marathon, it would not make sense to say: a race "contains" a racer.  So we'll write a method so our code will read as: a race "includes" a racer. Just like last time, we'll first write the method in the way we would like it consumed and then stub it out using the IDE.

[TestMethod()]
public void RacesTest()
{
    Racer joe = new Racer("Joe");
    Racer frank = new Racer("Frank");
    Race race1 = joe.Races(frank);
 
    Assert.IsTrue(race1.Includes(joe));
    Assert.IsTrue(race1.Includes(frank));
}

We'll right click to generate method stub.

image003.gif

And now we have our first method on our Race class.

public class Race
{
    public object Includes(Racer frank)
    {
        throw new NotImplementedException();
    }
}

We need to make a few modifications.

public class Race
{
    public Boolean Includes(Racer frank)
    {
        throw new NotImplementedException();
    }
}

Then we can generate a unit test

image004.gif

We'll wait to implement our tests because we'll handle the "IsJoinedBy()" method by first writing the test for our object model first.  It is usually a good idea to get as much of the surface are built out as possible before implementing the code because the method stubs will most likely change how the classes are composed.  This will help to avoid the situation where we have to throw out some implementation because it does not work with a new method signature.

[TestMethod()]
public void StructuralTest()
{
    Race race1 = new Race();
 
    Racer joe = new Racer("Joe");
    Racer frank = new Racer("Frank");
 
    race1.IsJoinedBy(joe);
    race1.IsJoinedBy(frank);
 
    Assert.IsTrue(race1.Includes(frank));
    Assert.IsTrue(race1.Includes(joe));                  
}

Now we'll generate "IsJoinedBy()" method stub.

image005.gif

 

And we get the following code:

public Object IsJoinedBy(Racer joe)
{
    throw new NotImplementedException();
}

This needs to be modified slightly to be exactly what we want.

public Race IsJoinedBy(Racer joe)
{
    throw new NotImplementedException();
}

We generate the test:

image006.gif

And now we have a solution that builds and a suite of failing unit tests that we can start working on:

image007.gif

Conclusion:

If we felt the surface are of our classes were pretty much complete according to our sketch, we could start implementing some of our classes to start getting our unit tests to pass or pass it along to a junior developer. Another option is to continue building out our tests and hammering out the surface area of the domain.  I like having as much of the surface are in place as possible because the surface area can affect the guts of the class and I want to keep rewriting implementation code to a minimum.  The process of picking where each linguistic element should reside is one of the tricky parts of building a solid API and this can be figured out before writing code ensuring no time is wasted coding implementations that have to be scrapped later. 

In my next article I'll show you how to use the Fluent Interface pattern to make more sophisticated linguistic code that is even easier to read and code against.

Until next time,
Happy Coding

I also uploaded a video demonstrating this approach if you are interested.