SOLID Principles in Action: From Slack to Twilio
Time to read: 12 minutes
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:
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
:
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:
2. Define a constructor that takes one or more of the private fields you defined, like:
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:
Notice the ampersand (&) in the URL. There’s only two valid ways to include an ampersand in an XML document:
1. escaped
Notice that the ampersand is transformed into an entity: &
2. enclosed CDATA (character data)
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:
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
:
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):
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
:
Notice that the magic-impl
is runtime
scope while magic-api
is compile
scope.
In the TwilioController
, we autowire in the TwilioResponseService
:
Now, let’s see what happens if we try to autowire the implementation class like this:
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:
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
:
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.
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:
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.
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
button.
app to Workspace
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:
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
– breakTwilioController
in two to keep each controller singly purposed. - O – Open-Closed Principle – Tag:
master
–SlackResponse
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 theSlackResponse
children returnsnull
or has unneeded methods or annotations. - I – Interface Segregation Principle – Tag:
slack-first-pass
throughmaster
–MagicCardService
andSlackResponseService
perform different functions and are therefore separate services. - D – Dependency Inversion Principle – Tag:
slack-first-pass
throughmaster
– 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.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.