Introduction to Interface-Driven Development (IDD)
This article explains one way to design and implement software systems called Interface-Driven Development (IDD).
Join the DZone community and get the full member experience.
Join For FreeDuring 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 by Bertrand Meyer, 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 needs to solve is 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 we need to do to achieve it.
We should tend to have simple interfaces but deep classes when designing interfaces. 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 clarifies the contractual obligation, 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 that 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 to 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:
From here, we need the following services:
- Website service -> editing capabilities, converting our text to HTML, preview 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 first need 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 the relation with other services, such as Data or Caching services, 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.
Published at DZone with permission of Milan Milanovic. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments