SOLID Principles in Action: From Slack to Twilio

November 07, 2017
Written by
Micah Silverman
Contributor
Opinions expressed by Twilio contributors are their own

mtg_nicol_bolas

It seems like there’s a RESTful API for everything these days. From payments to booking tables, from notifications to spinning up virtual machines, you can do almost anything with a simple HTTP interaction.

If you’re building a service of your own, you’ll often want to be able to use it on multiple platforms at once. Following time-tested OOD (object oriented design) principles makes your code more resilient and more easily extended.

In this post, we examine one particular design approach called SOLID (it’s an acronym). We put it to practical use in writing a service that’s a Slack integration and then extending it for use with Twilio.

The service sends you a random Magic the Gathering card. If you’d like to see it in action right away, you can text the word magic to: 1-929-236-9306 (U.S. and Canada only – you’ll get an image as an MMS, so messaging rates for your carrier may apply). You can also join my Slack organization by clicking here. Once you’re in, you can type: /magic.

SOLID Meets Magic the Gathering

If you are not yet familiar with  SOLID, it is a set of principles for Object Oriented Design (OOD), popularized by Uncle Bob Martin. SOLID is an acronym for:

  • S – SRP – Single Responsibility Principle
  • O – OCP – Open-Closed Principle
  • L – LSP – Liskov Substitution Principle
  • I – ISP – Interface Segregation Principle
  • D – DIP – Dependency Inversion Principle

By following this set of principles, your code is more maintainable and more easily extended. We’ll talk about each of these principles in more detail throughout this post.

There are a lot of good examples of SOLID in a variety of languages out there. Rather than repeat the typical Shape, Circle, Rectangle, Area example, I wanted to show off the benefit of SOLID in a real-world, fully functional application.

Recently, I’ve been playing around with the Slack API. It’s really easy to create custom slash commands. I’m also a huge fan of Magic the Gathering, so I thought I’d make a Slack slash command that returns an image of a random Magic the Gathering card.

I did this very quickly using Spring Boot. As you’ll see below, Spring Boot gives you a couple SOLID principles right out of the box.

Twilio has an excellent API for voice and text messaging services. I thought it would be interesting to see just how easy it was to take my Slack example and make it work with Twilio. The idea is that you text a command to a known phone number and you get back a random Magic the Gathering image.

What follows is a breakdown of the SOLID principles (not in order) in action along the way of this software development exercise.

All the code can be found here. We’ll later also look at how we can deploy the code and get it running on your Slack and/or Twilio account, if you want to do so.

A First Pass: Magic with Slack

Just by virtue of using Spring Boot to create the Magic App, you get 2 out of the 5 SOLID principles for free. However, you’re still responsible for architecting your app properly.

Since we’ll be looking at the different principles along the development of the code, you can look at the code example at any point by checking out the respective tags in the GitHub project (you’ll find them under “Releases”). For the full code of this section check out the slack-first-pass tag.

Let’s take a look at the SlackController (all Java source found in: magic-app/src/main/java/com/afitnerd/magic) code to review the D and the I in SOLID:

 

@RestController
@RequestMapping("/api/v1")
public class SlackController {

    @Autowired
    MagicCardService magicCardService;

    @Autowired
    SlackResponseService slackResponseService;

    @RequestMapping(
        value = "/slack", method = RequestMethod.POST,
        consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE
    )
    public @ResponseBody
    Map<String, Object> slack(@RequestBody SlackSlashCommand slackSlashCommand) throws IOException {

        return slackResponseService.getInChannelResponseWithImage(magicCardService.getRandomMagicCardImage());
    }
}

 

 

DIP: Dependency Inversion Principle

The DIP states:

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.

B. Abstractions should not depend on details. Details should depend on abstractions.

Java and Spring Boot make this very easy. The SlackController has the MagicCardService *injected* into it. MagicCardService is an *abstraction*, by virtue of it being a Java interface. And, because it’s an interface it has no details.

The implementation for the MagicCardService is not relevant to the SlackController. We’ll see later on how we can enforce this separation between the interface and its implementation by breaking the application up into modules. Additionally we’ll look at other more modern ways of accomplishing dependency injection in Spring Boot.

ISP: Interface Segregation Principle

The ISP states:

