DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Projections/DTOs in Spring Data R2DBC
  • Automating Cucumber Data Table to Java Object Mapping in Your Cucumber Tests
  • Munit: Parameterized Test Suite
  • The ABCs of Unity's Coroutines: From Basics to Implementation

Trending

  • Navigating Change Management: A Guide for Engineers
  • Building a Real-Time Audio Transcription System With OpenAI’s Realtime API
  • Analyzing Techniques to Provision Access via IDAM Models During Emergency and Disaster Response
  • How to Merge HTML Documents in Java
  1. DZone
  2. Data Engineering
  3. Data
  4. Introduction to Interface-Driven Development (IDD)

Introduction to Interface-Driven Development (IDD)

This article explains one way to design and implement software systems called Interface-Driven Development (IDD).

By 
Milan Milanovic user avatar
Milan Milanovic
DZone Core CORE ·
Dec. 06, 22 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
5.0K Views

Join the DZone community and get the full member experience.

Join For Free

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 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:

Model

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.

Blog Public interface Interface (computing) POST (HTTP) Testing Data Types

Published at DZone with permission of Milan Milanovic. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Projections/DTOs in Spring Data R2DBC
  • Automating Cucumber Data Table to Java Object Mapping in Your Cucumber Tests
  • Munit: Parameterized Test Suite
  • The ABCs of Unity's Coroutines: From Basics to Implementation

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!