言語ツールを使用したScalaでの検証を伴う依存性注入

Scalaの私の小さなDependency Injectionライブラリについてお話したいと思います。 私が解決したかった問題:実際の構築の前にディペンデンシーグラフをテストし、何か問題が発生した場合はできるだけ早く落下し、エラーが何であったかを正確に確認する機能。 これはまさに、 Scaldiの優れたDIライブラリに欠けているものです。 同時に、複雑になってマクロに陥るのではなく、構文の外部の透明性を保持し、可能な限り言語を使用したいと考えました。







また、クラスの実装を変更する必要がない最も単純で最も慣用的な方法として、コンストラクターを通じてDIに集中しているという事実にもすぐに注意を喚起したいと思います。







部分的な呼び出しを使用して、コンストラクターを関数としてScalaに渡すことができます。次に例を示します。







class A(p0: Int, p1: Int) Module().bind(new A(_: Int, _: Int))
      
      





レコードはかなり面倒です。明示的に渡すことができるコンストラクターを呼び出す定義済みの関数を使用する方が良い場合があります。







 class A(p0: Int, p1: Int) object A { def getInstance(p0: Int, p1: Int) = new A(p0, p1) } Module().bind(A.getInstance)
      
      





このスタイルの読みやすさは非常に優れているため、以下の例では使用を試みます。







使用例



build.sbt:







 libraryDependencies += "io.ics" %% "disciple" % "1.2.1"
      
      





輸入:







 import io.ics.disciple._
      
      





インスタンスを相互に実装する特定のクラスセットがあるとします。







ドメインエンティティ「ユーザー」







 case class User(name: String)
      
      





プリミティブ実装を持つ1つのメソッドを持つ、パラメーターとして管理ユーザーインスタンスを取るサービス







 class UserService(val admin: User) { def getUser(name: String) = User(name) }
      
      





このサービスのインスタンスをシングルトンの形式で作成したい-一度だけ作成されるようにするために、このクラスのインスタンスの静的カウンターを設定します(明確にするため、マルチスレッド実行を忘れます)。







 object UserService { var isCreated: Boolean = false def getInstance(admin: User) = { isCreated = true new UserService(admin) } }
      
      





このサービスに依存する条件付きコントローラー







 class UserController(service: UserService) { def renderUser(name: String): String = { val user = service.getUser(name) s"User is $user" } } object UserController { def getInstance(service: UserService) = new UserController(service) }
      
      





バインディングの種類



デフォルトでは、すべてのバインディングは遅延型であり(クラスのインスタンスはオンデマンドでのみ作成されます)、要求された回数だけ作成されます。







宣言にsingleton



メソッドを追加することもできます。この場合、コンポーネントは厳密に1回作成されます。 nonLazyメソッドはバインディングを遅延としてマークします。つまり、モジュールのbuild()



メソッドが呼び出されたときにこのコンポーネントが作成されます。 遅延できるのはシングルトンコンポーネントのみです。







DIscipleライブラリを使用して、依存関係グラフの作成がどのように見えるかを見てみましょう(バインダーを宣言する順序は重要ではないことに注意してください)。







 val depGraph = Module(). //  ,    singleton bind(UserController.getInstance _).singleton. //  ,           ,    , //    nonLazy,          build(). forNames('admin).bind(UserService.getInstance).singleton.nonLazy. //    ,   'admin bind(User("Admin")).byName('admin). //    ,    'customer.    //                   bind(User("Jack")).byName('customer). //        build()
      
      





注1 :おそらく、コントローラーの場合は関数としてパラメーターの転送を強制することに気づいたでしょうが、サービスの場合は-いいえ。 これは、引数のないby-name関数のバインド関数のオーバーロードがあるため、コンパイラーはパラメーターなしの関数をオブジェクトまたは関数として解釈する方法を理解できません。 誰かがこの軽微な矛盾を修正する方法を教えてくれたら嬉しいです。







使用法:







 assert(UserService.isCreated) //         build() println(depGraph[User]('customer)) //    User   customer println(depGraph[UserService].admin) //         println(depGraph[UserController].renderUser("George")) //      George
      
      





注2 :1つの引数を名前で受け取り、もう1つの引数をタイプで受け取る必要がある場合は、 *



