This library defines a series of interfaces that regulate cases of looking up global resources dynamically and handling weak coupling components.
Component
is what can be found in scope, despite it is stateful or has dependencies.Dependent
is a type ofComponent
. It wants to find other components in the scope. When a new component is set up to the scope, dependents'handle
will be called to get the information of this component, and the dependent may save the reference of that component.DynamicScope
is the scope we talked in the two previous concepts. It resolves the dependency relationship between components.
There is an example that shows how this library can be used to build a robot hierarchy.
Imagine that we have a such definition of motors:
class Motor(name: String, inverse: Boolean) : NamedComponent<Motor>(name) {
fun setPower(power: Double): Unit = {
// ...
}
}
NamedComponent
is a type of Component
which has a name. We can find it by name in the scope. A
motor can be set a power to run on. It also has a name
and inverse
which indicates that if the
direction of the motor should be inverted. This depends on how the motor was installed in real life.
Next, we have the definition encoders:
class Encoder(name: String, inverse: Boolean) : NamedComponent<Encoder>(name) {
fun getPosition(): Double {
//...
}
fun getSpeed(): Double {
//...
}
}
We can get the position and speed of an encoder. Similar to motors, encoders may need to inverse
as well. Motor
and Encoder
are real devices installed on our robot. In order to implement a
feedback control, we can assemble them together in a new structure:
class MotorWithEncoder(name: String) : Dependent,
NamedComponent<MotorWithEncoder>(name), ManagedHandler by managedHandler() {
private val pid = PID(/* args */)
private val motor: Motor by manager.must(name)
private val encoder: Encoder by manager.must(name)
var targetPosition: Double = .0
fun run() {
val delta = targetPosition - encoder.getPosition()
val output = pid.run(delta)
motor.setPower(output)
}
}
Dependent
means this component depends on other components,
and ManagedHandler by managedHandler()
creates a delegate that handles the dependencies. This is
the case that an encoder is installed with a motor, so we can know how much the motor run and
implement PID control. Two motors can driver a simple chassis:
class Chassis : Dependent, UniqueComponent<Chassis>(), ManagedHandler by managedHandler() {
private val left: MotorWithEncoder by manager.must("left")
private val right: MotorWithEncoder by manager.must("right")
fun translateToPosition(position: Double) {
left.targetPosition = position
right.targetPosition = position
}
}
UniqueComponent
indicates that Chassis
is a unique component in the scope. Also, it is
a Dependent
, where we use manager.must
to find MotorWithEncoder
in scope. In addition, We have
a distance sensor:
class DistanceSensor : UniqueComponent<DistanceSensor>() {
fun getDistanceToWall(): Double {
// ...
}
}
It is unique component, and can measure the distance to wall. A remote control can command our robot:
class RemoteControl : Dependent, UniqueComponent<RemoteControl>(),
ManagedHandler by managedHandler() {
private val chassis: Chassis by manager.must()
private val distanceSensor: DistanceSensor by manager.must()
fun translateRobot(position: Double) {
if (distanceSensor.getDistanceToWall() > position) {
chassis.translateToPosition(position)
}
}
}
It depends on Chassis
and DistanceSensor
. Again, we use the same trick to handle dependencies.
Finally, our robot is basically a dynamic scope:
val robot = scope {
fun setupMotorWithEncoder(name: String, inverse: Boolean) {
setup(Motor(name, inverse))
setup(Encoder(name, inverse))
setup(MotorWithEncoder(name))
}
setupMotorWithEncoder("left", false)
setupMotorWithEncoder("right", true)
setup(DistanceSensor())
setup(RemoteControl())
}
val remoteControl = robot.components.must<RemoteControl>().translateRobot(x)
We set up everything to the DynamicScope
, and the scope will help us deal with all dependencies.
No references passed through constructors, nor worries about instantiation orders!
We have seen how the manager was used to declare dependency. ManagedHandler by managedHandler()
is
trivial, and the following two code snippets are identical:
Manually:
class AAA : UniqueComponent<AAA>()
class BBB : Dependent, UniqueComponent<BBB>() {
val manager = DependencyManager()
val aaa: AAA by manager.must()
override fun handle(dependency: Component): Boolean = manager.handle(dependency)
}
managedHandler()
:
class AAA : UniqueComponent<AAA>()
class BBB : Dependent, UniqueComponent<BBB>(), ManagedHandler by managedHandler() {
val aaa: AAA by manager.must()
}
The library also provides an annotation style dependency injection:
class AAA : UniqueComponent<AAA>()
class CCC(name: String) : NamedComponent<CCC>(name)
class BBB : Dependent, UniqueComponent<BBB>() {
@Must
lateinit var aaa: AAA
@Maybe
@Name("ccc1")
var ccc: CCC? = null
@Must
lateinit var ccc2: CCC
private val injector by annotatedInjector()
override fun handle(dependency: Component): Boolean = injector.handle(dependency)
}
scope {
setup(AAA())
setup(CCC("ccc1"))
setup(CCC("ccc2"))
setup(BBB())
}
We need use annotatedInjector()
to create an injector for the dependent, and manually
delegate hanlde
method to the injector. @Must
declares a strict dependency with the type of the
field, and @Maybe
declares a weak dependency. @Name
can specify dependency's name. Field's name
will be used if no @Name
annotated.