Lars is a senior software architect for Telelogic Technologies and can be contacted at [email protected].
In compiler theory, we talk about a language, its lexical rules, grammar, and semantic structure. With imperative languages such as C, C++, Java, and the like, the language is based on textual statements, declarations, and expressions. These are just the means of expressing the structure and logic of applications. You manipulate programs by editing code in textual files. With model-driven architectures, however, you may instead directly manipulate symbolic, often graphical, representations of these elements as objects. In a sense, you are plugging right into the heart (or brains) of the compiler and visualizing the essential structures of the application.
Unified Modeling Language (UML) 1.x classes, relationships, and packages only addressed a subset of the entire application structure, and 1.x was never fully able to model the entire logic of an application. Consequently, UML 1.x models would then serve only as pictures to visualize limited aspects of the application, and potentially generate stubs or some code for the application. To some extent, this inherently disconnects the models from the actual code and, more often than not, the models end up in a pile as obsolete documents. The recently released UML 2.0 promises full modeling of entire application logic, meaning that code is just one among several possible views of a model.
UML is a visual language for specifying, constructing, and documenting software systems. Developed under the auspices of the Object Management Group (OMG) (http://www.omg.org/techprocess/meetings/ schedule/UML_2.0_Infrastructure_RFP .html), UML 2.0 provides built-in support for component-based software, greater alignment with Standards such as XML/XMI, SDL, and MSC, better support for executable models and dynamic behavior, better diagram interchange between tools, greater scalability, and the like.
In this article, I'll develop a Java application using UML 2.0 and a model-driven development strategy. To illustrate, I use Tau Generation2 (http://www.taug2.com/), a UML 2.0-enabled tool from Telelogic (the company I work for). Tau Generation2 is an integrated set of development and testing tools that provides a visual development environment hosted on Windows 2000/XP and Solaris, supporting a range of target environments spanning from small embedded platforms to large distributed enterprise systems. The complete source code for the Java application is available electronically; see "Resource Center," page 5.
Programming: How it Used to Work
In programming, the common paradigm has been that you write a program, then let a compiler check it for errors. The errors may be of several forms. For instance, you may have accidentally left a quoted string without an end quote, or a semicolon might be missing. These are lexical or syntactical errors and are usually simple to find and resolve. Other errors include referring to an object by name when the name is misspelled or not defined (or has been renamed by someone else). These are name-resolution errors. Apart from these, you may also invoke operations that give the wrong kinds of results. Most of these errors are detected by the compiler sooner or later, but with Java, errors can be detected even as late as at run time because there is no explicit linking phase. When (if ever) you rename a class or one of its features, you are left with the risk that some other codethat you may not be aware ofrefers to that name, thereby breaking the code. A completely different category of problems stems from misinterpreted requirements, conflicting requirements, or even the lack of requirements related to a particular module. This is largely an unsolved issue with classical programming: There is seldom a link from the actual code into the requirementsthe original motivation for adding a particular piece of code in the first place.
In the context of traditional programming, modeling has usually been a superficial activity with little or no practical use in the daily chores of the programming community. In special cases, it has been possible to "roundtrip" particular aspects of a program, but the overall logic has either been kept in the code or a limited part has been "sourced" in the modeling tool (things like interfaces, classes, attributes, and operation signatures).
Model-Based Engineering
I used to think of programming as a concrete way of modeling, only that the model was in my head. The model was only physically manifested in the declarations and code statements I wrote. And surely, this is true: Most programs constitute a model in some sense, but the model is primarily for the compiler to consume. The logical model is a little hard to communicate with humans without first having them digest all these code files, read the comments, and see diagrams made in a separate modeling tool. As far as the correctness of the program goes, that has largely been up to the compilers, testers, and ultimately the users.
Model-based engineering environments let you find out about any syntactic or semantic problems in your code before you start compiling or testing it. Also, in advanced uses, it can even tell you if all requirements are going to be met by the code. The key to this is to have UML 2.0 tools with full semantic awareness. Such tools manage not only the diagrams showing a number of human-readable views of the application, but also support a "modeling-through-code" paradigm. In addition, they should, of course, support diagrammatic views relevant to the design or logical structure of the code. Just like the code, these views show different aspects of the same program. Neither the code nor the diagrams are all encompassing; rather, they complement each other by conveying different information for different purposes. In fact, the code is, in this sense, just another view of a model that contains much more information. For instance, yet another view may depict the linking information that associates the code with related requirements or with other external documents.
Analyzing the Requirements
In many development efforts, there is generally some idea of what should be accomplished. One way of defining this is as a set of requirements, such as with the Java-based telephone-list application I present here.
For this telephone-list application, user-level requirements might be that users should be able to:
- Add new contacts.
- Retrieve information about a particular person.
- Update information concerning a particular contact.
The requirements analysis yields some use-cases (Figure 1). At the analysis level, these cases correspond to the requirements. Adding links to these requirements from the use-cases is natural in a requirements-management context. The use-case diagrams are one of the most simple, but most useful, views provided by UML and are an integral part of UML 2.0. Furthermore, sequence diagrams can illustrate the dynamics of the interactions of the system and give you key information about the desired externally visible behavior of the system.
Designing the Application
Given the relatively small scope of the requirements of this application, its design does not require UML 2.0's real-time and structural design-oriented features. Instead, the design involves the establishment of the main classes and relationships of the application. You have to decide if there are any useful commonalities between the classes of the application that should be exploited. You also need to establish what relationships exist between the entities, and maybe even add some initial code to get a better understanding of the design.
For the telephone-list application, the design may consist of one class PhoneBook for the application itself and its main method, and another class Contact for the information about the contents of each data record. Depending on which view you choose, the application has an association or attribute that represents the current set of data records (Figure 2). In addition, you decide that the use-cases can be manifested as public operations of the application class. This is done by moving these use-cases directly into the class, and showing them as operations in the class diagram. After adding empty methods to the operations, you can compile and run the program a first timeto no effect, of course, but it clears the way for incrementally adding on the implementation of each use-case.
So far, the model contains two classes, a couple of attributes, three operations, and the empty operations for adding, changing, and finding a Contact by name. These are displayed in two viewsthe use-case diagram and the class diagram. A UML 2.0-based tool such as Tau Generation2 uses one representation for these elements independently in whichever view they are displayed, such that if a change is made to one of the views, it is immediately propagated to the other view. Changing the name of the class Contact to Person does not change the logic of the application, only that name. If you do it in regular code, you would also have to change the type of the array in the PhoneBook application.
Detailing the Behavior
Once you have a preliminary object model for the application, you can start adding the code for realization of the use-cases. An empty method body was already added to the operations of the model in the design for the purpose of being able to compile the application. First, you add a main loop in which you simply accept simple textual commands such as add, find, and edit, and then dispatch calls to the related method until an exit command is received (Figure 3). For each use-case added, you can test the application by compiling and running the application. You also add the fundamental behavior to the use-case-related methods (Figure 4).
The semantic knowledge of a model-based tool is of particular value at this level of implementation. The main reason for saying this is the availability of the full modelfrom requirements through use-cases and detailed class definitions. These can be accessed directly from the model and manifested as a complete dictionary of the program with all of its scope structures, definitions, inheritance hierarchies, predefined types, and so on. Here, you can find all the members of a class or all the references to a particular variable or class member. So if, for instance, you need to find out about a particular operation and its uses, you may want to see other places where it is usedor jump straight to its implementation, maybe to a use-case diagram, or even to the requirements associated with itwithout having to leave the implementation environment to search in a separate modeling tool, in external documentation, or in a requirements tool.
Reengineering
In classic programming, several kinds of changes are difficult, which ultimately discourages you from making some needed changes. Examples of such changes include moving classes from one package to another or renaming classes, their members, or even local variables of methods. In themselves, these are not necessarily changes to the program logic. Modeling tools can, therefore, help make such changes, including automating all the necessary updates to references to such renamed entities (Figure 5). This reduces resistance to changes in the program in a very concrete manner because there is virtually no effort involved in keeping the code up-to-date for these classes of changes. Similarly, removing an object, such as a class or attribute, is straightforward because all references to it are reported by the tool as unresolved, even if the reference is made directly inside a method (Figure 6). In this case, you need to either substitute the removed definition with another one, or walk through the remaining uses of it and remove them as well.
Conclusion
The model-driven architecture and model-based engineering let you work pretty much the same way as you've always worked in many respects. The main difference is that your program is primarily manifested as a model. In contrast to previous versions of UML (1.x) and its supporting tools, UML 2.0 eliminates the disconnect between code and model. Model-driven architectures also let you focus less on the code files. Instead, you view and manipulate the logical constructs of your application. The modeling tool gives instant feedback on most syntactic and semantic problems, or avoids them entirely. Similarly, by thinking in terms of models and their abstractions, you will be able to get a more appropriate grip on the real issues of managing a program. The model-driven architecture is not focused on where to place particular elements, or which files need be updated to make a particular change, but instead is focused on what logical change is needed.
DDJ