機能アーキテクチャ-ポートとアダプター

Mark Seemannによる新しい記事を紹介します。 非常に多くの翻訳が行われているため、彼はここにアカウントがなくてもすぐにトップの著者になりそうです!



機能的アーキテクチャの興味深い点は何ですか? いわゆる「成功の穴」に陥る傾向があります。開発者は、良いコードを書くことを余儀なくされる状況に陥ります。









オブジェクト指向アーキテクチャについて説明するとき、ポートおよびアダプタアーキテクチャのアイデアを思いつくことがよくあります。 ポイントは、ビジネスロジックを技術的な実装の詳細から分離し、それらを個別に変更できるようにすることです。 これにより、ビジネスやテクノロジーの変化に対応して操作できます。





ポートとアダプター



ポートとアダプターのアーキテクチャーの背後にある考え方は、ポートがアプリケーションの境界を表すことです。 ポートは、外部の世界と対話するものです。ユーザーインターフェイス、メッセージキュー、データベース、ファイル、コマンドラインプロンプトなどです。 ポートは世界中のアプリケーションインターフェースですが、アダプターはポートとアプリケーションモデル間の変換を提供します。









アダプタの役割( 設計パターンとして )は2つの異なるインターフェイス間の通信を提供することであるため、「アダプタ」という用語が正常に選択されました。



前に説明したように、Injection Dependencyを使用している場合は、ある種のポートとアダプターに頼る必要があります。



ただし、このアーキテクチャの問題は、それを実装するには多くの説明が必要と思われることです。





私の経験では、ポートとアダプターのアーキテクチャーの実装はSisyphusの労力です。 それには多くの勤勉さが必要ですが、しばらく気を散らすと、ボルダーは再び倒れます。









オブジェクト指向プログラミングでポートおよびアダプタアーキテクチャを実装することは可能ですが、多大な労力が必要です。 それはとても難しいでしょうか?



チュートリアルとしてのHaskell



関数型プログラミングに純粋に興味を持って、Haskellを学ぶことにしました。 Haskellが唯一の関数型言語であったわけではありませんが、F#、Clojure、Scalaのいずれでも到達できないレベルのクリーンさを提供します。 Haskellでは、関数は、その型が別のことを示さない限り純粋です。 これにより、設計に注意を払い、副作用のある関数から純粋な関数を分離する必要があります。



Haskellを知らない場合、副作用のあるコードはIO(入力/出力)と呼ばれる特定の「コンテキスト」内にしか表示できません。 これはモナド型ですが、これは主なものではありません。 主なことは、関数の種類によって、それが純粋であるかどうかを判断できることです。 タイプのある関数



ReservationRendition -> Either Error Reservation
      
      





IO



型にIO



ため純粋です。 一方、次のタイプの関数:



 ConnectionString -> ZonedTime -> IO Int
      
      





返される型はIO Int



であるため、クリーンではありません。 これは、戻り値が整数であることを意味しますが、この整数は関数呼び出し間で変更できるコンテキストに由来します。



Int



を返す関数とIO Int



返す関数には根本的な違いがあります。 Haskellでは、 Int



を返す関数はen.wikipedia.org/wiki/Referential_transparencyにリンク透過的です。 これは、関数が同じ入力で同じ値を返すことが保証されていることを意味します。 一方、 IO Int



返す関数はそのような保証を提供しません。



Haskellでプログラムを作成するときは、不潔なコードをシステムの境界にシフトすることにより、純粋な関数の数を最大化するように努力する必要があります。 優れたHaskellプログラムには、純粋な関数の大規模なコアとI / Oシェルがあります。 おなじみですね。



一般的に、これはHaskell型システムがポートとアダプターのアーキテクチャを提供することを意味します。 ポートは入力/出力コードです。 アプリケーションの中核は、すべての純粋な機能です。 型システムは自動的に「成功の穴」にあなたを押し込みます。









Haskellは、純粋な関数と不純な関数を明確に区別できるため、優れた学習支援ツールです。 F#コードが「十分に機能する」かどうかをチェックするツールとして使用することもできます。



F#は主に関数型言語ですが、オブジェクト指向または命令型のコードを記述することもできます。 F#で「機能的な」方法でコードを記述すれば、Haskellに簡単に変換できます。 F#コードをHaskellに翻訳するのが難しい場合、おそらく機能していません。



以下は実際の例です。



F#でアーマーを受け入れ、最初の試み



私のPluralsightコースのF#によるテスト駆動開発 (省略された無料バージョンが利用可能です: http : //www.infoq.com/presentations/mock-fsharp-tdd )オンラインレストラン予約システムにHTTP APIを実装する方法を示します予約リクエストを受け入れます。 予約リクエストを処理する際の手順の1つは、レストランに予約を受け入れるのに十分な空き席があるかどうかを確認することです。 関数は次のようになります。



 // int // -> (DateTimeOffset -> int) // -> Reservation // -> Result<Reservation,Error> let check capacity getReservedSeats reservation =   let reservedSeats = getReservedSeats reservation.Date   if capacity < reservation.Quantity + reservedSeats   then Failure CapacityExceeded   else Success reservation
      
      