Many client-specific interfaces are better than one general-purpose interface.

In the SlackController, we have two separate interfaces injected: MagicCardService and SlackResponseService. One is specific to interacting with the Magic the Gathering site. The other is specific to interacting with Slack. It would violate the ISP to have a single interface to service these two disparate functions.

 

Next up: Magic with Twilio

To follow along with the code in this section, check out the twilio-breaks-srp tag.

Let’s look at the code of the TwilioController:

 

@RestController
@RequestMapping("/api/v1")
public class TwilioController {

    private MagicCardService magicCardService;

    static final String MAGIC_COMMAND = "magic";
    static final String MAGIC_PROXY_PATH = "/magic_proxy";

    ObjectMapper mapper = new ObjectMapper();

    private static final Logger log = LoggerFactory.getLogger(TwilioController.class);

    public TwilioController(MagicCardService magicCardService) {
        this.magicCardService = magicCardService;
    }

    @RequestMapping(value = "/twilio", method = RequestMethod.POST, headers = "Accept=application/xml", produces=MediaType.APPLICATION_XML_VALUE)
    public TwilioResponse twilio(@ModelAttribute TwilioRequest command, HttpServletRequest req) throws IOException {

        log.debug(mapper.writeValueAsString(command));

        TwilioResponse response = new TwilioResponse();
        String body = (command.getBody() != null) ? command.getBody().trim().toLowerCase() : "";

        if (!MAGIC_COMMAND.equals(body)) {
            response
                .getMessage()
                .setBody("Send\n\n" + MAGIC_COMMAND + "\n\nto get a random Magic the Gathering card sent to you.");
            return response;
        }

        StringBuffer requestUrl = req.getRequestURL();
        String imageProxyUrl =
            requestUrl.substring(0, requestUrl.lastIndexOf("/")) +
            MAGIC_PROXY_PATH + "/" +
            magicCardService.getRandomMagicCardImageId();
        response.getMessage().setMedia(imageProxyUrl);
        return response;
    }

    @RequestMapping(value = MAGIC_PROXY_PATH + "/{card_id}", produces = MediaType.IMAGE_JPEG_VALUE)
    public byte[] magicProxy(@PathVariable("card_id") String cardId) throws IOException {
        return magicCardService.getRandomMagicCardBytes(cardId);
    }
}

 

As mentioned earlier, we’ll now use a more modern (best practices) approach to dependency injection. You can see we’ve done that by providing Spring Boot Constructor Injection. This is a fancy way of saying that in the latest version of Spring Boot, you can accomplish dependency injection by:

1. Define one or more private fields in your class, like:

 

private MagicCardService magicCardService;

 

2. Define a constructor that takes one or more of the private fields you defined, like:

 

public TwilioController(MagicCardService magicCardService) {
    this.magicCardService = magicCardService;
}

Spring Boot automatically handles the injection of the implementation object at runtime. The benefit is that there’s an opportunity to do error checking and validation on the injected object within the constructor.

There are two paths in this controller: /twilio and /magic_proxy/{card_id}. The magic proxy path requires a little bit of background and explanation, so let’s look at that first before talking about how this violates the SRP.

Fun with TwiML

TwiML is the Twilio Markup Language. It’s the backbone of all responses from Twilio as it acts as instructions to tell Twilio what to do. It’s also XML. Ordinarily, that’s no problem. However, the URLs returned from the Magic the Gathering website present a problem for inclusion in TwiML documents.

The URL that retrieves a Magic the Gathering card image looks like this:

 

http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&type=card

 

Notice the ampersand (&) in the URL. There’s only two valid ways to include an ampersand in an XML document:

1. escaped

 

<Response>
    <Message>
        <Body/>
        <Media>http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&type=card</Media>
    </Message>
</Response>

 

Notice that the ampersand is transformed into an entity: &

2. enclosed CDATA (character data)

 

<Response>
    <Message>
        <Body/>
        <Media>
            <![CDATA[http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&type=card]]>
        </Media>
    </Message>
</Response>

 

Either of these approaches is easy to handle with Java and the Jackson Dataformat XML extension the the Spring Boot built-in Jackson JSON processor.

