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