SOLID principles in action: from Slack to Twilio
- Transfer
It seems that today the RESTful API exists for absolutely everything. From payments to booking tables, from simple notifications to deploying virtual machines - almost everything is available through simple HTTP interaction.
If you are developing your own service, you often want to ensure that it works simultaneously on several platforms. Time-tested principles of OOD (Object Oriented Design) will make your code more resilient and simplify extensibility.
In this article, we will explore one specific design approach called SOLID (this is an acronym). We use it in practice in writing a service with Slack integration , and then expand it for use with Twilio .
This service sends you a random Magic the Gathering card. If you want to check it in action right now, then send the word magic to 1-929-236-9306 (only the USA and Canada - you will receive an image via MMS, so your operator’s rates may apply). You can also join my Slack organization by clicking here . After logging in, type: / magic .
SOLID for Magic
If you are not familiar with SOLID , this is a set of principles of object-oriented design (OOD), which was popularized by Uncle Bob Martin . SOLID is an acronym for:
- S - SRP - Single Responsibility Principle
- O - OCP - Open Closed Principle
- L - LSP - Barbara Liskov Substitution Principle
- I - ISP - Interface Segregation Principle
- D - DIP - Dependency Inversion Principle
Following this set of principles will make your code more resilient and simplify extensibility. Later in the article we will talk more about each of these principles.
There are many good SOLID examples in a variety of languages. Instead of repeating the well-known example
Shape
, Circle
, Rectangle
, Area
I would like to show SOLID advantages in rich applications in the real world. I recently played with the Slack API . It is really very easy to create your own teams with a slash there. I'm also a big fan of Magic the Gathering , so I came up with the idea of making a Slack slash command that produces an image of a random Magic the Gathering card.
I quickly implemented my plan with Spring Boot. As you will see later, Spring Boot follows a couple of SOLID principles right out of the box.
Twilio has a great voice and text messaging API. I thought it would be interesting to see how easy it is to take my Slack example and integrate it with Twilio. The idea is that you send a text message with the team to a known phone number - and get a random Magic the Gathering image.
The following is a discussion of the SOLID principles (out of order) in action during this programming exercise.
All code can be found here . Later we will see how to apply this code on your own Slack and / or Twilio account, if you like.
First run: Magic with Slack
Just the fact of using Spring Boot to create a Magic application immediately provides two of the five SOLID principles without any special effort on your part. However, you are still responsible for the correct application architecture.
Since we will learn different principles in the process of writing the code, you can look at the code example at any time by checking the corresponding tags in the GitHub project (you will find them in the “Releases” section). The full code for this chapter is displayed by tag
slack-first-pass
. Look at the code
SlackController
(all of the Java source code here: magic-app / src / main / java / com / afitnerd / magic), which is an example of the principles D
and 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 slack(@RequestBody SlackSlashCommand slackSlashCommand) throws IOException {
return slackResponseService.getInChannelResponseWithImage(magicCardService.getRandomMagicCardImage());
}
}
DIP: dependency inversion principle
The principle of DIP is:
A. Upper level modules should not be dependent on lower level modules. Both types of modules should depend on abstractions.
B. Abstractions should not depend on the details. Details should depend on abstractions.
Java and Spring Boot make it extremely easy to implement this principle. In
SlackController
* introduced * service MagicCardService
. This is an * abstraction * because it is a Java interface. And since this is an interface, there are no details. The implementation is
MagicCardService
not specifically dependent on SlackController
. Later we will see how to ensure such a separation between the interface and its implementation, breaking the application into modules. We’ll take a look at other modern ways to implement dependencies in Spring Boot.ISP: principle of interface separation
The ISP Principle states:
Many separate client interfaces are better than one universal interface.
In
SlackController
we have implemented two separate interfaces: MagicCardService
and SlackResponseService
. One of them interacts with the site of Magic the Gathering. Another interacts with Slack. Creating a single interface to perform these two separate functions would violate the ISP principle.Next: “Magic” with Twilio
To track the code in this chapter, see the tag
twilio-breaks-srp
. Take a look at the TwilioController code:
@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 apply a more modern approach to the implementation of addiction (best practices). As you can see, we did this with the Spring Boot Constructor Injection. This is just a beautiful way to say that in the latest version of Spring Boot, dependency injection is done as follows:
1. Set one or more hidden fields in your class, for example:
private MagicCardService magicCardService;
2. Define the constructor for the set hidden fields:
public TwilioController(MagicCardService magicCardService) {
this.magicCardService = magicCardService;
}
Spring Boot will automatically handle the implementation of the object at run time. The advantage is that here it is possible to run error checking and validation on the embedded object inside the constructor.
The controller contains two parts:
/twilio
and /magic_proxy/{card_id}
. The magic_proxy path requires a little explanation, so first we’ll go over it before talking about violating the SRP principle.Fun with TwiML
TwiML is the Twilio Markup Language. This is the basis of all the answers from Twilio, because TwiML provides instructions for Twilio. It is also XML. This is usually not a problem. However, the URLs returned by the Magic the Gathering site present a problem for inclusion in TwiML documents.
The URL at which the Magic the Gathering map image is extracted looks something like this:
http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&type=card
Note the ampersand (&) in the URL. There are only two valid ways to embed ampersand in XML documents:
1. Escape characters
http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&type=card
Here, instead of an ampersand, an element is specified
&
. 2. CDATA fragment (character data)
Any of these options is easy to implement in Java with the Jackson Dataformat XML extension in the Jackson JSON processor embedded in Spring Boot.
The problem is that the first option leads to an error when receiving images from the Wizards of the Coast website (Magic the Gathering maintainers), and the second option is not supported in Twilio (hey Twilio: maybe implement CDATA support in TwiML?)
I bypassed This is a proxy restriction for requests. In this case, TwiML code is generated:
http:///api/v1/magic_proxy/144276
Upon receipt of such code, Twilio addresses the endpoint
/magic_proxy
, and behind the scene the proxy receives a picture from the Magic the Gathering website and issues it. Now we continue to study the principles of SOLID.
SRP: sole responsibility principle
The principle of SRP states:
A class should have only one function.
The above controller works as it is, but violates SRP, because it is responsible for both returning a TwiML response and a proxy for pictures.
This is not a big problem in this example, but it’s easy to imagine how the situation quickly gets out of hand.
If you follow the tag
twilio-fixes-srp
, you will see 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);
}
}
Its only task is to return bytes of the image received on the proxy from the Magic the Gathering website.
Now the only function
TwilioController
is to issue TwiML code.Modules for DIP Implementation
Maven makes it easy to break a project into modules. They may have different scopes, but there are the same ones: compilation (by default), execution, and test.
Areas take control when modules are activated in that area. The scope
runtime
checks that the classes of a particular module are * not * available at compile time. They are available only at runtime. This helps to implement the DIP principle. Easier to show with an example. See the code for the tag
modules-ftw
. You can see that the organization of the project has changed radically (as seen in IntelliJ): Now there are four modules. If you look at the module
magic-app
, you pom.xml
can see how it relies on other modules:
...
com.afitnerd magic-config com.afitnerd magic-api compile com.afitnerd magic-impl runtime
Notice what
magic-impl
is in the area runtime
, and magic-api
in the area compile
. In
TwilioController
we are automatically attached to TwilioResponseService:@RestController
@RequestMapping(API_PATH)
public class TwilioController {
private TwilioResponseService twilioResponseService;
…
}
Now look at what happens if we try to automatically bind the implemented class this way:
@RestController
@RequestMapping(API_PATH)
public class TwilioController {
private TwilioResponseServiceImpl twilioResponseService;
…
}
IntelliJ cannot find the TwilioResponseServiceImpl class because it is * not * in scope
compile
. For fun you can try to remove the line
runtime
from pom.xml
- and you will see that then IntelliJ will happily find the class TwilioResponseServiceImpl
. As we have seen, maven modules combined with scopes helps to implement the DIP principle.
Finish Line: Slack Refactoring
When I wrote this application for the first time, I did not think about SOLID. I just wanted to hack the Slack app to play around with the functionality of slash commands.
In the first version, all services and controllers related to Slack simply gave out . This is a good trick for Spring Boot applications - to give out any JSON response without worrying about formal Java models representing the structure of the response. As the application developed, there was a desire to create more formal models for readable and reliable code. See the source code for the tag . Let's look at the class in the module :
Map
slack-violates-lsp
SlackResponse
magic-api
public abstract class SlackResponse {
private List attachments = new ArrayList<>();
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public List getAttachments() {
return attachments;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public abstract String getText();
@JsonProperty("response_type")
public abstract String getResponseType();
...
}
Here we see that the class
SlackResponse
has an array Attachments
, a text string, and a string response_type
. SlackResponse
declared type abstract
and methods of implementation functions getText
, and getResponseType
lay down on the child classes. Now 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 method
getText()
returns null
. With this answer, the answer will contain * only * image. The text is returned only in case of an error message. Here * clearly * LSP smells.LSP: Barbara Lisk substitution principle
The LSP principle states:
Objects in the program should be able to replace with their subtypes without changing the accuracy of the program.
When you are dealing with an inheritance hierarchy and the child class * always * returns null, this is a clear sign of a violation of the LSP principle. Because the child class does not need this method, but it has to implement it because of the interface described in the parent class.
Check out the thread
master
in the GitHub project. The hierarchy was refactored there SlackResponse
to match the LSP.public abstract class SlackResponse {
@JsonProperty("response_type")
public abstract String getResponseType();
}
Now the only common thing for all child classes that they must implement is the method
getResponseType()
. The class
SlackInChannelImageResponse
has everything you need for a correct answer with a picture:public class SlackInChannelImageResponse extends SlackResponse {
private List attachments = new ArrayList<>();
public SlackInChannelImageResponse(String imageUrl) {
attachments.add(new Attachment(imageUrl));
}
public List getAttachments() {
return attachments;
}
@Override
public String getResponseType() {
return "in_channel";
}
…
}
No need to ever return
null
. There is another small improvement: earlier we had some JSON annotations in the class
SlackResponse
: @JsonInclude(JsonInclude.Include.NON_EMPTY)
and @JsonInclude(JsonInclude.Include.NON_NULL)
. They were needed to guarantee that an empty attachment array or text field with a zero value would not get into JSON. Although these are powerful annotations, because of them, the objects of our model become fragile, and it may not be clear to other developers what is happening.
OCP: open / closed principle
The final principle that we will take on our journey through SOLID is OCP.
The OCP Principle states:
Software entities ... must be open for expansion, but closed for modification.
The idea is that when changing the terms of reference, your code will more effectively cope with any new requirements if you extend the classes rather than adding the code to existing classes. This helps keep the code from creeping in.
In the above example, there is no additional reason to change the class
SlackResponse
. If we want to add support for other types of Slack answer types to the application, we can easily describe this specificity in subclasses. Here again, the strength of Spring Boot is evident. Take a look at the class
SlackResponseServiceImpl
in the module magic-impl
.@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();
}
}
According to the terms of interface methods
getInChannelResponseWithImage
and getErrorResponse
return an object SlackResponse
. Within these methods, various child objects are created
SlackResponse
. Spring Boot and its built-in jackson-mapper for JSON are smart enough to produce the correct JSON for a specific object, which is characterized internally. If you want to provide integration for your own organization in Slack or implement support for your Twilio account (or both), read on! Otherwise, you can go to the resume at the end of the article.
Application Deployment
If you want to use this application to its fullest, then you need to properly configure Slack and Twilio after deploying the application on Heroku.
Alternatively, you can install either Slack or Twilio. In any case, the first thing you need to do is deploy the application to Heroku. Fortunately, it is simple.
Heroku Deployment
The easiest way to deploy the application to Heroku is to click the friendly purple button in the README section of the GitHub project . You will need to specify two details:
BASE_URL
and SLACK_TOKENS
. BASE_URL
Is the full path and name of your Heroku app. For example, I have the application installed here: https://random-magic-card.herokuapp.com . Stick to the same format when the application name: .
There is a kind of chicken and egg problem here, because Heroku needs some information from Slack, and Slack integration needs some information about Heroku. At first, you can leave the default value in the field - later we will return and update this value with the current Slack API token.https://.herokuapp.com
SLACK_TOKENS
You can verify the installation by going to the address . You should see a random Magic the Gathering map in the browser. If an error occurs, see the error log in the web interface of the Heroku application. Here is an example web interface in action .
https://.herokuapp.com
Slack setup
Go to https://api.slack.com/apps and click on the button
Create New App
to start: Enter a name
App Name
and select the working environment Workspace
where you will add the application: Next, click on the link with slash commands
Slash Commands
on the left, and there’s the button for creating a new command Create New Command
: Fill in values for the command (for example:)
/magic
, Request URL
(for example:) and a short description. Then click .
Your Slack slash command is now fully configured:
Go to the section in the left pane and expand the section on the screen . Click the button .
Then the button for authorization:
Scroll the screenhttps://.herokuapp.com/api/v1/slack
Save
Basic Information
Install app to your workspace section
Install app to Workspace
Basic Information
where you returned and record the verification token. If you installed Heroku CLI, then
SLACK_TOKENS
you can correctly set the property with the following command:heroku config:set \
SLACK_TOKENS= \
--app
Alternatively, go to the Heroku dashboard , go to your application and change the value
SLACK_TOKENS
in the settings. Now the slash command should work on your organization’s Slack channel, and in return you will receive the Magic the Gathering card:
Twilio setup
To configure Twilio integration, go to the Twilio dashboard in the console .
Click on the ellipsis and select
Programmable SMS
: Select
Messaging Services
: Create a new messaging service by clicking on the button with a red plus (or click “Create new Messaging Service” if there are no services yet):
Enter
Friendly Name
, select Notifications, 2-Way
in the column Use Case
and press the button Create
: Check for a tick in
Process Inbound Messages
and enter Request URL
for your Heroku application (for example ):
Click the button to save the changes.
Go to the section in the left menu and make sure that your Twilio number is added for the messaging service:https://.herokuapp.com/api/v1/twilio
Save
Numbers
Now you can test the Twilio service by sending a word
magic
in the form of a text message to your number : ** Note: ** If you send anything other than the word
magic
(regardless of case), the error message shown above will pop up.SOLID Summary
Once again, publish the SOLID table, this time with the Github project tags that match each principle:
- S - SRP - The principle of sole responsibility. Tag:
twilio-fixes-srp
. Divides the controllerTwilioController
into two parts, where each controller has only one function. - O - OCP - The principle of openness / closeness. Tag:
master
. The class isSlackResponse
one-piece and cannot be changed. It can be expanded without changing the code of an existing service. - L – LSP – Принцип подстановки Барбары Лисков. Тег:
master
. Никакой из дочерних классовSlackResponse
не возвращаетnull
, не содержит ненужных классов или аннотаций. - I – ISP – Принцип разделения интерфейса. Тег:
slack-first-pass
посредствомmaster
. СлужбыMagicCardService
иSlackResponseService
выполняют разные функции и поэтому отделены друг от друга. - D – DIP – Принцип инверсии зависимостей. Тег:
slack-first-pass
посредствомmaster
. Зависимые службы автоматически привязаны к контроллерам. Внедрение контроллера — это «лучшие практики» внедрения зависимости.
There are some difficulties in developing this application. I already mentioned above about the problem with TwiML. But with Slack there are special problems that I outlined in this article. TL; DR: Slack accepts * only * POST requests for slash commands
application/x-www-form-urlencoded
, not more modern ones application/json
. This makes it difficult to process JSON input with Spring Boot. The basic idea is that SOLID principles made the code much easier to work and expand on.
This concludes our overview of SOLID principles. I hope it was more useful than the usual simple Java examples.