The problem is that #1 above causes an error in retrieving the image from the Wizards of the Coast website (maintainers of Magic the Gathering) and #2 causes a problem with Twilio (Hey Twilio: shouldn’t TwiML be able to handle CDATA?)

The fix I came up with for this is to proxy the requests. The actual TwiML that’s returned looks like this:

 

<Response>
    <Message>
        <Body/>
        <Media>
            http://<my magic host>/api/v1/magic_proxy/144276
        </Media>
    </Message>
</Response>

 

When this TwiML is returned, Twilio hits the /magic_proxy endpoint and behind the scenes, the code retrieves the image from the Magic the Gathering website and responds with it.

Now, we can proceed in our examination of SOLID.

SRP: Single Responsibility Principle

The SRP states:

A class should have only a single responsibility.

The controller above works as-is, but it violates the SRP. That’s because the controller is responsible both for returning a TwiML response and for proxying images.

This is not really a big deal in this example, but you could imagine how this could quickly grow out of control.

If you check out the twilio-fixes-srp tag, you’ll find a new controller called MagicCardProxyController:

 

@RestController
@RequestMapping("/api/v1")
public class MagicCardProxyController {

    private MagicCardService magicCardService;

    public MagicCardProxyController(MagicCardService magicCardService) {
        this.magicCardService = magicCardService;
    }

    @RequestMapping(value = MAGIC_PROXY_PATH + "/{card_id}", produces = MediaType.IMAGE_JPEG_VALUE)
    public byte[] magicProxy(@PathVariable("card_id") String cardId) throws IOException {
        return magicCardService.getRandomMagicCardBytes(cardId);
    }
}

 

It’s only responsibility is to return the image bytes proxied from the Magic the Gathering website.

Now, the only responsibility of the TwilioController is to return the TwiML.

Modules for enforced DIP

Maven let’s you easily break a project up into modules. Those modules can have different scopes with the common scopes being: compile (default), runtime, and test.

Scopes control when modules are brought into scope. The runtime scope ensures that the classes in the specified module are *not* available at compile time. They’re only available at runtime. This helps us enforce the DIP.

It’s easier to demonstrate this by example. Check out the modules-ftw tag. We can  see that the organization of the project has changed pretty radically (as seen in IntelliJ):

solid_modules.png

There are now 4 modules. If we take a look at the magic-app module, we can see how it relies on the other modules by looking at its pom.xml:

 


<dependencies>
	...
    <dependency>
        <groupId>com.afitnerd</groupId>
        <artifactId>magic-config</artifactId>
    </dependency>
    <dependency>
        <groupId>com.afitnerd</groupId>
        <artifactId>magic-api</artifactId>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>com.afitnerd</groupId>
        <artifactId>magic-impl</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

 

Notice that the magic-impl is runtime scope while magic-api is compile scope.

In the TwilioController, we autowire in the TwilioResponseService:

 

@RestController
@RequestMapping(API_PATH)
public class TwilioController {

    private TwilioResponseService twilioResponseService;
    …
}

 

Now, let’s see what happens if we try to autowire the implementation class like this:

 

@RestController
@RequestMapping(API_PATH)
public class TwilioController {

    private TwilioResponseServiceImpl twilioResponseService;
    …
}

IntelliJ is not able to find the TwilioResponseServiceImpl class since it’s *not* in compile scope.

For fun, you can try removing the <scope>runtime</scope> line from the pom.xml and then you’ll see IntelliJ happily finds the TwilioResponseServiceImpl class.

As we have seen, using maven modules in conjunction with scopes helps enforce DIP.

Home Stretch: Refactor Slack

When I first wrote this app, I wasn’t thinking about SOLID. I just wanted to hack together a Slack app to play with the slash command functionality.

In the first iteration, all the Slack related Services and Controllers just returned Map<string, object=""></string,>. This is a great trick in Spring Boot apps to return any JSON response without having to worry about formal Java models representing the structure of the response.

As an app matures, we want to create more formal models for readable and resilient code.

Check out the source code of the slack-violates-lsp tag.

Let’s look at the SlackResponse class in the magic-api module:

public abstract class SlackResponse {

    private List<Attachment> attachments = new ArrayList<>();

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    public List<Attachment> getAttachments() {
        return attachments;
    }

    @JsonInclude(JsonInclude.Include.NON_NULL)
    public abstract String getText();

