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

オブジェクト指向アーキテクチャについて説明するとき、ポートおよびアダプタアーキテクチャのアイデアを思いつくことがよくあります。 ポイントは、ビジネスロジックを技術的な実装の詳細から分離し、それらを個別に変更できるようにすることです。 これにより、ビジネスやテクノロジーの変化に対応して操作できます。
ポートとアダプター
ポートとアダプターのアーキテクチャーの背後にある考え方は、ポートがアプリケーションの境界を表すことです。 ポートは、外部の世界と対話するものです。ユーザーインターフェイス、メッセージキュー、データベース、ファイル、コマンドラインプロンプトなどです。 ポートは世界中のアプリケーションインターフェースですが、アダプターはポートとアプリケーションモデル間の変換を提供します。

アダプタの役割( 設計パターンとして )は2つの異なるインターフェイス間の通信を提供することであるため、「アダプタ」という用語が正常に選択されました。
前に説明したように、Injection Dependencyを使用している場合は、ある種のポートとアダプターに頼る必要があります。
ただし、このアーキテクチャの問題は、それを実装するには多くの説明が必要と思われることです。
- Dependency Injectionに関する私の本は500ページの長さです。
- ロバート・マーチンのソリッド原則、パッケージ設計、コンポーネントなどに関する本。 700ページもかかります。
- 問題指向プログラミング-500ページ。
- などなど...
私の経験では、ポートとアダプターのアーキテクチャーの実装は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モスクワ会議のこれらのレポートに興味があるでしょう。
- ニコライ・グセフによるC#の関数型プログラミングに関する軽やかで病みつきになるストーリー
- パターンの専門家であるマーク・シーマンによる実践者向けの興味深いレポート「 依存性注入から依存性拒否まで 」
- タイププロバイダーに関するローマネボリンによる実用的で有用なストーリー:それらの使用方法、それらが解決する問題、およびそれらの作成方法
- Andrey Akinshininの「 パフォーマンステスト 」に関する基調講演。