実用的な原則:SlackからTwilioまで





今日では、 RESTful APIが完全に存在するようです。 支払いから予約テーブル、簡単な通知から仮想マシンの展開まで-ほぼすべてが簡単なHTTPインタラクションを通じて利用できます。



独自のサービスを開発している場合、多くの場合、複数のプラットフォームで同時に動作するようにしたいことがあります。 OOD(オブジェクト指向設計)の昔からの原則により、コードの復元力が高まり、拡張性が簡素化されます。



この記事では、SOLID(これは頭字語です)と呼ばれる特定の設計アプローチを検討します。 Slack統合を使用してサービスを作成する際に実際に使用し、 Twilioで使用するために拡張します。



このサービスは、マジックザギャザリングカードをランダムに送信します。 今すぐ動作を確認したい場合は、 マジックという言葉を1-929-236-9306に送信します(米国とカナダのみ-MMS経由で画像を受信するため、オペレーターの料金が適用される場合があります)。 こちらをクリックして、Slack組織に参加することもできます 。 ログイン後、 / magicと入力します。



SOLID for Magic



SOLIDを初めて使用する場合、これはボブ・マーティンおじさんが普及させたオブジェクト指向設計(OOD)の原則のセットです。 SOLIDは以下の頭字語です。





この一連の原則に従うと、コードのフォールトトレランスが向上し、拡張性が簡素化されます。 記事の後半で、これらの各原則について詳しく説明します。



さまざまな言語の多くの良い例があります。 有名なShape



Circle



Rectangle



Area



例を繰り返す代わりに、実世界の完全に機能するアプリケーションでSOLIDの利点を示したいと思います。



私は最近Slack APIでプレイしました。 スラッシュを使用して独自のチームを作成するのは非常に簡単です。 私はマジックザギャザリングの大ファンでもあるので、マジックザギャザリングのランダムカードの画像を生成するSlackスラッシュコマンドを作成するというアイデアを思いつきました。



私はすぐにSpring Bootで計画を実行しました。 後で見るように、Spring Bootはすぐに使用できるいくつかの固い原則に従います。



Twilioには、優れた音声およびテキストメッセージングAPIがあります。 Slackの例を取り上げてTwilioと統合することがどれほど簡単かを見るのは面白いと思いました。 アイデアは、チームとテキストメッセージを既知の電話番号に送信し、マジックザギャザリングのランダムな画像を取得することです。



以下は、このプログラミング演習中の実際の動作中のSOLID原則(故障)の内訳です。



すべてのコードはここにあります 。 必要に応じて、このコードをご自身のSlackやTwilioアカウントに適用する方法については後ほど説明します。



最初の実行:Slackを使用したマジック



Spring Bootを使用してMagicアプリケーションを作成するという事実だけで、特別な労力をかけることなく、5つのSOLID原則のうち2つが即座に提供されます。 ただし、正しいアプリケーションアーキテクチャについては引き続き責任があります。



コードの作成プロセスでさまざまな原則を学習するため、GitHubプロジェクトで対応するタグを確認することで、いつでもコードサンプルを見ることができます(「リリース」セクションで確認できます)。 この章の完全なコードは、 slack-first-pass



タグで表示されます。



SlackController



コードを見てみましょう(すべてのJavaソースはここにあります:magic-app / src / main / java / com / afitnerd / magic)。これはSOLIDのD



I



原則の例です。



 @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()); } }
      
      





DIP:依存関係の逆転の原則



DIPの原理は次のとおりです。



A.上位レベルのモジュールは、下位レベルのモジュールに依存しないようにしてください。 どちらのタイプのモジュールも抽象化に依存する必要があります。



B.抽象化は詳細に依存するべきではありません。 詳細は抽象化に依存する必要があります。


JavaとSpring Bootを使用すると、この原則を非常に簡単に実装できます。 SlackController



*実装* MagicCardService



