SOLID principles in action: from Slack to Twilio

Original author: Micah Silverman
  • 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, AreaI 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 Dand Iin 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 MagicCardServicenot 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 SlackControllerwe have implemented two separate interfaces: MagicCardServiceand 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: /twilioand /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&​amp;type=card

Here, instead of an ampersand, an element is specified &​amp;.

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 TwilioControlleris 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 runtimechecks 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.xmlcan see how it relies on other modules:


	...
    com.afitnerdmagic-configcom.afitnerdmagic-apicompilecom.afitnerdmagic-implruntime

Notice what magic-implis in the area runtime, and magic-apiin the area compile.

In TwilioControllerwe 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 runtimefrom 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

SlackResponsemagic-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 SlackResponsehas an array Attachments, a text string, and a string response_type.

SlackResponsedeclared type abstractand methods of implementation functions getText, and getResponseTypelay 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 masterin the GitHub project. The hierarchy was refactored there SlackResponseto 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 SlackInChannelImageResponsehas 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 SlackResponseServiceImplin 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 getInChannelResponseWithImageand getErrorResponsereturn 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_URLand SLACK_TOKENS.

BASE_URLIs 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 Appto start:



Enter a name App Nameand select the working environment Workspacewhere you will add the application:



Next, click on the link with slash commands Slash Commandson 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/slackSave







Basic InformationInstall app to your workspace sectionInstall app to Workspace







Basic Informationwhere you returned and record the verification token.



If you installed Heroku CLI, then SLACK_TOKENSyou 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_TOKENSin 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-Wayin the column Use Caseand press the button Create:



Check for a tick in Process Inbound Messagesand enter Request URLfor 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 magicin 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 controller TwilioControllerinto two parts, where each controller has only one function.
  • O - OCP - The principle of openness / closeness. Tag: master. The class is SlackResponseone-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.

Also popular now: