Birth of a Part
January 1, 1999 - Jack Harich - Go Back
Here's the short story of how a reusable part was born. We designed the part and then hooked it up to the system. This was easy due to the surrounding infrastructure the parts live inside of.
Problem Overview
Britt came to me and said "We need to internationalize the client, starting with Japanese and German". The client is 100% Java, built on the Bean Assembler and the display portion of the Data Framework. About 20 windows are used. There are about 20 User Task, 25 Meta Worker, 23 API Worker and 7 Communication layer classes. These classes show several hundred different error or helpful hint messages (in a Message Box) to the user in addition to window titles, labels and data.
The fixed window title and label text is stored in parameter files, since all windows are created from these parameter text (parex) files. This text must be localized before it is passed to the UI subsystem.
We used the Mini Process since we are standardizing on that for all development.
First Design Iteration
Nutshell Vision - A reusable configurable approach to Internationalization compatible with the Bean Assembler and Data Framework.
Use Case List
- Configure system for a particular locale, such as English or Japanese.
- Show parex text on a window.
- Show text in Message Box.
- Format and show data such as date, time, fancy numeric. (later)
- Sort Strings.
- Server communication.
This was a swift exploratory iteration. A page of info accompanied this model:
Design Critique
This did accomplish its main purpose, to understand the problem and design options better. But the model is confusing. It's hard to explain to others. It is not very self explanatory. It doesn't emphasize important classes. It's not clear what resolves which Use Cases.
The largest problem was this approach assumed one locale per JVM process. This is very limiting. For example a developer might be using the Bean Assembler Editor in English but displaying Japanese windows. Or in the future the product may have to be multi-lingual for various unanticipated reasons.
Another problem is it takes too many classes to get the job done. Fewer classes would be a simpler solution. The part philosophy is missing. The model is a jumble of classes with no clean connections to the frameworks it lives in or the other parts that use these classes. It feels awkward.
Second Design Iteration
Nutshell Vision - A reusable approach to Internationalization for the JCON Data Framework.
Use Case List (Note 2 additional Use Cases discovered)
- Set the system locale, such as English or Japanese.
- Show parex text on a window.
- Show text in a Message Box.
- Format and show data such as date, time, fancy numeric. (later)
- Sort Strings.
- Server communication.
- Edit locale files.
- Different help files per locale.
Design Strategy
We threw away the first model and started over, studied up on the JavaDoc for ResourceBundle and other Intz JDK classes, ran a Java Tutorial test class and fiddled with it. We discovered a bug in the ResourceBundle documentation that forced studying the source code for getBundle(). This explained how things were really working.
A deep look at the old model versus good parts philosophy revealed that a Fascade pattern approach would be best. One new part would be used by all the existing parts for their localization needs. This new part would hide the use of ResourceBundle, Locale, MessageText, and the various property files that contain text translations per locale. This part was named LocaleMgr and was designed to be a completely reusable part in the JCON bagful of reusables. Here's the model:
Design Critique
This is much better, and has that certain feel that it's ready for an easy implementation because it looks so simple. LocaleMgr has all the methods any other part will need to do localization. It's a great Fascade, allowing easy extensibility as future new requirements arise. Not all methods are shown, such as getCollator().
The Responsibilities and Implementation Plan clearly show where each Use Case is resolved. This makes the model very understandable and easily implementable. All this info is not hidden in the developer's brain - It's there for all to see and understand. In this case, the model served as the key hand-off document to allow other developers to complete and use LocaleMgr after the Initial Implementation. Without a good model this hand-off would have been awkward.
We expected to resolve some issues in implementation. These are listed in Misc. It turned out that font handling was transparently done by the JDK.
Initial Implementation
This was creating LocaleMgr, hooking it up, creating the locale directories and files, and doing Use Case 1, 2, and 3 for Logon. Following our own advice, we wrote a Unit Test first. Then we did only enough work to get that simple test to work. This gave us a solid first iteration to build upon. The public methods of interest are:
- void setMarkerClassName(String className)
- void setLocale(String language, String country, String variant)
- String getString(String area, String key)
- String getFormattedString((String area, String key, String[] values)
- getCollator() will be added later
Notice how LocaleMgr behaves as a good part by being a good Fascade. It exposes only what other parts need, and hides the rest.
Once we had LocaleMgr working standalone with the unit test, we hooked it up to the system one step at a time.
First we did the hookup for Use Case 2, "Show parex text". This was easy, since the Bean Assembler supports parex text replacement before it converts the parex text to a Param and calls setParam(Param) on ParamDriven parts. We made these changes:
public class LocaleMgr implements BuildParamListener { private static final String INTZ = "INTZ"; //---------- BuildParamListener Implementation ----------- public String[] getBuildParamKeys() { ....return new String[] {INTZ}; } public void processBuildParamEvent(BuildParamEvent evt) { ....// We only process INTZ replacements ....if (! evt.getBuildKey().equals(INTZ)) return; ....// value is of the format "area.key" ....// Get area and key from value ....String value = evt.getBuildValue(); ....int dotIndex = value.indexOf("."); ....String area = value.substring(0, dotIndex); ....String key = value.substring(dotIndex + 1); ....// Use area and key to set replacement ....String newValue = getString(area, key); ....print(" - replacing " + value + " with " + newValue); ....evt.setReplacement(newValue); } |
Plus we added the LocaleMgr to the root container for the system, set it as a "MessageService" listening for "BuildParam" messages, and made changes to the Logon.parex file for replacements, which looked like the below portion. For example "[[INTZ LogonWindow.WindowTitle]]" will become "Logon Please" in English.
// Logon.parex - Drives TaskLogic, Logon ViewNames is: MainView TaskClassName is: com.realm.client.system.Logon MainView has: ....Title is: [[INTZ LogonWindow.WindowTitle]] ....IsPrimeView is: true ....// Set window size so not so wimpy ....WindowWidth is: 250 ....WindowHeight is: 140 ....HelpId is: main.html ....Workers has: ........RowFieldPanel has: ............IsModule is: true ............Type is: RowFieldPanel ............Style is: Default ............AddType is: EndRow ............NestedContainer has: ................//--- UserID ................UserIDLabel has: ....................Type is: Label ....................Style is: FieldLeft ....................Text is: [[INTZ LogonWindow.UserID]] ....................End: UserIDLabel (snip) |
We added an English locale file for the Logon window as follows:
# LogonWindow.locale WindowTitle=Logon Please... # Labels UserID=User ID Password=Password # Buttons OkayBtn=OK CancelBtn=Cancel # Messages - More to be added. UserIDRequired=User ID is required. |
Then we implemented Use Case 1, "Set system locale." We modified LocaleMgr to implement ContainerServicesUser and added about 30 lines of code for that interface and getting the LocaleID from the SystemProperties file, which contains the line: "LocaleID= is: en_US". SystemPropertes is a reusable part that was already in the system. It allows the LocaleID to be gathered either from the file or as a command line option.
And that's all there was to localizing all window titles and labels, though in this iteration we only localized the Logon window. The rest are duck soup....
Then we had to let all application parts that needed LocaleMgr to get it. Since we were using an application Plugpoint Framework, this was easy. A single class WorkerMgr, controls collaboration between the two plugpoints. We added setLocaleMgr(LocaleMgr) and getLocaleMgr() to WorkerMgr, added one line to the root container parex file to call setLocaleMgr(), modified WorkerContext to have a LocaleMgr, and we were done. This took less than an hour. In a system with inadequate architecture this would have taken much longer or static methods would have been used. Avoid statics, because they are Singleton oriented. In this case a static getLocaleMgr() would have made supporting multiple locales transparently per JVM impossible.
The hard part was now behind us. Next we modified various reusables that used MessageBox to support Use Case 3. This was done by passing an additional argument, LocaleMgr, if localization was desired.
Summary
Certainly the most interesting aspect of this is Design 1 took about a day, Design 2 took about 2 days, and the Initial Iteration took a day. No significant problems or changes were encountered in the implementation. This shows the power of the Mini Process and good modeling.
This also illustrates the power of parts living in infrastructure. When you need to add new functionality to a system, you modify existing parts, parameters, or add a new part. What you don't have to do is much lifecycle, parameter initialization or collaboration work, because the infrastructure already does that. One thinks in terms of reusable, configurable parts, not code, and everything changes. We surpass object oriented to become part centric.
From my experience in using parts in the Bean Assembler, Data Framework and application Plugpoint Frameworks, it adds up to a 2 fold productivity improvement if you have a so-so understanding of the technology, and a 4 fold improvement if you understand it deeply. These are estimates. I have no metrics. We are talking full project cycle time here, not just coding.
Some of the principles making this radical productivity improvement possible are standard low level system architecture (hierarchy), easy Layered and Services Architecture, Anonymous Collaboration via Messages, Declarative Knowledge driven reusable parts, and application neutral Microkernel to build frameworks on top of. The key underlying concept is Ultra High Reuse.
Most software modelers engage in what I characterize as "Classical Modeling". They identify classes and their relationships for each system. There is a higher abstraction which could be called "Reuse Modeling". Here one layers applications over reusable mechanisms (and layers) which are "basic" parts, containers and partitions. For example a Customer is basically an Entity member with Workflow Datatrons. For example LocaleMgr is a Reactor to Messages containing Datatrons and Services. More on this later.