サービス。 これは、Javaインターフェースであるため、*抽象化*です。 これはインターフェースであるため、詳細はありません。



MagicCardService



の実装はMagicCardService



ません。 後ほど、アプリケーションをモジュールに分割して、インターフェイスとその実装をこのように分離する方法を確認します。 Spring Bootで依存関係を実装する他の現代的な方法を見ていきます。



ISP:インターフェース分離の原理



ISPの原則は次のように述べています。



多くの個別のクライアントインターフェイスは、1つのユニバーサルインターフェイスよりも優れています。


SlackController



MagicCardService



SlackResponseService



2つの個別のインターフェイスを実装しSlackResponseService



。 それらの1つは、マジックザギャザリングのサイトと対話します。 別の人はSlackと対話します。 これら2つの個別の機能を実行する単一のインターフェイスを作成すると、ISPの原則に違反します。



次:Twilioでの「マジック」



この章のコードを追跡するには、 twilio-breaks-srp



タグを参照してください。



TwilioControllerコードを見てください:



 @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); } }
      
      





前に述べたように、中毒の実装にはより現代的なアプローチを適用します(ベストプラクティス)。 ご覧のとおり、Spring Boot Constructor Injectionでこれを行いました。 これは、Spring Bootの最新バージョンでは、依存関係の注入が次のように行われることを示す良い方法です。



1.クラスに1つ以上の非表示フィールドを設定します。次に例を示します。



 private MagicCardService magicCardService;
      
      





2.セットの非表示フィールドのコンストラクターを定義します。



 public TwilioController(MagicCardService magicCardService) { this.magicCardService = magicCardService; }
      
      





Spring Bootは、実行時にオブジェクトの実装を自動的に処理します。 利点は、ここで、コンストラクター内の埋め込みオブジェクトでエラーチェックと検証を実行できることです。



コントローラーには、 /twilio



/magic_proxy/{card_id}



2つの部分が含まれています。 magic_proxyパスには少し説明が必要なので、まずSRPの原則に違反することについて説明する前に説明します。



TwiMLをお楽しみください



TwiMLはTwilioマークアップ言語です。 TwiMLはTwilioの指示であるため、これはすべてのTwilioの回答の基礎です。 また、XMLです。 通常、これは問題ではありません。 ただし、Magic the Gatheringサイトから返されるURLは、TwiMLドキュメントに含めるには問題があります。



Magic the Gatheringマップ画像が抽出されるURLは次のようになります。



 http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&type=card
      
      





URLのアンパサンド(&)に注意してください。 アンパサンドをXMLドキュメントに埋め込むには、2つの有効な方法しかありません。



1.エスケープ文字



 <Response> <Message> <Body/> <Media>http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&​amp;type=card</Media> </Message> </Response>
      
      





ここでは、アンパサンドの代わりに、 &​amp;







2. CDATAフラグメント(文字データ)



 <Response> <Message> <Body/> <Media> <![CDATA[http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=144276&type=card]]> </Media> </Message> </Response>
      
      





これらのオプションはいずれも、Spring Bootに組み込まれたJackson JSONプロセッサーのJackson Dataformat XML拡張機能を使用して、Javaで簡単に実装できます。



問題は、Wizards of the Coast Webサイト(マジックザギャザリングメンテナー)から画像を受信するときに最初のオプションがエラーになり、Twilioでは2番目のオプションがサポートされないことです(ちょっとTwilio:TwiMLでCDATAサポートを実装しますか?)



リクエストのプロキシを使用してこの制限を回避しました。 この場合、TwiMLコードが生成されます:



 <Response> <Message> <Body/> <Media> http://<my magic host>/api/v1/magic_proxy/144276 </Media> </Message> </Response>
      
      





このようなコードを受け取ると、Twilioはエンドポイント/magic_proxy



参照し、すでに舞台裏でプロキシがMagic the Gathering Webサイトから画像を受信して​​発行します。



