Where do you start with design? What are good OSGi based designs? Obviously, most criteria for good design are shared with non-OSGi based systems. However, the OSGi Framework provides an environment for components. Designing with components is clearly not yet mainstream and there are few established rules. So let me give you my 2cts worth.
What is a component? An OSGi component is a dynamically deployable unit that provides a function through a set of services, which are defined by a Java interface. For example, an OSGi Log component would be implemented in a bundle, its packaging, which could potentially contain other components. If this bundle is started it provides a Log Service and a Log Reader Service to the world. Other bundles can get these services and use them as they see fit. From a design point of view we can therefore treat the component as a black box with ports (the services) for the communication.
I know there are today standard module and interface symbols in UML but I never got the hang of them (I really tried!). They happen to be the most complicated symbols in UML, while a component design uses them almost exclusively. Also, the dynamic nature of OSGi services is very difficult to represent with UML. Trying to use UML for what belongs to the primitives of an OSGi Framework tends to clutter the picture. I therefore usually use a circle for a bundle and a triangle for a service. The service can be used in 3 different ways by a bundle, and this needs to be depicted:
1. Consumer – The bundle that gets the service is the consumer. The arrow from the bundle attaches to the horizontal or vertical line on a service triangle. An OSGi service can be consumed many times by any bundle. Many bundles can therefore connect to the horizontal or vertical line of the triangle.
2. Implementer – The bundle that implements the service must register it. This is depicted by drawing a line to a corner of the triangle. An OSGi service can be registered many times by the same or different bundles.
3. Listener – A listener bundle connects to one of the angled lines of the triangle. A listener is notified of registrations, modifications and unregistrations of the service. A service can have many listeners.
A service is identified by its interface. The service can be seen as a joint or flexpoint in the system. It decouples bundles from each other and decoupling is something you have to pay a lot of attention to! This decoupling is achieved because all three involved dependencies are directed towards the service. None of the bundles has a dependency on any of the other bundles. I might bore you, but I can not stress enough how important this concept is!
So where do you need these joints in the system? Well, the simple answer is: everywhere you want to decouple, which should be in a lot of places. The trick is to put a service whenever you can break the coupling between bundles.
Services are obvious for big functions like a log service, a configuration admin service, and, for example, the permission admin service. These types of services are similar to what you would get from JNDI or interfaces that would be injected with systems like Spring. These types of services are used to wire the application to providers of specific types of function blocks. Normally, the dependency on these services is static because the bundle can not operate without the presence of these function blocks.
Services are also applicable when you dynamically need instances of a specific type, but you could not care less how they are implemented. An example of this class is the archetypical OSGi scenario of controlling lights. You would not believe how many home automation protocols exist, and each protocol has its own cumbersome and particular way to control a light. The service registry was developed with this use case very much in mind. Different bundles could register a “Light” service, mapping the interface semantics to different protocols. The bundle that needed to control the lights no longer has to worry about trivial details like bits and protocols; it could just get the Light services and play with them as it sees fit, oblivious of implementation details. This pattern translates well to Bluetooth devices, available database servers, available printers, legacy system connections, etc. This pattern very clearly elucidates why OSGi services are dynamic.
Another class of services is the listeners. After release 1 we realized that we could seriously simplify applications when we used the whiteboard pattern. The whiteboard pattern is an example of inversion of control (IoC). That is: “Do not call us, we will call you”. If you need something from the world or have something useful functionality to offer to the world, register a service and wait till somebody calls you. The basic approach of the whiteboard pattern is to announce what you have, but in no way try to control the usage. You provide, but you do not control. Today there are many standard OSGi services that use this pattern: Event Admin, for example, will send its events to any service that registers itself as an Event Handler. The Event Handler uses properties to select the events it is interested in, preventing unnecessary callbacks. This is an extremely successful model, albeit many programmers have a hard time getting used to the loss of control that they feel.
The best litmus test for a service is: “Do I decrease coupling”? If I put a service here, do we need less people to decide upon its interface then when I put it there? If you can minimize interactions between bundles (implementations) you are moving in the right direction. The reason that you want to minimize the coupling is because this usually directly translates to development cost. Trying to agree on interfaces between 10 parties is a lot harder than agreeing between 2 parties. Later phases are also more complex when more coupling is present. For example, testing decoupled components is so much easier than components that are coupled to the kitchen sink as well as to the coffee maker.
At first this way of designing is cumbersome, you have the feeling you create lots of interfaces and do not do anything really productive like writing code. However, hours spent selecting those interfaces are probably your most productive hours of the entire project. Do not hesitate to throw designs and start over from scratch. For one customer, I found almost 200 deliverables (bundles, OSGi services, or external interfaces). This large number explained why they had had so much problem progressing with the development of the system. Doing this analysis gave them an insight in the complexity for the first time. A part of that system is shown in the following picture (the parallelograms are web services). Though I can not show the details due to confidentiality agreements (the fuzziness is on purpose), it shows the extensive use of services and bundles.
After you have done this work, the next step is to develop the service interfaces. This is frustrating work! It usually involves hard negotiations between different parties at the time that most participants have not much of a clue yet. So assume you won’t get it right the first time. However, do not worry about it too much because it will need iterations anyway. Your gain is that you minimized the parties involved in a discussion because you minimized coupling. Most of the time, a service interface is owned by a small group or even a person, and used by other groups or persons. If you did the design right, you should have found the minimum number of interactions. So yes, it will be a lot of work but at least it will be the minimal amount of work.
A key advantage with this model is that you now have a relatively stable set of services and that all involved parties can start coding against these interfaces, and even compile. For larger projects it is worth the effort to develop test stubs early in the process. This will allow the users of the interfaces to test their code as well. If you can get the group(s) responsible for implementing these key services to write them, you will notice that they usually modify them in this process because they learn a lot from this experience.
The next phase is implementing the bundles. This is usually the longest part because it means getting things to work really. Do not hesitate to refactor the service interfaces when you learn the original design was flawed. Developing software is in a large part a discovery process, refactoring is inherent in the fact that your insights change. If you had followed the guidelines and had minimized the overall decoupling of the system, refactoring of the interfaces should be straightforward. Never hesitate to improve the service interfaces if you see a possibility where you can do better. You might save a few days by ignoring these improvements but you will pay much more later if you ignore those improvements.
A well designed component system will notice that interactions between groups are significantly less and more focused on specific service interfaces than monolithic designs. Obviously, this directly translates in development dollars.
The only caveat I know is the surprise that comes at integration time when you do not prepare early for that surprise. Unconstrained, bundle programmers will happily code away and implement the requested functionality in the best way they can. However, what they can not see is how their components will work when put together with 200 other components. A typical example is the initialization time. Bundle programmers will not notice a 2 second initialization time because they do a simple DNS lookup in the Bundle Activator. However, multiplying this with 200 will not put a smile on your manager’s face: 400 seconds is still six and a half minute. If this happens a day before the navigation system needs to be delivered, it will be sufficient time to walk to your desk and pack your belongings. Therefore, a component design requires early integration and lots of later integrations. This is the only way to find bottlenecks early on.
Hmm, maybe this subject is a bit too much for a blog, I do not think I ever got to page 5 before. However, this subject needs more attention because designing with services for an OSGi service platform is different than writing a normal Java application. Let me know if you think this is useful or if I should stick to the details from now on …