演算子を使用できます。







 case class A(label: String) case class B(a: A, label: String) case class C(a: A, b: B, label: String) val depGraph = Module(). forNames('labelA).bind { A }. forNames(*, 'labelB).bind { B }. forNames(*, *, 'labelC).bind { C }. bind("instanceA").byName('labelA). bind("instanceB").byName('labelB). bind("instanceC").byName('labelC). build()
      
      





境界条件



不完全な依存関係セット



 val depGraph = Module(). bind { A("instanceA") }. bind { C(_: A, _: B, "instanceC") }. build()
      
      





この例では例外がスローされます。IllegalStateException:{Type [io.ics.disciple.B]}のバインディングが見つかりません。 (どこでもIllegalStateExceptionを使用する代わりに、例外の階層を作成する方が良いかもしれませんが、それまでは例外を処理していません)







周期的な依存関係の検出



 case class Dep1(label: String, d: Dep2) case class Dep2(d: DepCycle) case class DepCycle(d: Dep1) Module(). bind(Dep1("test", _: Dep2)). bind(Dep2). bind(DepCycle). build()
      
      





ここで例外がスローされます:IllegalStateException:依存関係グラフには循環依存関係が含まれます:({Type [io.ics.disciple.DepCycle]}-> {Type [io.ics.disciple.Dep1]}-> {Type [io.ics.disciple .Dep2]}-> {Type [io.ics.disciple.DepCycle]})







ポリモーフィックバインディング



デフォルトでは、コンポーネントはbind()



渡された関数の結果の最終タイプによってバインドされますが、多くの場合、これは特性によってコンポーネントをバインドする必要がある場合など、期待どおりの動作ではありません。







 trait Service class ServiceImpl extends Service val depGraph = Module(). bind(new ServiceImpl(): Service). build()
      
      





ボンネットの下



実装の詳細には触れずに、一般的な概念を伝えようとします。 .bind()



メソッドを呼び出すことにより、ペアのリスト(DepId, List[Dep])



(DepId, List[Dep])



。ここで、 DepId



は結果タイプの説明、または識別子でもあります。







 sealed trait DepId case class TTId(tpe: Type) extends DepId { override def toString: String = s"{Type[$tpe]}" } case class NamedId(name: Symbol, tpe: Type) extends DepId { override def toString: String = s"{Name[${name.name}], Type[$tpe]}" }
      
      





Depは、依存関係+ IDのリスト、依存するコンポーネントのラップされたコンストラクター関数(インジェクター)のペアです。







 case class Dep[R](f: Injector[R], depIds: List[DepId])
      
      





Scalaで頻繁に行わなければならないように、異なる数の引数に対してメソッドをオーバーロードするには、ボイラープレートを生成する必要があります。 そのような場所の1つがbind()



メソッドです。 しかし、幸いなことに、 sbt-boilerplateプラグインはこのアクティビティを少し悲しませます。 宣言の繰り返し部分を角括弧とラティスで囲むだけで、すべてのユニットをn、デュースをn + 1などで置き換えながら、プラグインはそれらを繰り返す必要があることを理解します。BindBoilerplate.scala.template その結果、テンプレートはコンパクトになり、これらの巨大なシートを手で維持する必要がなくなります。







build()



メソッドが呼び出されると、依存関係のリストがグラフ(つまり、DepId-> Depマップ)に変換され、DFSアルゴリズムを使用して循環依存関係の完全性と不在がチェックされます。その複雑さはO(V + E)で推定されます。 、Eはそれらの間の依存関係の数です。 何か問題が発生した場合、例外がスローされます。そうでない場合、DepGraphクラスのオブジェクトが返されます。これは、最終コンポーネントを取得するために既に使用できます: depGraph[T]



またはdepGraph[T]('Id)



-名前付きコンポーネントを取得する必要がある場合







コンポーネント名にStringではなくSymbolを使用しました。これにより、コード内の通常の文字列定数と識別子が即座に区別されるためです。 さらに、さらに、強制収容が行われます。これは、この場合に役立つ場合があります。







乾燥しているが、より詳細でサンプルが豊富な説明とソースコード








All Articles