現在、SOLIDの原理の研究を続けています。



SRP:単独の責任原則



SRPの原則は次のとおりです。



クラスには1つの関数のみが必要です。


上記のコントローラーはそのまま動作しますが、TwiML応答と画像のプロキシの両方を返すため、SRPに違反します。



この例ではこれは大きな問題ではありませんが、状況がすぐに手に負えなくなることは容易に想像できます。



twilio-fixes-srp



タグに従うと、 twilio-fixes-srp



と呼ばれる新しいコントローラーが表示され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); } }
      
      





その唯一のタスクは、Magic the Gathering Webサイトからプロキシで受信した画像のバイトを返すことです。



現在、唯一のTwilioController



関数はTwiMLコードを発行することです。



DIP実装用のモジュール



Mavenを使用すると、プロジェクトをモジュールに簡単に分割できます。 スコープは異なっていてもかまいませんが、コンパイル(デフォルト)、実行、テストという同じスコープがあります。



モジュールがそのエリアでアクティブになると、エリアが制御を取得します。 runtime



スコープは、特定のモジュールのクラスがコンパイル時に*利用できない*ことを確認します。 それらは実行時にのみ利用可能です。 これは、DIP原則の実装に役立ちます。



例で簡単に表示できます。 modules-ftw



タグのコードを確認してください。 プロジェクトの構成が根本的に変更されたことがわかります(IntelliJで見られるように)。







現在、4つのモジュールがあります。 magic-app



モジュールを見ると、 pom.xml



から他のモジュールにどのように依存しているかがわかります。



 <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



runtime



にあり、 magic-api



compile



ことに注意してください。



TwilioController



自動的にバインドします。



 @RestController @RequestMapping(API_PATH) public class TwilioController { private TwilioResponseService twilioResponseService; … }
      
      





実装されたクラスをこの方法で自動的にバインドしようとするとどうなるか見てみましょう。



 @RestController @RequestMapping(API_PATH) public class TwilioController { private TwilioResponseServiceImpl twilioResponseService; … }
      
      









IntelliJはTwilioResponseServiceImplクラスを見つけることができません。これは、 compile



スコープ内に*ない*ためです。



楽しみのために、 pom.xml



からruntime



行を削除してみてくださいTwilioResponseServiceImpl



クラスを見つけてTwilioResponseServiceImpl



ことがTwilioResponseServiceImpl



ます。



これまで見てきたように、mavenモジュールとスコープを組み合わせることで、DIPの原則を実装できます。



フィニッシュライン:スラックリファクタリング



このアプリケーションを初めて作成したとき、SOLIDについては考えませんでした。 Slackアプリをハックして、スラッシュコマンドの機能を操作したかっただけです。



最初のバージョンでは、すべてのSlack関連のサービスとコントローラーはMap<String, Object>



返しました。 これは、Spring Bootアプリケーションにとって良いトリックです。応答の構造を表す正式なJavaモデルを心配することなく、JSON応答を発行できます。



アプリケーションの開発に伴い、読み取り可能で信頼性の高いコードのより正式なモデルを作成したいという要望がありました。



slack-violates-lsp



のソースコードを参照してください。



magic-api



モジュールのSlackResponse



クラスを見てみましょう。



 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



クラスにはAttachments



配列、テキスト文字列、およびresponse_type



文字列があることがSlackResponse



ます。



SlackResponse



は型abstract



宣言し、 getText



およびgetResponseType



メソッドの実装関数は子クラスに分類されます。



SlackInChannelImageResponse



子クラスの1つを見てみましょう。



 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()



メソッドはnull



返しnull



。 この回答では、回答には*画像のみが含まれます。 テキストは、エラーメッセージの場合にのみ返されます。 ここ*明らかに* LSPのにおいがします。



LSP:バーバラリスク代替原理



LSPの原則は次のとおりです。



