Lookup
From APIDesign
Lookup is a library that provides API that unifies component injection with adaptable pattern. It is based on long time JDK standard for registering instances of services in JAR files which has been formalized as ServiceLoader in JDK 6. Lookup extends this area and allows local pools of objects that can be queried for certain capability. Lookup is also suited for dynamic environment as it supports notification of changes. Lookup bridges the classical injection pattern with local adaptable pattern.
- See Lookup
- Install Ubuntu or Debian package libnb-org-openide-util-lookup-java (7.0.1+dfsg1-5ubuntu2)
- Download from NetBeans Maven snapshot repository.
- Visualize Lookup via its mind map
The basic idea, in its ServiceLoader incarnation is that it is enough to compose an application from various JAR files and let these JAR files communicate. This is achieved by defining well-known location inside the JAR file META-INF/services/nameoftype that is read from all available libraries JARs on the classpath.
This greatly simplifies final assembly and deployment of the application. It is enough to drop the JARs with an implementation available in the system and all the pieces can work suddenly cooperate by mutually discovering themselves via Lookup.getDefault().lookup(...).
On the other hand, looking from a local point of view, there can be many pools to fish for an interface. Almost any object can implement Lookup.Provider interface and thus give access to its adaptable features to everyone.
Using the same interface to play two different roles (component injection and Adaptable pattern) shows the common properties of both. The Lookup can serve as a assembly language to provide pool of objects to use during real Dependency Injection. The bridge between Lookup and Spring provides more details about this area.
Contents |
Tim Boudreau on Lookup
In a response to question about appropriate use of Lookup, Tim provided following answers.
System-level-decoupling
You provide some interface and expose it via Injectable Singleton. Some other module will actually implement the interface. You want modules to be able to use your API, without caring who implements it, just that some implementation is there. Example: The status line.
StatusDisplayer.getDefault()
returns some implementation of StatusDisplayer. In NetBeans typically it is provided by the window system. But I once wrote an implementation that would instead hide the status bar and instead show the message in a translucent popup that appears over the main window. That would not have been possible if all code that wanted to display status messages was tied at compile-time to the implementation class provided by the window system.
Designing an API for a not-well-defined problem space
For Example the Project.getLookup(). In the case of projects, when that API was designed, the only things that could be known for sure about a project were that:
- A project is a directory
- For any file, there will be zero or one projects that own that file
Designing a Project API that would provide for everything C/C++, Ruby, Java, DocBook, HTML, Web, J2EE, J2ME, etc. projects (this had been tried) would end up with something bloated and filled with functionality that any random client would never use - a very noisy, hard-to-use API. Since in that case the requirements were not and could not be known, the lookup pattern made it possible to create an API and let clients define additional APIs (like ClassPathProvider for Java projects, which would make no sense in a DocBook project), and provide client access to them through the project's Lookup.
Granular decoupling
The uses of Lookup in Node and TopComponent: Here, you have some API type. You make it available in the Lookup of files of a certain type. You don't necessarily know all the ways your UI will change in the future. Other modules want to add actions (to popup menus, toolbars, whatever) that can operate on your type. Those actions should be enabled whenever the selection contains one (or more) of your object. By writing actions sensitive to your type in the global selection lookup (Utilities.actionsGlobalContext()), no rewrite of those actions is required if, at some point, you write a new window component that shows, say, virtual files or some random tree of objects that contain your type.
Mutable Capabilities
You are designing an API for an object whose capabilities are actually mutable. Listening for a particular type in a Lookup is much less code, and much clearer, than defining a bunch of event types, listener classes and addThisListener(), addThatListener(). Example: In the Java Card modules, there is a class Card. A Card has a lookup. Now a card might be a physical device plugged into your computer. Or it might be a virtual card definition used by an emulator for testing. A virtual card has capabilities like Stop, Start and Resume. When you call StartCapability.start(), the StartCapability disappears from the Card's lookup and a StopCapability appears. But if it is a physical card, Start and Stop make no sense whatsoever - so for a real card they are not there. Other capabilities, such as PortProvider, which will tell you what TCP ports to use to send code to, attach a debugger to, etc., are present for both virtual cards and some real cards, if HTTP is the mechanism to deploy code to them - but other cards may have you run a native executable to deploy code and use no ports. So PortProvider is another optional capability.
Note that you can add typing to Lookup-based APIs if you find it useful or it makes your API easier (with Find Usages or Javadoc) to use. In org.netbeans.modules.javacard.spi.Card, in fact, there is
public <T extends ICardCapability> T getCapability(Class<T> type);
which delegates to Lookup but guarantees the return value is a subtype of something you can search on. I don't recommend that for all situations (part of the birth of Lookup was that Node.getCookie() returned something that implemented the marker interface Node.Cookie, and for things that wanted lookup-like functionality but had no connection to Nodes whatsoever, it made no sense to make them drag around a JAR with the Nodes API just for a marker interface). But in restricted situations, it can make an API more usable.
Perhaps there are other scenarios that I have missed for when Lookup makes sense, but I think those 4 cover most of the typical cases. If you're not doing something like that - if using Lookup adds complexity to your code without adding needed flexibility or future-proofing - then it's the wrong tool for the job.
Overuse of Lookup
Sometimes people call Lookup magical bag. The pejorative flavor of the nickname is based on observation that you can put your hand in it and pray to find the object you are seeking for. Sometimes people believe there shall be a better way. Yes, in many cases there is. Lookup is not handy for everything. It is basically a tool to implement a teleinterfaces. In case a teleinterface is not what you want (like in case of CumulativeFactory), you'd rather prefer type safety and design proper API without magical bag - aka Lookup. More about that in Chapter 7.