Interface-Driven Development (IDD)

Interface-Driven Development (IDD)

Interface-Driven Development (IDD)

Interface-Driven Development (IDD)

Introduction to Interface-Driven Development (IDD)

During my work on different projects and using different languages, frameworks, styles, and idioms, I found out that there are no silver bullets on how to design software. Starting from a set of requirements we need to implement, we have preferences that we should first write code and then test, on the other side we have the TDD approach emerging for years now, as well as some other approaches (design-first, etc.). Here I want to explain one approach that I find to work very well when designing and implementing software, especially if it is a component or library.

The main question here is how you start designing your code. Do you start with some kind of drawing, write tests first (TDD), or start imidate with an implementation?

This is the method I developed over the years, which I found working very well in my development workflow. I call it Interface-Driven Development (IDD) and it is like the TDD process to some extent. This concept already existed in some areas, such as Protocol-oriented programming in Swift or Interface-based programming in Java and it is based on Design by Contract from Bertrand Meyer he described in his book “Object-Oriented Software Construction” [3]. In the book, he discusses standards for contracts between a method and a caller. Also, Hunt and Thomas rely upon a similar concept in their “The Pragmatic Programmer” book [2], in the section on Prototyping Architecture: “Most prototypes are constructed to model the entire system under consideration. As opposed to tracer bullets, none of the modules in the prototype system need to be particularly functional. What you are looking for is how the system hangs together as a whole, again deferring details.

The problem that this process need to solve are components that are vaguely defined during design and we tend to give more responsibility to some components than is necessary. A usual implication of such design is bad and untestable code.

A method

The method consists of five consecutive steps, as follows:

1. Create a high-level idea

When I start to think about the problem I need to solve, the focus is always on a user, developers, or other components which need to interact with a system I’m creating. This originates in User-Driven Design (UDD) where we always start by thinking about how anyone else will use our code. What actions are going to happen and who is going to do them? What kind of events do we need to handle?

Here we need to know what kind of messages our code will communicate with other components in the system, what we receive and what we can expect.

At this point, you can use any tool which is in help for you, whiteboard, post-it cards, UML, or some other kind of diagrams you like. The point is to understand which elements you have in your design and how they will communicate with each other. Here we don’t care about actual implementation.

2. Design the public interface first

Now, we create interfaces from the components we have and all public methods needed for them to communicate with each other. It is important to note here that we don’t have all information we need, but we create a minimal set of interfaces and methods based on the knowledge we have currently. Later, we will add more to this and it is an iterative process.

Here we can describe public contracts as a list of operations involved including preconditions and postconditions, their parameters, return types, and eventual errors. To know how operations would be called in a sequence, we need to set some conditions and that can be done in a form of a use case. A use case describes an interaction between a client and our interface that fulfills the goal. Use cases are usually expressed in technology-independent terms; work cases might include the names of the methods in the interface. E.g., for one case we can write a series of steps need to do to achieve it.

When designing interfaces, we should tend to have simple interfaces, but deep classes [6]. So we need to define such an interface, which is clear and easy to use, with a few parameters only, but an implementation of such methods should be deep as needed.

3. Write test cases

Now, we want to test our interface, to ensure that an implementation of an interface meets its contract. We can specify a contract in documentation, yet, a test makes the contractual obligation clearer and we can verify it. One general rule here is that interface definition is not done until we have tested it for at least one implementation. So, here do black-box testing, where we test an interface without looking inside to see how it’s implemented.

For our defined interface, our aim here is to write minimal passing test cases that will use our interfaces (unit tests). Yet, without an implementation yet and all test cases should fail (like TDD). And for the implementation, we will use mocks or stubs. With mocks and stubs, we go fast and we don’t lose time for implementation.

This process gives us two main things. First, we will check how our interface interacts with other interfaces or classes in the system and it will allow us to revise our public interface if needed before we go to the implementation. Along with these advantages, this allows us to better understand how our piece of code will work in the system, but also enable us to adapt it if needed, as it is very cheap to do at this point.

Here we can see if we need to introduce many dependencies, it is a sign that our code is not written properly and needs to be refactored.

4. Refactor interfaces

If we find any issues during running our tests and also in the way how our interface interacts with other components in the system, here we do adjustments and refactor our public interface. It is a fast way to do it, as we don’t have any underlying implementation to change.

