
Shape , Circle , Rectangle , Area I would like to show the advantages of SOLID in a full-featured real-world application.slack-first-pass tag.SlackController code (all Java sources are here: magic-app / src / main / java / com / afitnerd / magic), which presents 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<String, Object> slack(@RequestBody SlackSlashCommand slackSlashCommand) throws IOException { return slackResponseService.getInChannelResponseWithImage(magicCardService.getRandomMagicCardImage()); } } A. The modules of the upper levels should not depend on the modules of the lower levels. Both types of modules must depend on abstractions.
B. Abstractions should not depend on details. Details must depend on abstractions.
SlackController * has * MagicCardService . This is * an abstraction * because it is a Java interface. And since this is an interface, there are no details.MagicCardService not specifically dependent on SlackController . Later we will see how to provide such a separation between the interface and its implementation by breaking the application into modules. Additionally, consider other modern ways to implement dependencies in Spring Boot.Many separate client interfaces are better than one universal interface.
SlackController we implemented two separate interfaces: MagicCardService and SlackResponseService . One of them interacts with the Magic the Gathering site. The other interacts with Slack. Creating a single interface to perform these two separate functions would violate the ISP principle.twilio-breaks-srp tag. @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); } } private MagicCardService magicCardService; public TwilioController(MagicCardService magicCardService) { this.magicCardService = magicCardService; } /twilio and /magic_proxy/{card_id} . The magic_proxy path requires a little explanation, so we first analyze it before talking about violation of the SRP principle. http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&type=card <Response> <Message> <Body/> <Media>http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&​amp;type=card</Media> </Message> </Response> &​amp; . <Response> <Message> <Body/> <Media> <![CDATA[http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&type=card]]> </Media> </Message> </Response> <Response> <Message> <Body/> <Media> http://<my magic host>/api/v1/magic_proxy/144276 </Media> </Message> </Response> /magic_proxy , and already behind the scenes, the proxy receives a picture from the Magic the Gathering site and issues it.A class must have only one function.
twilio-fixes-srp tag, 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); } } TwilioController function is to issue TwiML code.runtime field checks that the classes for a particular module are * not * available at compile time. They are available only at run time. This helps to realize the principle of DIP.modules-ftw tag. You can see that the project organization has radically changed (as seen in IntelliJ):
magic-app module, then from pom.xml can see how it relies on other modules: <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> magic-impl is in the runtime , and magic-api is in the compile .TwilioController we automatically bind to TwilioResponseService: @RestController @RequestMapping(API_PATH) public class TwilioController { private TwilioResponseService twilioResponseService; … } @RestController @RequestMapping(API_PATH) public class TwilioController { private TwilioResponseServiceImpl twilioResponseService; … } 
compile .runtime line from pom.xml - and you will see that then IntelliJ will happily find the TwilioResponseServiceImpl class.Map<String, Object> . This is a good trick for Spring Boot applications — to output any JSON response without worrying about the formal Java models that represent the structure of the response.slack-violates-lsp .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(); ... } SlackResponse class SlackResponse is an Attachments array, a text string and a response_type string.SlackResponse declared the type abstract , and the implementation functions of the getText and getResponseType methods fall on 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"; } } getText() method returns null . With this answer, the answer will contain * only * image. Text is returned only in the case of an error message. Here * clearly * smells like LSP.Objects in the program should be able to be replaced by their subtypes without changing the accuracy of the program.
master branch in a project on github. It refactored the SlackResponse hierarchy to match the LSP. public abstract class SlackResponse { @JsonProperty("response_type") public abstract String getResponseType(); } getResponseType() method.SlackInChannelImageResponse class SlackInChannelImageResponse is everything you need for the correct answer with a picture: 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"; } … } null .SlackResponse class: @JsonInclude(JsonInclude.Include.NON_EMPTY) and @JsonInclude(JsonInclude.Include.NON_NULL) .Software entities ... must be open for expansion, but closed for modification.
SlackResponse class. If we want to add support for other types of Slack responses to the application, we can easily describe these specifics in subclasses.SlackResponseServiceImpl class in the 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(); } } getInChannelResponseWithImage and getErrorResponse return a SlackResponse object.SlackResponse child objects are SlackResponse . Spring Boot and its built-in jackson mapper for JSON are smart enough to produce the correct JSON for the particular object that is featured inside.BASE_URL and SLACK_TOKENS .BASE_URL is the full path and name of your Heroku application. For example, my application is installed here: https://random-magic-card.herokuapp.com . Stick to the same format when choosing an application name: https://<app name>.herokuapp.com .SLACK_TOKENS field — we will return later and update this value with this Slack API token.https://<app name>.herokuapp.com . You should see the random Magic the Gathering card in your browser. If an error occurs, look at the error log in the Heroku web interface. Here is an example of a web interface in action .Create New App button to get started:
App Name and select the Workspace workspace where you will add the application:
Slash Commands slash commands on the left, and then there is a button to create a new Create New Command :
/magic ), Request URL (for example: https://<your app name>.herokuapp.com/api/v1/slack ) and a short description. Then click Save .

Basic Information section in the left pane and expand the Install app to your workspace section on the screen. Click the Install app to Workspace button.

Basic Information screen where you returned, and record the verification token.
SLACK_TOKENS property SLACK_TOKENS following command: heroku config:set \ SLACK_TOKENS=<comma separated tokens> \ --app <your heroku app name> SLACK_TOKENS in the settings.

Programmable SMS :
Messaging Services :

Friendly Name , select Notifications, 2-Way in the Use Case column and click the Create button:
Process Inbound Messages in Process Inbound Messages and enter the Request URL for your Heroku application (for example, https://<your app name>.herokuapp.com/api/v1/twilio ):
Save button to save the changes.Numbers section in the left menu and make sure that your Twilio number has been added to the messaging service:
magic to your number as a text message:
magic (regardless of case), the error message shown above will pop up.twilio-fixes-srp . Splits the TwilioController controller into two parts, where each controller has only one function.master . The SlackResponse class SlackResponse solid and cannot be changed. It can be expanded without changing the code of the existing service.master . None of the SlackResponse child classes return null , it does not contain unnecessary classes or annotations.slack-first-pass through master . The MagicCardService and SlackResponseService perform different functions and therefore are separated from each other.slack-first-pass through master . Dependent services are automatically associated with controllers. Implementing a controller is the “best practice” of dependency injection.application/x-www-form-urlencoded, not more modern ones application/json. Because of this, there are difficulties with processing incoming JSON data with Spring Boot.Source: https://habr.com/ru/post/343966/
All Articles