    @JsonProperty("response_type")
    public abstract String getResponseType();

    ...
}

 

From this, we can see that the SlackResponse has an array of Attachments, a text string, and a response_type string.

SlackResponse is declared abstract and it’s the responsibility of child classes to implement the getText and getResponseType methods.

Now, let’s take a look at one of the child classes, SlackInChannelImageResponse:

 

public class SlackInChannelImageResponse extends SlackResponse {

    public SlackInChannelImageResponse(String imageUrl) {
        getAttachments().add(new Attachment(imageUrl));
    }

    @Override
    public String getText() {
        return null;
    }

    @Override
    public String getResponseType() {
        return "in_channel";
    }
}

 

The getText() method returns null. With this type of response, *only* an image is included. Text is only included in an error response. This is a *huge* code smell as relates to the LSP.

LSP: Liskov Substitution Principle

The LSP states:

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

When you’re dealing with an inheritance hierarchy and a child class *always* returns null, that’s a good sign that LSP is violated. That’s because the child class doesn’t need that method, but it must implement it because of the interface defined in the parent.

Check out master from the GitHub project. The SlackResponse hierarchy is refactored to conform to LSP.

 

public abstract class SlackResponse {

    @JsonProperty("response_type")
    public abstract String getResponseType();
}

 

Now the only thing that all child classes have in common and must implement is the getResponseType() method.

The SlackInChannelImageResponse class has everything it needs for a proper image response:

 

public class SlackInChannelImageResponse extends SlackResponse {

    private List<Attachment> attachments = new ArrayList<>();

    public SlackInChannelImageResponse(String imageUrl) {
        attachments.add(new Attachment(imageUrl));
    }

    public List<Attachment> getAttachments() {
        return attachments;
    }

    @Override
    public String getResponseType() {
        return "in_channel";
    }
    …
}

 

We now no longer need a return null; anywhere.

There’s also another subtle improvement: before, we had some JSON annotations in SlackResponse: @JsonInclude(JsonInclude.Include.NON_EMPTY) and @JsonInclude(JsonInclude.Include.NON_NULL).

This was to ensure that there wouldn’t be an empty attachments array in the JSON and no text field if it was null. While these annotations are powerful, it makes our model objects brittle and it may not be clear to other developers what’s going on.

OCP: Open-Closed Principle

The last principle we’ll cover in our SOLID journey is OCP.

The OCP states:

Software entities … should be open for extension, but closed for modification.

The idea here is that as software requirements change, your code will handle any curve balls more effectively if you extend classes rather than add more code to existing classes. This helps contain “code sprawl”.

In our example above, there’s no further reason to change SlackResponse. If we want the app to support other types of Slack responses, we can easily create those specifics in other sub-classes.

This is where some of the power of Spring Boot comes in once again. Take a look at the SlackResponseServiceImpl class in the magic-impl module.

 

@Service
public class SlackResponseServiceImpl implements SlackResponseService {

    MagicCardService magicCardService;

    public SlackResponseServiceImpl(MagicCardService magicCardService) {
        this.magicCardService = magicCardService;
    }

    @Override
    public SlackResponse getInChannelResponseWithImage() throws IOException {
        return new SlackInChannelImageResponse(magicCardService.getRandomMagicCardImageUrl());
    }

    @Override
    public SlackResponse getErrorResponse() {
        return new SlackErrorResponse();
    }
}

 

Per our interface contract, both the getInChannelResponseWithImage and the getErrorResponse methods return a SlackResponse object.

Internally, those methods are creating different child objects of SlackResponse. Spring Boot and it’s built-in jackson mapper for JSON are smart enough to return the proper JSON for the concrete object being instantiated internally.

If you’re interested in providing the integration to your own slack organization or making it available through your Twilio account (or both), read on! Otherwise, you can jump to the summary section, A SOLID Recap at the end.

Deploy the Magic App

 

If you want to take full advantage of this app, you’ll need to set up Slack and Twilio properly after deploying the app to Heroku.

You can optionally just set up either Slack or Twilio. Either way, the first step is to deploy to Heroku. Fortunately, that’s the easy part.

Deploy to Heroku

The easiest way to deploy the app to Heroku is to use the friendly purple button found in the README of the GitHub project. You’ll need to provide two details: a BASE_URL and a SLACK_TOKENS.
The BASE_URL is the fully qualified name of your heroku app. For instance, I have the app deployed to: https://random-magic-card.herokuapp.com. Follow the same format using on the app name you chose: https://.herokuapp.com

There’s a bit of a chicken-and-egg game here in that the Heroku app needs some information from Slack and the Slack integration needs some information about the Heroku app. For now, you can leave the default value in SLACK_TOKENS and we will come back later and update this value with a real Slack API Token. 

You can check that the deployment worked properly by navigating to: https://.herokuapp.com. You should see a random Magic the Gathering card in your browser. If you get an error, you can look at the error log in the Heroku app web interface. Check out https://random-magic-card.herokuapp.com to see the web interface in action.

Set up Slack

Navigate to https://api.slack.com/apps and click the Create New App button to get
started:

Enter in values for the App Name and choose the Workspace you’ll be adding the app to:

Next, click the Slash Commands link on the left side followed by the Create New Command button:

Fill in values for Command (like: /magic), Request URL (like: https://.herokuapp.com/api/v1/slack),
and a Short Description. Hit Save afterwards.

At this point, your Slack slash command is all setup:

Go to Basic Information on the left side and expand the Install app to your workspace section. Click the Install
app to Workspace
button.

Afterwards press the Authorize button:

Scroll down on the Basic Information screen you are returned to and make note of the Verification Token.

If you’ve installed the Heroku CLI, you can issue this command to set the SLACK_TOKENS property properly:

 

heroku config:set \
SLACK_TOKENS=<comma separated tokens> \
--app <your heroku app name>

 

Alternatively go to your Heroku dashboard, navigate to your application and change the value of SLACK_TOKENS under Settings.

You should now be able to issue the slash command in a channel in your Slack organization and get a Magic the Gathering card in return:

Set up Twilio

To configure your Twilio integration, navigate to your Twilio Console Dashboard.

Click the three dots and choose Programmable SMS:

Choose Messaging Services:

Create a new Messaging Service by clicking the red plus ( ) button (or click “Create new Messaging Service” if you don’t have any yet):

Enter a Friendly Name, Choose Notifications, 2-Way for the Use Case and click the Create button:

Check the Process Inbound Messages checkbox and enter in the Request URL of your Heroku app (e.g.
https://.herokuapp.com/api/v1/twilio):

Press the Save button to save your changes.

Navigate to the  Numbers section on the left and make sure your Twilio number is added to the messaging service:

At this point, you should be able to test out the Twilio service by sending the word magic as a text message to your Twilio number:

**Note:** If you send anything other than the word magic (case insensitive), you’ll get the error response shown above.

A SOLID Recap

Here’s the SOLID table once again, this time with the Github Project tags used for each principle:

  • S – Single Responsibility Principle – Tag: twilio-fixes-srp – break TwilioController in two to keep each controller singly purposed.
  • O – Open-Closed Principle – Tag: masterSlackResponse is complete and does not need to be changed. It can be extended without changing existing service code.
  • L – Liskov Substitution Principle – Tag: master – none of the SlackResponse children returns null or has unneeded methods or annotations.
  • I – Interface Segregation Principle – Tag: slack-first-pass through masterMagicCardService and SlackResponseService perform different functions and are therefore separate services.
  • D – Dependency Inversion Principle – Tag: slack-first-pass through master – Dependent services are autowired into controllers. Constructor injection is the “best practices” way to do dependency injection.

There were some challenges along the way in developing this app. I’ve already spoken about the TwiML challenge above. Slack presented its own challenges as I outlined in this blog post. TL;DR: Slack will *only* POST application/x-www-form-urlencoded for slash commands, as opposed to the more modern application/json. This made it challenging to deal with the incoming JSON data with Spring Boot.

The point here is that using the SOLID principles made the code much easier to work with and extend along the way.

This completes our tour of SOLID principles. It’s my hope that it’s been valuable beyond the usual light-weight Java examples. I want to give a shout out to the Spring Framework Guru for his treatment on SOLID, especially OCP and LSP.

Have questions? Run into any difficulties setting up the example project yourself? You can comment below or tweet to me at: @afitnerd.