And here we iterate between points 3. and 4. until we are satisfied without changes.

When our interface is designed properly and all tests are green we go with the next step.

5. Implement real code

When we have our public interfaces written and tested, real implementation of those methods can start, by using communication patterns we created with interfaces.

Here we should say that during the implementation process we may need to alter again our public interfaces, but our initial design should stay stable with minimal adjustments in the end.

An example

So, when we want to implement something, select a couple of use cases first for each requirement we need to implement. From here, we continue with the process (IDD) as described above.

Here we will see the process in the example of creating a simple microblogging platform in C#. Here I want to add blogging functionality to my existing website, where I want to post an article with some tags, have the possibility to list all blog posts on my site, have a preview of each blog post, and search blog posts via tags or direct text search. We need to be able to write down our blog posts to some data storage and retrieve it, and on top of that, we want to cache them for better efficiency.

For such a platform we need a few cases (C stands for a case):

C1. I can post blogs with tags

C2. Blogs can be listed

C3. A blog post can be viewed

C4. Blogs can be searched by using tags

From here it is obvious that we have some existing Website service, which needs to communicate with some kind of Blogging service. This service will be our main component for this requirement. Also, based on requirements, it is obvious that we need also some way to store or cache data.

Now, we need to understand our components and define their boundaries, because we don’t one that one component take much responsibility for itself (e.g., Blogging service to store and read data, do cache, and everything else), as this would break Single-Responsibility Principle (SRP) [5]. Here we want to move responsibilities to their respective components.

So, our design could look to this:

Model

From here we need the following services:

  • Website service -> editing capabilities, converting our text to HTML, previewing it.
  • Blogging service -> main service to handle all main use cases.
  • Data service -> our storage engine, which could be Firebase, or local files (in JSON, Raw data, or HTML format).
  • Caching service -> cache data in memory, read and write.

So, let’s start with implementing use cases in IDD style:

C1. Add a blog post

To add a blog post, we need first some kind of a BloggingService to handle all these requirements. The use case here would be: creating a blog post with no precondition and one postcondition that a blog post is created. So, let’s start with the interface first and a signature:

public interface IBloggingService
{
    public bool AddBlogPost(string text, string[] tags);
}

Here we still don’t know the implementation of this method. How and where that blog post will be stored actually. From here, we call some other service or repository to store it.

Now, our existing Website service can call this service to add a blog post. As an example:

public class IWebsiteService
{
    // Start service
    // Stop service
    // Render elements
    // Layouting
    // ...

    public bool AddBlogPost(string htmlText) {
        // Sanitize inputs
        // Validation

        bloggingService.AddBlogPost(text, tags);

        // Success
    }
}

From this, we can already draw some conclusions. How these two components will work, what data is passed around, what kind of communication we will have, etc.

For other use cases, it is similar, as follows.

C2. List all blog posts

Now we want to list all blog posts so that we list them on our Website. For this, we need to add a new method to the Blogging service.

public interface IBloggingService
{
    public bool AddBlogPost(string text, string[] tags);

    // New method
    public List<Post> GetAllBlogPosts();
}

C3. A post can be viewed

When a user clicks on one blog post, we need to retrieve it from our system and show it. This we can do through a passed ID from the upper component (WebsiteService):

public class WebsiteService
{
    public Post GetBlogPostById(string ID) {
        // Sanitize inputs
        // Validation

        bloggingService.GetBlogPostById(ID);

        // Success
    }
}

and our interface will look like this:

public interface IBloggingService
{
    public bool AddBlogPost(string text, string[] tags);
    public List<Post> GetAllBlogPosts();

    // New method
    public Post GetBlogPostById(string ID);
}

C4. Search blog posts by a tag

As our blog posts have one or more tags, we want to have the opportunity to search them by those tags.

public interface IBloggingService
{
    public bool AddBlogPost(string text, string[] tags);
    public List<Post> GetAllBlogPosts();
    public Post GetBlogPostById(string ID);

    // New method
    public List<Post> SearchPostsByTag(string tag);
}

So now, we know what our interface will look like, and now we write test cases for each one.

Test cases

We will show here an example of one test case (c1). For others, we would follow a similar approach.

C1. Add a blog post