プログラム内のオブジェクトは、プログラムの精度を変更せずにサブタイプに置き換えることができる必要があります。


継承階層を処理していて、子クラス*が常に* nullを返す場合、これはLSP原則違反の明確な兆候です。 子クラスにはこのメソッドは必要ありませんが、親クラスに記述されているインターフェイスのために実装する必要があるためです。



GitHubプロジェクトのmaster



ブランチをご覧ください。 そこで、 SlackResponse



階層はLSPに一致するようにSlackResponse



れました。



 public abstract class SlackResponse { @JsonProperty("response_type") public abstract String getResponseType(); }
      
      





これで、実装する必要があるすべての子クラスに共通する唯一のものはgetResponseType()



メソッドです。



SlackInChannelImageResponse



クラスには、正解に必要なすべての写真が含まれています。



 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



を返す必要はありません。



もう1つ小さな改善があります。以前、 SlackResponse



クラスにいくつかのJSONアノテーションがありSlackResponse



@JsonInclude(JsonInclude.Include.NON_EMPTY)



@JsonInclude(JsonInclude.Include.NON_NULL)



です。



値がゼロの空の添付ファイル配列またはテキストフィールドがJSONに入らないことを保証するために必要でした。 これらは強力な注釈ですが、そのため、モデルのオブジェクトは脆弱になり、他の開発者には何が起こっているのかがはっきりしない場合があります。



OCP:オープン/クローズド原則



SOLIDでの旅の最後の原則はOCPです。



OCPの原則は次のとおりです。



ソフトウェアエンティティ...は拡張のために開かれている必要がありますが、変更のために閉じられている必要があります。


考え方は、参照条件を変更するとき、クラスを拡張し、既存のクラスにコードを追加しない場合、コードは新しい要件により効果的に対処するということです。 これにより、コードが忍び寄るのを防ぎます。



上記の例では、 SlackResponse



クラスを変更する追加の理由はありません。 他のタイプのSlack回答タイプのサポートをアプリケーションに追加する場合、この特異性をサブクラスで簡単に説明できます。



ここでも、Spring Bootの強さは明らかです。 magic-impl



SlackResponseServiceImpl



クラスを見てください。



 @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



およびgetErrorResponse



SlackResponse



オブジェクトを返します。



これらのメソッド内で、さまざまなSlackResponse



子オブジェクトがSlackResponse



ます。 Spring BootとJSONに組み込まれているjackson-mapperは、特定のオブジェクトに対して正しいJSONを生成するのに十分なほどスマートであり、これは内部的に特徴付けられています。



Slackで組織に統合を提供するか、Twilioアカウントのサポート(またはその両方)を実装する場合は、先に進んでください! それ以外の場合は、記事の最後にある履歴書にアクセスできます。



アプリケーションの展開



このアプリケーションを最大限に使用する場合は、Herokuにアプリケーションをデプロイした後、SlackとTwilioを適切に構成する必要があります。



または、SlackまたはTwilioをインストールできます。 いずれの場合でも、最初に行う必要があるのは、アプリケーションをHerokuにデプロイすることです。 幸いなことに、それは簡単です。



Herokuの展開



アプリケーションをHerokuにデプロイする最も簡単な方法は、GitHubプロジェクトのREADMEセクションにある紫色のフレンドリーなボタンをクリックすることです。 BASE_URL



SLACK_TOKENS



2つの詳細を指定する必要があります。



BASE_URL



は、Herokuアプリケーションの完全なパスと名前です。 たとえば、 https//random-magic-card.herokuapp.comにアプリケーションをインストールしています 。 アプリケーション名を選択するときは、 https://<app name>.herokuapp.com



と同じ形式に従いhttps://<app name>.herokuapp.com







HerokuにはSlackからの情報が必要であり、Slackの統合にはHerokuについての情報が必要であるため、ここには鶏卵に関する問題があります。 最初は、デフォルト値をSLACK_TOKENS