解説が示すように、 getReservedSeats



の2番目の引数は、 DateTimeOffset -> int



型の関数です。 check



機能はそれを呼び出して、要求された日付のすでに予約されている座席の数を取得します。



単体テスト中に、純粋な関数をスタブに置き換えることができます。次に例を示します。



 let getReservedSeats _ = 0 let actual = Capacity.check capacity getReservedSeats reservation
      
      





また、アプリケーションの最終ビルド中に、固定の固定戻り値を持つクリーン関数を使用する代わりに、データベースにクエリを実行して必要な情報を取得するクリーン関数を作成できます。



 let imp =   Validate.reservation   >> bind (Capacity.check 10 (SqlGateway.getReservedSeats connectionString))   >> map (SqlGateway.saveReservation connectionString)
      
      





ここで、 SqlGateway.getReservedSeats connectionString



は部分的に適用される関数であり、その型はDateTimeOffset -> int



です。 F#では、タイプによってそれが汚れていると言うことはできませんが、それは私がこの関数を書いたためであることを知っています。 この関数はデータベースを照会するため、参照クリーンではありません。



これはすべて、F#でうまく機能します。特定の機能がクリーンであるか、クリーンでないかは、ユーザーによって異なります。 imp



はこのアプリケーションのコンポジションルートで構成されているため、汚れた関数SqlGateway.getReservedSeats



およびSqlGateway.saveReservation



はシステム境界にのみ表示されます。 システムの残りの部分は、副作用から十分に保護されています。



機能的に見えますが、本当にそうですか?



Haskellフィードバック



この質問に答えるために、Haskellでアプリケーションの主要部分を作り直すことにしました。 空席を確認する最初の試みは、次のように直接翻訳されました。



 checkCapacity :: Int             -> (ZonedTime -> Int)             -> Reservation             -> Either Error Reservation checkCapacity capacity getReservedSeats reservation = let reservedSeats = getReservedSeats $ date reservation in if capacity < quantity reservation + reservedSeats     then Left CapacityExceeded     else Right reservation
      
      





これはコンパイルされ、一見すると有望に思えます。 関数タイプgetReservedSeats



- ZonedTime -> Int



IO



はこのタイプのどこにも現れないため、Haskellはそれがクリーンであることを保証します。



一方、予約済みの座席の数をデータベースから取得する関数を実装する必要がある場合、戻り値が変更される可能性があるため、その性質上、汚れている必要があります。 Haskellでこれを有効にするには、関数は次のタイプでなければなりません。



 getReservedSeatsFromDB :: ConnectionString -> ZonedTime -> IO Int
      
      







ConnectionString



に最初の引数を部分的に適用できますが、戻り値はIO Int



ではなくIO Int



になります。



ZonedTime -> IO Int



ような関数はZonedTime -> IO Int



と同じではありません。 IOコンテキスト内で実行された場合でも、 ZonedTime -> IO Int



をZonedTime -> Int



変換することはできません。



または、IOコンテキスト内で不純な関数を呼び出し、IO Int



からInt



を抽出することもできます。 これはcheckCapacity



のcheckCapacity



関数と完全には一致しないため、設計を再考する必要があります。 F#のコードは「非常に機能的」に見えましたが、この設計は実際には機能的ではないことがわかりました。



checkCapacity



のcheckCapacity



関数を注意深く見ると、なぜ予約済みの場所の数を決定するために関数を渡す必要があるのか​​疑問に思うかもしれません。 なぜその番号を渡さないのですか?



 checkCapacity :: Int -> Int -> Reservation -> Either Error Reservation checkCapacity capacity reservedSeats reservation =   if capacity < quantity reservation + reservedSeats   then Left CapacityExceeded   else Right reservation
      
      





とても簡単です。 システムの境界では、アプリケーションはIOコンテキストで実行され、クリーンな関数とアンクリーンな関数を作成できます。



 import Control.Monad.Trans (liftIO) import Control.Monad.Trans.Either (EitherT(..), hoistEither) postReservation :: ReservationRendition -> IO (HttpResult ()) postReservation candidate = fmap toHttpResult $ runEitherT $ do r <- hoistEither $ validateReservation candidate i <- liftIO $ getReservedSeatsFromDB connStr $ date r hoistEither $ checkCapacity 10 ir >>= liftIO . saveReservation connStr
      
      





