We want to build services which can easily be attached. With simple and consistent rules we can make a platform which supports much more complicated systems.
When you connect two things together you don't always have a concept of direction. For example, when you connect a Servo and Arduino together, are you connecting the Servo to the Arduino, or the Arduino to the Servo ? It's ambiguous. In order to help people "connect" the services together, I suggest the code allow either direction ?
Both servo.attach(arduino, 7) or arduino.attach(servo, 7) would be valid.
Although we made it easy for the "user", the ambiguity can potentially create a maintenance nightmare. The "same" code could exist in each attach. This would be very unfortunate, because :
If you have the same code in 2 places, you can guarantee that one of them will be wrong ! This happens to non-normalized code falling out of sync because of the necessity of dual-maintenance. I may fix something in servo.attach, but perhaps I (most likely) forgot to update arduino.attach.
How do we make it "easy" for the user, yet low maintenance ?
I believe we can have highly attachable low maintenance services if we follow the Control-Controller-Manifesto .
In summary it says :
-
Contain all the complex logic in an attach method which only takes the other service,
for example : arduino.attach(servo) - aditional methods with parameters would just set attributes on the service before calling the single parameter method.public void attach(ServoControl servo, int pin) throws Exception { servo.setPin(pin); attach(servo); }
-
The first thing an attach method should do is check to see if the paramaterized service is "already" attached.
public void attach(ServoControl servo) throws Exception { if (isAttached(servo)) { log.info("servo {} already attached", servo.getName()); return; } ...
-
The last thing an attach metho should do is call attach on the parameterized service with itself as a parameter.
// ... servo.attach(this); }
- The body of the attach should "only" work on changing itself to support the new connection, it should not change attributes of the parameterized service.
With these 4 simple design rules we can provide the end user with either arduino.attach(servo) or servo.attach(arduino). They have identical results, because under the hood they call each other.
It also give the opportunity for both services to take actions to successfully attempt to attach. Mats suggested thinking about services which were "resource users" consuming resources from "resource providers". But what if both are simultaneously consumers and producers ?
For example, SpeechSynthesis & SpeechRecognition.
Currently, SpeechSynthesis has a method addEar and SpeechRecognition addMouth.
I think it should be
speech.attach(recognizer)
This single call, would produce the same results as calling both speech.addEar(recognizer) & recognizer.addMouth(speech). If the 4 design rules above were followed, I think we would have a good map to the future.
References :
backwards compatability
I'm all for renaming the addEar / addMouth to attach.. but we should keep the old method signatures around for script compatability.
so.. addEar would call attach under the covers for example.
I agree
I agree
Attach direction
I'm thinking a bit from GUI perspective.
And a basic rule fore the GUI's should be that they only should talk to their own services. Like ServoGui ( in it's two flavours ) should only talk to the Servo service, and perhaps in some cases to Runtime. Some for the ArduinoGui, It should only talk to the Arduino service. Widgets may have different rules, but I think they are very closley conneted to being "views" of a class.
So seen from that perspective, a servo.attach(ServoController controller) makes sense for the Gui.
To use the alternate direction arduino.attach(ServoControl servo) would require the GUI to speak to an external service, or alternativley the ArduioGui would use the arduino.attach(ServoControl servo).
I think I rembemer seeing an image of an erarly Arduino gui built that way. It is possible to build the ArduinoGui that way, but I don't think its the best way to do it.
I'm also thinking in the way of what service has the initiative. And the servo tells the Arduino what to do, not the other way around, even if the Arduino may publish data back to the Servo service.
----------------------------------------------------------------------------------------------------------------------------------
I'm trying to figure out how your suggestion would fit with for example the DiyServo.
It uses 3 different other services ( or more correct 2 interfaces and one service ).
It uses the PID2 service, and starts it as a peer. I don't see any problem with that.
But the it also needs to drive a Motor. First I used the Motor service, because I wanted to avoid having to build the same configuration as was already implemented in Motor. But the I changed to use the MotorController interface. The MotorController interface is implemented in at least 3 different services. Arduino, Adafruit16CServoDriver and Sabertooth. So either should be possible to use.
And the DiyServo needs to get some analog input. The analog input uses the PinArrayControl interface, and it's implemented in at least two services. Arduino and Ads1115.
The way I have been thinking about it this far, is that the DiyServo service needs two attach,
DiyServo.attach(MotorController motorController) and DiyServo.attach(PinArrayControl pinArrayControl)
If it should be the other way around I don't really now how the arduino.attach(DiyServo ...Control) should look like, since the Arduino service can be used in two alternate ways. Either as a MotorController or a PinArrayControl.
-----------------------------------------------------------------------------------------------------------------------------------------
For the example of speech.attach(recognizer)
To me that is saying that a speech service wants to use some methods in the recognizer. Like stop listening while the speech service is saying something.
The other way around recognizer.attach(speech) would imply that the recognizer wants to use some methods in speech. I would think of that as the speech service saying what the recognizer hears. So in this case I think you are correct about the direction. And it makes more sense that the recognizer would attach to the AI or htmlfilter,
-------------------------------------------------------------------------------------------------------------------------------------------
So the question is also: When to use the publish/subscribe pattern and when to use attach. Perhaps DiyServo should use attach for MotorController and publish/subscribe for the analog values. It' actually does so, but masked as an attach.
---------------------------------------------------------------------------------------------------------------------------
10 cents from Mats
Gui Support
These were my (sometimes conflicting) design goals :
For example :
I don't want to give up the ability to do this
serviceA.attach(serviceB)
While at the same time support the following conversation between services to support a Gui
You said the gui should only be a view into the Service it supports. I do not believe that, and that is not what exists currently. It is a good model to have a framework which understands the pairing between a concrete type and a Gui view, but at some time you will want to combine 2 or more services together with initialization data. Its possible to blindly set references such as the service name, but in order to do this you have to contact Runtime to get the set of valid Service names - or you let the user free-form type the name. The Gui is supposed to "guide" the user to success. Too little information and too little control do not provide value above free-form scripting.
In the conversation above the conversation starts with the Motor(SimpleH)
The Runtime guides the user to the set of appropriate services it might attach to.
The MotorController is guiding the user to appropriate pins - this is specific between Motor(SimpleH) & MotorController.
Right now I think many gui pin selections are simply hardcoded eg.(1 through 54) :P
It takes more work to supply the necessary methods and structure for the conversation - but if its done it provides a better user experience and more likely a successful attachment.
Making the methods serviceA.attach(serviceB) serviceB.attach(serviceA) symmetric does not mean I "must" make the Gui symmetric.
Gui conversations
I agree on the conversation above and that it should be possible to do it from the GUI. ( At least the three first steps ).
Next question is then where the conversation to Runtime should take place. It can be in the GUI or the service. I prefer to put it in the service, since then I can use the same result in both the Swing and Web gui.That's what I did in the i2c services ( for example Ads1115Gui.html + Ads1115Gui,js )
subscribe(Runtime.getInstance().getName(), "registered", this.getName(), "onRegistered");
and
controllers = Runtime.getServiceNamesFromInterface(I2CController.class);
is in the service, not in the GUI.
You can see how the dropdownlists in the webgui becomes simple data bindings with very small javascripts.
-------------------------------------------------------
I agree that the dropdownlists should be populated with values from the selected service, not hardcoded values. So we need to add methods, something like getAnalogOutputPins , getAnalogInputPins,, getDigitalInputPins, getDigitalOutputPins defined in the interfaces. It adds a bit of extra code, but I think it's worth doing.
I got stuck on the point when I tried to improve the DiyServoGui, since the MotorController interface doesn't have those methods, just like you probably found the same problem when trying to improve the MotorGui.
I hope theis is 30 cent value :)
And thaks for having this conversation. It is much easier to think clear when you also have to explain to somebody else :)
I'm Rich !
Your a wealth of good ideas Mats.
It's great we agree and are getting more clear on how things should be.
We agree on the Gui conversation, and its better to have the methods in the Service so that any UI can share. I'd like to look at your gui helper shared methods for future reference. And yes the PinArrayControl could support more diverse queries - like getPwmPins, .. etc.
.. goodtimes ...
On to "attach" !
It would be great if we came up with a solid set of rules to go forward with. The "publish" rule has helped progress considerably. Although the situation "if your service has interesting data - send it to a publish{Datatype} method" is more simple than connecting two services together.
How about - "if your service supports attaching with another interface then you should write the following"
interfaceA.attach{InterfaceB}(InterfaceB)
and if this exists - then another should be stubbed out .. even if presently its a NOOP
interfaceB.attach{InterfaceA}(InterfaceA)
This would mean there needs to be a
MotorController.attach(MotorControl) and a MotorControl.attach(MotorController)
The first thing both of these methods do is check to see if the single parameter service is already attached - if so just return - Here is MotorController stub.
last thing it calls the passed interfaces' attach with itself as a parameter.
So although it irks me the type information is bleeding into the method name, I agree with you Mats - when multiple services have several interfaces - the details of what "part" of the service you want attached needs to be specified. So, as a task to the developer she should create "interface mangled" attach statements - and this is where all the complex business code should exist.
A "convienence" method can still be created which has the mangling removed - all it does is call the mangled version.
Parameters besides the service should not be used. If they are used they should only set attributes and eventually the single parameter mangled method would be called.
I started creating "Config" data for the different Motor types ... I think this is a VERY BAD idea.
The problem started when we try to handle very critical details, but instead of creating new motor services, I tried to hide the details in Config classes. Currently, there are 4 Motor subtypes.
These details are Motor specific - in that a Motor instance needs this detail. A Motor Controller can potentially support multiple "Types" - but when attached only on configuration is needed.
My mistake was to put these details in an arbitrary Config class. I did not want 4 separate Motor services, because the amount of copied code X 4 would be large and maintenance would be a laborious mess.
But, I am now of the strong belief that these SHOULD HAVE BEEN 4 DIFFERENT SERVICES ! A Service is the base unit, it has all the configuration and methods to adjust in the Service. Burying configuration in a sub-class just makes it more difficult to support. (This can be seen with OpenCVs VideoProcessor which contains or uses all the critical config data - and there are a zillion breaking methods in the OpenCV class whos purpose is to change the VideoProcessors config ... useless methods for useless abstraction)
My proposal to fix Motors is the following :
What would this give us ? At the minimum it would give us low maintenance motor services which can be created and configured and sent as a single parameter to a MotorController.
Configuration becomes structured and can be done clearly at the beginning of any script
The complexity is always stored in a single parameter, interface/type mangled attach method which follows the same pattern.
Attach and the xxxControl xxxController interfaces
The way I have seen MotorControl vs MotorController has been that MotorControl would have the "high" level methods ( implemented by Motor ) and MotorController "low level" methods ( implemented by Arduino or any other service that can drive a motor ).
But they were also described as pairs that should work togeter.
So when started with the DiyServo service I tried to implement MotorControl becasuse it should work togeter with MotorController as a pair. But then I found that I had to define a lot of methods that never would/could be used in DiyServo.
So for the attach pattern to work and be possible to use in other services than the Motor, the MotorControl needs to be split into two separate interfaces.
One that contains the "high" level methods that a Motor service would implement, and one "callback" interface, that defines the attach and callback methods that MotorController would use. For example the getLeftPin and getRightPin.
I completely agree with you
I completely agree with you in principle Mats,
but I would humbly disagree on a small detail.
First what I agree with :
The difference is I think the MotorController should handle the details based on the runtime type of the Motor and not define another interface. Why ? Because, usually "less code" is better code - code that does not exist is maintenance and bug free. And the new interfaces you describe I think would only be used to describe a single service.
This is where MotorController uses the Motor classes as a form of interface. I'm not necessarily against defining an interface for MotorDualPwm ... but I don't see any other class implementing that interface. So we would be defining many interfaces with each only handling a single implementation class.
DiyServo using MotorController
I had DiyServo using the Motor service as a Peer when I first built it, because I wanted to avoid having the complexity of the Motor configuration in the DiyServo service. But then I changed it because you suggested that I should use the MotorController interface directy.
It should be easy to revert back to the original implementation,
The second point you made I
The second point you made I think gets to an important issue when you try to structure methods in a uniform way.
serviceA.attach(serviceB) is simple, but when the services begin to become more complex and support multiple interfaces - ambiguity is created :(
This issue is not specific to Control & Controller, but any case where one service type can connect to another service type in more than one way.
It's a very good point you raise Mats. I'm not sure how to deal with it.
I see that you started casting to force the attachment you wanted in DiyServo
dyiServo.attach((ServoController)arduino);
Casting is not an option in Jython/Python .. so we are not guaranteed we get what we want. I tried this and was really surprised it chose an attach "randomly" :( - actually worse than getting an error.
I'm still interested in finding a set of simple rules to help with the connecting of services. Its not clear, especially for situations of "one-to-many" ... my first gut instinct is to say, if there is Ambiguity then begin mangling .. :(
diyServo.attachServoController(arduino)
diyServo.attachPinArrayControl(arduino)
I'm still of the belief there is worth in a parameterless attach
serviceA.attach(serviceB) which does the best it can to connect successfully
I also believe the last thing serviceA.attach should do is call serviceB.attach(serviceA)
because in a general way, the two services may not need to modify their attribute based on the other service, but the design should allow them to have the opportunity to do so.
When to use Attach or Pub/Sub Mask ?
I was hoping "attach" would do "all the necessary things" to get two services working properly together. This includes creating the appropriate pub/subs setup, or even switching to addListener if its a local service. It's not a mask, it doesn't hide the pub sub capabilities ..
Attach is supposed to "connect" in a default way, which the developer thought the user would want it to connect. Its subjective, and in some cases probably difficult - but if successful, saves the user time.