Extremely Extensible Software Design (Part 1)

Update

Benji Shults
5 min readJul 14, 2018

I’ve created a github repo with better, simpler, and more complete code and a better write-up of the ideas. The ideas in this, Part 1, article are laid out there. The ideas in Part 2 are in another file in that repo.

What makes code easy to extend?

I like to think of it this way:

Code is extensible if a developer need not edit any existing code other than configuration code in order to add a new feature.

Of course, folks will have to write some new code in order to add a new feature but that’s (almost) inevitable.

If they can add a feature to our code without editing any existing code, that is ideal.

If they can add a feature to our code by editing only existing configuration code, that is excellent.

Code is not so extensible if I have to do any of the following:

  • Add an element to an enumeration.
  • Edit a utility function or class.
  • Edit a business-logic function or class.
  • Add cases to a switch or if statement.
  • Add a boolean property to an existing data class to indicate that an instance is related to the new feature.
  • Add any conditional logic to existing code.

In Part 3, I’ll go into more justification for why these things are danger signs.

In Part 2, I will describe another really nice pattern that makes code extremely extensible and the only existing code that needs to be edited is configuration code.

In this article, I will describe one of the patterns I like to use that allows new features to be added without editing any existing code. It works through a naming convention for singletons in your application context.

Assumptions

The code samples in this part of the series will be in Java but should be readable if you know any modern programming language.

I’m also assuming that you are familiar with some framework that supports an application context with named singletons (that I’ll call “beans”) that can be looked up from the context by name. The Spring Framework is an example of such a framework but there are many others.

Finally, I am not including all the code for the implementation. If you’ve written back-end code connecting a web endpoint to a database, you’ll know how to fill in the missing bits. This article is focusing on the extensible design of the solution.

Problem

Let’s say we work at a company with a number of web applications. Our task is to write a back-end service that will support users writing comments that are visible only to selected other users. Say our company’s platform already supports different kinds of groups of users to whom a user might want their comments to be visible.

Here is our domain model:

The “External Groups” already exist in various tables in our database. We are creating the Comment User Group and the Comment entities. (This article will focus on the Comment User Groups.)

One of the endpoints we need to support is GET /commentUserGroup/{commentUserGroupId}/members that returns the members of a comment user group to any member of the group. (This endpoint might be used to support type-ahead when a user wants to mention someone in the group.)

We want to do two things with this request:

  • ensure that the user is a member of the group
  • return a list of all members of the group

We also want to design the code in a way that developers can add new types of groups without editing any existing code!

The key idea

How are we going to design a solution that can be extended without editing any existing code?

Requests will come in with a keyword and the way we deal with that request will depend on the keyword.

Prior to reading this article, we might have used a switch statement, or an enumeration or both. But that has downsides: in order to add a new kind of group, we need to edit this switch statement and enumeration.

Instead, we will have an interface and an implementation of the interface for each different behavior we support. We will add instances of those classes to our application context using a naming convention.

When a request comes in, we will simply use the keyword in the request to find the proper implementation! Details below.

Solution

Comment user group data model

First, a bit more about our data model. We represent a comment user group as follows:

The extGroupType values will be code-names such as user, contentAccess, adminUser, etc. The extGroupId will the the id of the user, or content access group, etc. in the appropriate table (USERS, CONTENT_ACCESS_GROUPS, etc.)

Preparing for polymorphism

The different types of groups will have different behavior. We will capture those points of difference in an interface:

Notice that these two methods are exactly the two things we want our endpoint to do.

We will write an implementation of this interface for each type of external group. (Details excluded.)

We will need a naming convention for our singleton beans of type ExternalGroupHelper. Each type of external group will have a code-name such as user or contentAccess or adminUser. Our naming convention will be this: for the group with code-name xyz, the bean for the ExternalGroupHelper implementation will be named xyzExternalGroupHelper. E.g., we will have beans named userExternalGroupHelper, adminUserExternalGroupHelper, etc.

Implementing the endpoint

Naturally, the implementation of our endpoint will fetch the comment user group from the database by the given commentUserGroupId and learn its extGroupType and extGroupId.

We will use the group type to select the proper implementation of the ExternalGroupHelper interface, then call the isMember and the members methods.

Fetching the correct implementation

We already have an interface and that’s all we need.

Instead, we will implement a singleton bean that will provide the desired bean based on the code-name for the group type. This is the key to the extensible design!

For most application context frameworks, this will work out to be a hash-table lookup at runtime.

Details

Now, back to the request. We can simply write the following template code:

Conclusion

Now let’s say we want to support a new type of external group. We do not need to edit a single line of existing code!

Instead, we simply implement the ExternalGroupHelper interface and add our new singleton to the context with the proper name. Our new implementation will be picked up by the existing code automatically!

In future installments of this series, I’ll describe some of my other favorite patterns for producing extensible code.

Now, go right on to Part 2!

--

--

Benji Shults

Staff Software Engineer at SmartThings with a PhD in Mathematics and Artificial Intelligence from UT-Austin