Scalaでの依存性注入について尋ねられたとき、非常に多くの回答が、ScalazのリーダーMonadを使用するか、または独自のリーダーMonadを使用することを指し示しています。アプローチの基本を説明する非常に明確な記事が多数あります(例 Runarの話 、 Jasonのブログ )が、より完全な例を見つけることができませんでした。そして、例えば、そのアプローチの利点を見ることができませんより伝統的な「マニュアル」DI( 私が書いたガイド を参照)。おそらく私はいくつかの重要なポイントを見逃しているので、質問です。
ちょうど例として、これらのクラスがあると想像してみましょう:
trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }
class FindUsers(datastore: Datastore) {
def inactive(): Unit = ()
}
class UserReminder(findUser: FindUsers, emailServer: EmailServer) {
def emailInactive(): Unit = ()
}
class CustomerRelations(userReminder: UserReminder) {
def retainUsers(): Unit = {}
}
ここでは、クラスとコンストラクターパラメーターを使用して物事をモデリングしています。これは、「従来の」DIアプローチと非常にうまく機能しますが、この設計にはいくつかの良い面があります。
UserReminder
は、FindUsers
がデータストアを必要とすることを知りません。機能は別々のコンパイル単位でも可能ですIO
モナドにラップされた値を返すことができます。Readerモナドでこれをどのようにモデル化できますか?上記の特性を保持して、各機能が必要とする依存関係の種類を明確にし、ある機能の依存関係を別の機能から非表示にすることをお勧めします。 class
esを使用する方が実装の詳細です。 Readerモナドを使用した「正しい」ソリューションでは、他の何かを使用する可能性があります。
やや関連する質問 を見つけました。
しかし、これらのソリューションのすべてで、そのような単純なことに関しては少し複雑すぎます(しかしそれは主観的です) retainUsers
メソッド(emailInactive
を呼び出し、inactive
を呼び出して非アクティブなユーザーを見つける)は、Datastore
依存関係について知る必要があります。ネストされた関数を適切に呼び出す-または間違っていますか?
このような「ビジネスアプリケーション」にReader Monadを使用することは、コンストラクターパラメーターを使用することよりもどの面で優れているでしょうか。
Readerモナドでこれをどのようにモデル化できますか?
これがshould Readerでモデル化されているかどうかはわかりませんが、次の方法で作成できます。
開始する直前に、この答えに有益だと感じた小さなサンプルコードの調整について説明する必要があります。最初の変更は、_FindUsers.inactive
_メソッドに関するものです。アドレスリストを_List[String]
_メソッドで使用できるように、_UserReminder.emailInactive
_を返すようにしました。また、メソッドに単純な実装を追加しました。最後に、このサンプルでは、以下の手巻きバージョンのReaderモナドを使用します。
_case class Reader[Conf, T](read: Conf => T) { self =>
def map[U](convert: T => U): Reader[Conf, U] =
Reader(self.read andThen convert)
def flatMap[V](toReader: T => Reader[Conf, V]): Reader[Conf, V] =
Reader[Conf, V](conf => toReader(self.read(conf)).read(conf))
def local[BiggerConf](extractFrom: BiggerConf => Conf): Reader[BiggerConf, T] =
Reader[BiggerConf, T](extractFrom andThen self.read)
}
object Reader {
def pure[C, A](a: A): Reader[C, A] =
Reader(_ => a)
implicit def funToReader[Conf, A](read: Conf => A): Reader[Conf, A] =
Reader(read)
}
_
たぶんそれはオプションであるかどうかはわかりませんが、後で理解の見栄えが良くなります。結果の関数はカリー化されていることに注意してください。また、最初のパラメーター(パラメーターリスト)として以前のコンストラクター引数を取ります。そのように
_class Foo(dep: Dep) {
def bar(arg: Arg): Res = ???
}
// usage: val result = new Foo(dependency).bar(arg)
_
になる
_object Foo {
def bar: Dep => Arg => Res = ???
}
// usage: val result = Foo.bar(dependency)(arg)
_
Dep
、Arg
、Res
の各タイプは完全に任意である可能性があることに注意してください。タプル、関数、または単純タイプです。
以下は、関数に変換された初期調整後のサンプルコードです。
_trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }
object FindUsers {
def inactive: Datastore => () => List[String] =
dataStore => () => dataStore.runQuery("select inactive")
}
object UserReminder {
def emailInactive(inactive: () => List[String]): EmailServer => () => Unit =
emailServer => () => inactive().foreach(emailServer.sendEmail(_, "We miss you"))
}
object CustomerRelations {
def retainUsers(emailInactive: () => Unit): () => Unit =
() => {
println("emailing inactive users")
emailInactive()
}
}
_
ここで注意すべきことは、特定の関数はオブジェクト全体に依存せず、直接使用される部分にのみ依存するということです。ここでOOP version UserReminder.emailInactive()
インスタンスはuserFinder.inactive()
を呼び出しますが、ここではinactive()
を呼び出します-最初に渡された関数パラメータ。
コードは、質問から3つの望ましいプロパティを示していることに注意してください。
retainUsers
メソッドは、データストアの依存関係について知る必要はありませんReaderモナドでは、すべてが同じ型に依存する関数のみを作成できます。これは多くの場合そうではありません。この例では、_FindUsers.inactive
_はDatastore
に依存し、_UserReminder.emailInactive
_はEmailServer
に依存します。その問題を解決するために、すべての依存関係を含む新しいタイプ(多くの場合、Configと呼ばれます)を導入し、すべての依存関係に依存するように関数を変更して、関連するデータのみを取得できます。これは、依存関係管理の観点から明らかに間違っています。なぜなら、これらの関数を、そもそも知らないタイプにも依存させるからです。
幸いなことに、関数が関数の一部のみをパラメーターとして受け入れる場合でも、Config
で機能する方法が存在することがわかりました。 Readerで定義されているlocal
というメソッドです。 Config
から関連部分を抽出する方法を提供する必要があります。
手元の例に適用されるこの知識は、次のようになります。
_object Main extends App {
case class Config(dataStore: Datastore, emailServer: EmailServer)
val config = Config(
new Datastore { def runQuery(query: String) = List("[email protected]") },
new EmailServer { def sendEmail(to: String, content: String) = println(s"sending [$content] to $to") }
)
import Reader._
val reader = for {
getAddresses <- FindUsers.inactive.local[Config](_.dataStore)
emailInactive <- UserReminder.emailInactive(getAddresses).local[Config](_.emailServer)
retainUsers <- pure(CustomerRelations.retainUsers(emailInactive))
} yield retainUsers
reader.read(config)()
}
_
このような「ビジネスアプリケーション」にReader Monadを使用することは、コンストラクターパラメーターを使用することよりもどの面で優れているでしょうか。
この答えを準備することにより、プレーンなコンストラクターをどの面で倒すかを自分で判断しやすくなることを願っています。それでも、これらを列挙する場合、ここに私のリストがあります。免責事項:私はOOPバックグラウンドを持っていますが、私はそれらを使用しないのでReaderとKleisliを完全に評価しないかもしれません。
local
呼び出しを振りかけるだけです。この点はIMOです。コンストラクタを使用するとき、誰かが愚かなことをしない限り、OOPで悪い習慣と見なされるコンストラクタでの作業をしない限り、誰もあなたが好きなものを作成することを妨げないからです。sequence
、traverse
メソッドは無料で実装されています。また、Readerで自分が気に入らないことを伝えたいと思います。
pure
、local
とのセレモニー、およびそのためのタプルを使用した独自のConfigクラスの作成。 Readerは、問題の領域に関するものではないコードを追加するように強制するため、コードにノイズが混入します。一方、コンストラクターを使用するアプリケーションは、ファクトリパターンを使用することが多く、これも問題ドメインの外部からのものであるため、この脆弱性はそれほど深刻ではありません。あなたが欲しい。技術的にはcanそれを避けますが、FindUsers
クラスをオブジェクトに変換しなかった場合にどうなるかを見てください。理解のためのそれぞれの行は次のようになります。
_getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
_
それはそんなに読めません、そうですか?重要なのは、Readerが関数を操作するため、まだ持っていない場合は、インラインで構築する必要があることです。
主な違いは、この例では、オブジェクトがインスタンス化されるときにすべての依存関係を注入することです。 Readerモナドは基本的に、依存関係を指定して呼び出す複雑な関数を作成し、それから最上位のレイヤーに戻ります。この場合、関数が最後に呼び出されたときに注入が行われます。
直接的な利点の1つは、特にモナドを一度構築してから、異なる注入された依存関係でモナドを使用したい場合の柔軟性です。欠点の1つは、あなたが言うように、潜在的に明快さが低いことです。どちらの場合も、中間層はそれらの直接の依存関係を知るだけでよいため、どちらもDIの広告として機能します。