(完全なソースコードはこちらから入手できます: https : //gist.github.com/ploeh/c999e2ae2248bd44d775 )



この構成のすべての詳細を理解していなくても心配しないでください。 以下の要点について説明しました。



postReservation



関数は、 ReservationRendition



を受け取り(これをJSONドキュメントと見なします)、 IO (HttpResult ())



を返します。 IO



は、このすべての機能がIOモナドで実行されることを通知します。 言い換えれば、関数は汚れています。 これはシステムの境界であるため、驚くことではありません。



また、 liftIO



関数liftIO



2回呼び出されることに注意してください。 その機能を詳細に理解する必要はありませんが、IOタイプから値を「プル」する必要があります。 つまり、たとえば、 IO Int



からInt



をプルします。 したがって、コードがどこできれいでどこがきれいでないかが明らかになりますliftIO



関数liftIO



getReservedSeatsFromDB



とsaveReservation



適用されます。 これは、これら2つの機能が汚れていることを示しています。 例外メソッドでは、残りの関数( validateReservation



、 checkCapacity



およびtoHttpResult



)は純粋です。



また、純粋な関数と不純な関数をどのように交代させることができるかという問題も生じます。 よく見ると、データがpure validateReservation



関数からgetReservedSeatsFromDB



関数にどのように転送されているかがわかり、両方の戻り値( r



およびi



)がpure checkCapacity



関数に渡され、最後にunclean saveReservation関数に渡されます。 これはすべて(EitherT Error IO) () do



ブロックで発生するため、これらの関数のいずれかがLeft



返した場合、関数は閉じて最終エラーを生成します。 どちらかのタイプのモナドの明確で視覚的な紹介については、Scott Wlaschinの優れた記事であるRailway Oriented Programming (EN)を参照してください。

この式の値は、組み込み関数runEitherT



を使用して取得されます。 そして再びこのクリーンな機能で:



 toHttpResult :: Either Error () -> HttpResult () toHttpResult (Left (ValidationError msg)) = BadRequest msg toHttpResult (Left CapacityExceeded) = StatusCode Forbidden toHttpResult (Right ()) = OK ()
      
      





postReservation



関数全体postReservation



汚れており、IOを処理するためシステムの端にあります。 同じことがgetReservedSeatsFromDB



およびsaveReservation



もsaveReservation



。 以下の図の下部に、データベースを操作するための2つの関数を意図的に配置しました。これにより、マルチレベルのアーキテクチャ図に慣れている読者には馴染みのあるものになります。 円の下に、データベースを表す円筒形のオブジェクトがあると想像できます。









validateReservation



およびtoHttpResult



は、アプリケーションモデルに属するtoHttpResult



として表示できます。 これらはクリーンで、外部データ表現と内部データ表現の間で変換されます。 最後に、必要に応じて、 checkCapacity



関数はアプリケーションドメインモデルの一部です。



F.での最初の試みの設計のほとんどは、 Capacity.check



関数を除いて保持されました。 Haskellでデザインを再実装すると、F#のコードに適用できる重要な教訓が得られました。



F#での鎧の受け取り、さらに機能的



必要な変更は小さいため、HaskellのレッスンはF#ベースのコードに簡単に適用できます。 主な原因はCapacity.check



関数で、次のように実装する必要があります。



 let check capacity reservedSeats reservation =   if capacity < reservation.Quantity + reservedSeats   then Failure CapacityExceeded   else Success reservation
      
      





これは実装を単純化するだけでなく、構成をもう少し魅力的にします。



 let imp =   Validate.reservation   >> map (fun r ->       SqlGateway.getReservedSeats connectionString r.Date, r)   >> bind (fun (i, r) -> Capacity.check 10 ir)   >> map (SqlGateway.saveReservation connectionString)
      
      





これはHaskell関数よりも少し複雑に見えます。 Haskellの利点は、 do



ブロック内でMonad



クラスを実装する任意の型を自動的に使用できることです。 (EitherT Error IO) ()



はMonad



インスタンスであるため、 do



構文は無料です。



F#でも同様のことができますが、結果タイプの計算式の独自のコンストラクタを実装する必要があります。 ブログで説明しました。



まとめ



優れた機能設計は、ポートおよびアダプターのアーキテクチャーと同等です。 Haskellを「理想的な」機能アーキテクチャの基準として使用すると、純粋な関数と不純な関数を明示的に区別することで、いわゆる「成功のピット」がどのように作成されるかがわかります。 IOモナド内でアプリケーション全体を記述しない場合、Haskellは自動的に違いを反映し、外部の世界とのすべての通信をシステムの境界にプッシュします。



F#などの一部の機能言語は、この区別を明示的に使用しません。 ただし、F#では、非公式に実装し、システムの境界にある不純な機能を持つアプリケーションを構築するのは簡単です。 この区別は型システムによって課されるものではありませんが、それでも自然なようです。






関数型プログラミングのトピックがこれまで以上に関連している場合、おそらく2日間の11月のDotNext 2017モスクワ会議のこれらのレポートに興味があるでしょう。






All Articles