This library defines a series of interfaces that regulate cases of looking up global resources dynamically and handling weak coupling components.
Componentis what can be found in scope, despite it is stateful or has dependencies.Dependentis a type ofComponent. It wants to find other components in the scope. When a new component is set up to the scope, dependents'handlewill be called to get the information of this component, and the dependent may save the reference of that component.DynamicScopeis 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:
classMotor(name:String, inverse:Boolean) : NamedComponent<Motor>(name) {
funsetPower(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:
classEncoder(name:String, inverse:Boolean) : NamedComponent<Encoder>(name) {
fungetPosition(): Double {
//...
}
fungetSpeed(): 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:
classMotorWithEncoder(name:String) : Dependent,
NamedComponent<MotorWithEncoder>(name), ManagedHandler by managedHandler() {
privateval pid =PID(/* args */)
privateval motor:Motor by manager.must(name)
privateval encoder:Encoder by manager.must(name)
var targetPosition:Double= .0funrun() {
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:
classChassis : Dependent, UniqueComponent<Chassis>(), ManagedHandler by managedHandler() {
privateval left:MotorWithEncoder by manager.must("left")
privateval right:MotorWithEncoder by manager.must("right")
funtranslateToPosition(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:
classDistanceSensor : UniqueComponent<DistanceSensor>() {
fungetDistanceToWall(): Double {
// ...
}
}It is unique component, and can measure the distance to wall. A remote control can command our robot:
classRemoteControl : Dependent, UniqueComponent<RemoteControl>(),
ManagedHandler by managedHandler() {
privateval chassis:Chassis by manager.must()
privateval distanceSensor:DistanceSensor by manager.must()
funtranslateRobot(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 {
funsetupMotorWithEncoder(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:
classAAA : UniqueComponent<AAA>()
classBBB : Dependent, UniqueComponent<BBB>() {
val manager =DependencyManager()
val aaa:AAA by manager.must()
overridefunhandle(dependency:Component): Boolean= manager.handle(dependency)
}managedHandler():
classAAA : UniqueComponent<AAA>()
classBBB : Dependent, UniqueComponent<BBB>(), ManagedHandler by managedHandler() {
val aaa:AAA by manager.must()
}The library also provides an annotation style dependency injection:
classAAA : UniqueComponent<AAA>()
classCCC(name:String) : NamedComponent<CCC>(name)
classBBB : Dependent, UniqueComponent<BBB>() {
@Must
lateinitvar aaa:AAA
@Maybe
@Name("ccc1")
var ccc:CCC?=null
@Must
lateinitvar ccc2:CCCprivateval injector by annotatedInjector()
overridefunhandle(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.