フィールドに残しておくことができます。後でこの値を返し、現在のSlack APIトークンで更新します。



https://<app name>.herokuapp.com



して、インストールを確認できhttps://<app name>.herokuapp.com



。 ブラウザにマジックザギャザリングマップがランダムに表示されます。 エラーが発生した場合は、HerokuアプリケーションのWebインターフェイスでエラーログを確認してください。 動作中のWebインターフェイスの例を次に示します



スラックのセットアップ



https://api.slack.com/appsに移動し、[ Create New App



Create New App



]ボタンをクリックして開始します。







名前App Name



を入力し、アプリケーションを追加するWorkspace



を選択します。







次に、左側のスラッシュコマンドSlash Commands



リンクをクリックします。新しいコマンドを作成するためのボタンがあります新しいコマンドのCreate New Command











コマンドの値(例: /magic



)、 Request URL



(例: https://<your app name>.herokuapp.com/api/v1/slack



)および短い説明を入力します。 次に[ Save



]をクリックしSave











Slackのスラッシュコマンドが完全に構成されました。







左ペインのInstall app to your workspace section



Basic Information



セクションに移動し、画面上のInstall app to your workspace section



Install app to your workspace section



Install app to your workspace section



を展開します。 [ Install app to Workspace



]ボタンをクリックします。







次に、承認のためのボタン:







戻ったBasic Information



画面までスクロールし、検証トークンを記録します。







Heroku CLIをインストールした場合、次のコマンドでSLACK_TOKENS



プロパティSLACK_TOKENS



正しく設定できます。



 heroku config:set \ SLACK_TOKENS=<comma separated tokens> \ --app <your heroku app name>
      
      





または、 Herokuダッシュボードに移動し、アプリケーションに移動して、設定のSLACK_TOKENS



値を変更します。



これで、組織のSlackチャンネルでスラッシュコマンドが機能するはずです。その代わりに、マジックザギャザリングカードを受け取ります。







Twilioのセットアップ



Twilio統合を構成するには、コンソールのTwilioダッシュボードに移動します。







省略記号をクリックして、 Programmable SMS



を選択します。







Messaging Services



選択しMessaging Services











赤いプラスの付いたボタンをクリックして、新しいメッセージングサービスを作成します(サービスがまだない場合は[新しいメッセージングサービスの作成]をクリックします)。







Friendly Name



入力し、[ Use Case



]列で[ Notifications, 2-Way



Friendly Name



を選択し、[ Create



]をクリックしCreate











Process Inbound Messages



チェックマークを確認し、HerokuアプリケーションのRequest URL



Request URL



します(例https://<your app name>.herokuapp.com/api/v1/twilio



):







[ Save



]ボタンをクリックして、変更を保存します。



左メニューの[ Numbers



セクションに移動し、メッセージングサービスにTwilio番号が追加されていることを確認します。







これで、 magic



という単語を番号にテキストメッセージとして送信して、Twilioサービスをテストできます。







**注:**大文字と小文字を問わず、 magic



という単語以外を送信すると、上記のエラーメッセージが表示されます。



ソリッドサマリー



もう一度、SOLIDテーブルを公開します。今回は、各原則に一致するGithubプロジェクトタグを使用します。





このアプリケーションの開発にはいくつかの困難があります。TwiMLの問題については、すでに上で述べました。しかし、Slackには、この記事で説明した特別な問題がありますTL; DR:Slackはapplication/x-www-form-urlencoded



、最新のコマンドではなく、スラッシュコマンドの*のみ* POSTリクエストを受け入れますapplication/json



これにより、Spring BootでJSON入力を処理することが難しくなります。



基本的な考え方は、SOLIDの原則により、コードの作業と拡張がはるかに簡単になるということです。



これで、SOLID原則の概要は終わりです。通常の単純なJavaの例よりも便利であることを願っています。



All Articles