Now, we want to implement our main method for adding a new blog post and using the underlying service which is going to store it. We implement a method without an implementation:

public class BloggingService: IBloggingService 
{
     public bool AddBlogPost(string text, string[] tags) 
    {
        throw new NotImplementedException();
    }
}

and here we write a test case for it:

[TestClass]
public class WebsiteServiceTest
{
    // ... Setup

    [TestMethod]
    public void AddBlogPost_OnePost_BlogPostIsAdded()
    {
        // Arrange 
        var bloggingServiceStub = new Mock<IBloggingService>();

        var sut = new WebsiteService(bloggingServiceStub.Object);
        var blogPost = @"This is a formatted <b>blog post</b> with @tag1 @tag2";

        // Act 
        var result = sut.AddBlogPost(blogPost);

        // Assert
        Assert.IsFalse(result); // Fail
        bloggingServiceStub.Verify(x => x.AddBlogPost(blogPost), Times.Once);
    }
}

The test will fail. At this point, we don’t use the real implementation of our method, but we will mock it.

[TestClass]
public class WebsiteServiceTest
{
    // ... Setup

    [TestMethod]
    public void AddBlogPost_OnePost_BlogPostIsAdded()
    {
        // Arrange 
        var bloggingServiceStub = new Mock<IBloggingService>();
        var blogPostSanitized;
        bloggingServiceStub.Setup(x => x.AddBlogPost(It.IsAny<string>(), 
                                                     It.IsAny<string[]>()))
                           .Callback<string>(callbackResult => blogPostSanitized = callbackResult)
                           .Verifiable();

        var sut = new WebsiteService(bloggingServiceStub.Object);
        var blogPost = @"This is a formatted <b>blog post</b> with @tag1 @tag2";

        // Act 
        var result = sut.AddBlogPost(blogPost);

        // Assert
        Assert.IsTrue(result); // Success
        Assert.AreEqual(blogPostSanitized, "This is a formatted <b>blog post</b>");
        bloggingServiceStub.Verify(x => x.AddBlogPost(blogPost), Times.Once);
    }
}

We can continue the development of our IBloggingService, adjusting it and understanding its dependencies. At this point, the interface can change, but also a test. If we want to test relation with other services, such as Data or Caching service, we can also create a stub and test it.

Now, when we are sure that our design works, we can continue with implementing our method.

public class BloggingService : IBloggingService 
{
    // Some props and dependencies
    IDataService dataService; 
    ICacheService cacheService;

    public(IDataService dataService, ICacheService cacheService)
    {
        this.dataService = dataService;
        this.cacheService = cacheService;
    }

    public bool AddBlogPost(string text, string[] tags) 
    {
        // First we check do we have the same blog post already
        // if yes, we update

        // Store text with data service
        var result = dataService.StoreBlogPost(text, tags);

        return result; // Success
    }
}

Here we can see what kind of an interface we need on the data service side, so we can start with its definition too:

public interface IDataService
{
    public bool StoreBlogPost(string text, string[] tags);

    // More methods
}

At this point, we don’t know any of the internals of data service, how it will store or fetch data, in which format, etc. And now the cycle continues, we create a test for this method, check how they work together, and later implement it. In the same manner, we would do it for a caching service.

Conclusion

In this text, I presented one way to design and implement software systems, called Interface-Driven Development (IDD). It starts from the high-level idea, creating interfaces and method signatures, but not implementing them. We write tests by using mocks and stubs and do the necessary corrections here. When we are sure how those components (and their interfaces will work), we implement them. This process looks to the TDD to some extent, yet, the focus is here on proper design, while the implementation is left for the last step.

Literature:

  1. “Design Patterns: Elements of Reusable Object-Oriented Software”, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, 1994.
  2. “The Pragmatic Programmer: From Journeyman to Master”, Andrew Hunt, David Thomas, 1999.
  3. “Object-Oriented Software Construction”, Bertrand Meyer, 2000.
  4. “Interface-oriented design”, Ken Pugh, 2006.
  5. “Design Principles and Design Patterns,” Robert C. Martin, 2000.
  6. “A Philosophy of Software Design”, John Ousterhout, 2018.
Avatar
Dr Milan Milanović
Chief Technology Officer

Building great products, building great teams!