Geschreven door Ian van Nieuwkoop

Working with third-party libraries and what to do when you find yourself in a mess

Development6 minuten leestijd

Why reinvent the wheel, when you can save yourself some time by using some vendor library, open source or even libraries that come bundled with the platform you are using?

Using existing libraries is often easier and quicker. However, if you are not careful you can end up in a mess with code which is: 

Difficult to change. Leveraging third-party APIs leaves you with code which often looks like it is nothing but repeated calls to someone else’s library. Frequently resulting in your code being tightly knotted with the library’s. 

Difficult to test. Use of someone else’s code lulls you into a false sense of security. You end up thinking that you don’t need tests. After all, we aren’t doing anything significant, just calling some methods here and there. And before you know, the code has continued to grow and you are now left with something that isn’t simple anymore. It’s a patchwork of untestable code that no one wants to touch.

Help your future self and colleges and start wrapping third-party APIs.

“Wrapping third-party APIs is a best practice.” — Robert C. Martin

When you wrap a third-party API, you minimize your dependencies upon it. You are able to more easily move to a different library or mock out third-party calls when you are testing your own code.

You are not tied to a particular vendor’s API design choices. You can define an API that you feel comfortable with.

Systems littered with library calls are harder to deal with than homegrown systems. First, it's hard to see how to improve the structure, because all you see are API calls. Most things that can give you a hint at a design is hidden in an external library. Secondly, you don’t own the API. You can’t rename anything to make them clearer. You can’t hide or add extra methods.

Uncle Bob goes as far as advising you to wrap internal platform classes, like the Java Collection Map class. This may be a little much in most cases.

Techniques for wrapping existing Code

So, you are refactoring code not implementing wrappers, how do you do this safely? Michael Feathers, in his book ‘Working Effectively with Legacy Code’, describes two methods:

Skin and Wrap. When refactoring existing code, this is the safest method to use.   

You start by making an interface which mirrors the existing API as closely as possible. Most IDEs come with build-in options for extracting interfaces from classes.

You can now choose to preserve the signatures of the original API. First, you create wrappers for each one of the external library classes used in your code. These wrappers can delegate to the real API in production and point to mock objects when testing. (Code Snippet 1 & 2.)

You may recognise this as the Decorator-pattern. If the third-party class contains too many methods, which you simply don’t need, you can skip creating the interface. Just wrap by passing the library class as a parameter to the constructor of your wrapper. (Code Snippet 3.)

After swapping out the third-party class for your own, you are left with code which is more loosely coupled and easier to test (using mock objects).

 //   Example class to be wrapped 
class API{
    public void doSomething(){ /*implementation*/ }
  // … more methods 
}
Snippet 1: The original class
//Wrapping using the Decorator Pattern
interface API {
  void   doSomething();

}

//Example wrapped class
class CustomAPI extends API{
    private API api;

    public CustomAPI(API api){
     this.api = api; 
    }  

    public void doSomething(){
    //add new functionality here
      this.api.doSomething();  
    //or here without breaking   previous code 
  }
  //other methods
} 
 Snippet 2: Using the Decorator pattern
//Example wrapped class, but not so decorator-ish
class CustomAPI{
    private API api;

    public CustomAPI(API api){
     this.api = api; 
    }  
    public void doSomething(){
    //add new functionality here
      this.api.doSomething();  
    //or here without breaking   previous code 
  }
  //other methods
}
  Snipped 3: Not so decorator-ish.

Responsibility-Based. However, when working with complex APIs with multiple classes containing dozens of methods it soon becomes unmanageable.

Basically, you start designing your own API based on the functions you are using. And then wrap your new methods nicely in classes based on their responsibilities. Most likely, you will combine multiple third-party classes, or use the same third-party class in multiple custom classes.

Of course, when you wrap a class, your new methods should only return standard Java classes or your own custom classes.

When moving code around, you want to separate the external libraries API from your custom logic. This is because you don’t want to test the third-party library. It has already been tried and tested. Also, you want your business logic to be library agnostic. Any major future changes to the third-party library, should at most only affect your custom API classes.

By slowly extracting larger pieces of code into smaller methods, it makes the code easier to understand. When you locate custom logic, extract it to a new utility class. As soon as you feel like you have finished grouping functions together, you can more easily refactor and rename your utility classes to better reflect their functionality.

/*
*Example of custom designed Interfaces. They only accept standard or your own designed *parameters.

*/
public interface ContentEngine {
 CETask getTaskObject(String id);
 // etc.
} 

public interface CETask {
  //getters
  String getAttribute();
  //setters
  void setAttribute(String value);
  //more complex commands
  void doSomethingNeat();
  //etc
}

/*
*In this example Domain, ObjectStore, Factory and Connection belong to 
*a third-party library.
/* 
public class ContentEngineImp implements ContentEngine { 
    private ObjectStore objectstore;
     
    public ContentEngine(Connection connection, String defaultObjectStoreName) {
        Domain domain = Factory.Domain.getInstance(connection, null);
        this.objectstore = 
          Factory.ObjectStore.getInstance(domain, defaultObjectStoreName);
    }
    
    public CETask getTaskObject(String id){
      Task task = Factory.Query.findObject(this.objectStore, id);
      return new CETask(task);
    }

  // … more custom API methods
}

/*
*Task is a third-party Class.
*/
public class CETaskImp implements CETask {
  private String attribute;
 
  public CETaskImp(Task task){
    this.attribute = this.parse(task.getSomething());
  }
  
  protected String getAttributeImp(String attribute){
    return doSomethingWith(attribute);
   }    
  
  public String getAttribute(){
    return getAttributeImp(this.attribute); 
   }
  
  // … more custom API methods
}

Summary

Wrapping third-party libraries is definitely a good practice to follow. It may cost you some extra time, but in the long run you are left with a system which is easier and faster to change.

References
Robert Martin (2009). Clean Code. A Handbook of Agile Software Craftmanship.
David Thomas and Andrew Hunt (2020). The Pragmatic Programmer.
Michael Feathers (2005). Working Effectively with Legacy Code.