10.3.1 - Model Transformation
10.3.2 - Refactoring
10.3.3 -0 Forward Engineering
10.3.4 - Reverse Engineering
10.3.5 - Transformation Principles
10.4.1 - Optimizing the Object Design Model
Direct translation of analysis models to code is often inefficient. This section describes four common optimizations to improve performance or meet other design goals.
Optimizing access paths.
Repeated association traversals. Operations that are performed frequently can suffer from poor performance if a large number of associations must be traversed. Performance can be enhanced by adding a direct link, ( at the cost of additional overhead to create and maintain the new link. ) Examine sequence diagrams of frequently performed operations to identify long association paths.
"Many" associations. If a lot of time is being spent working down a list of associations in a one-to-many or many-to-many situation, see if the to-many can be reduced to a to-one, possibly through qualified associations. Alternatively, order or index the associations to make finding the desired one faster.
Misplaced attributes. Some classes may turn out to have very little if any interesting behavior. If an attribute is involved only in setter and getter operations, consider moving the attribute to the class that calls it. If enough of the attributes can be moved out of the class, it can sometimes be eliminated all together. ( See next section. )
Collapsing objects: Turning objects into attributes. A class that has few attributes and behaviors, and is associated only with one other class, can be collapsed into that class, reducing the overall complexity of the system.
Delaying expensive computations. Put off performing expensive calculations until they are really needed.
Caching the result of expensive computations. Sometimes it is more efficient to save the result of expensive calculations, rather than re-doing the calculations every time the result is needed. The tradeoff is the additional variable(s) that must be stored and maintained. It is also wise to combine this idea with the previous one, so that the expensive calculation is only performed when the result is needed, and the result cached for future re-use. If the inputs to the expensive calculation change, then the cached value should be invalidated, but not re-calculated until the result is needed.
10.4.2 - Mapping Associations to Collections
Unidirectional one-to-one associations. In the example below, each Account is associated with exactly 1 Advertiser, and the only traversal of the link is from the Advertiser to the associated Account. In this case the Account is created by the Advertiser constructor, and the Advertiser retains a link to the Account, but not vice-versa:
Bidirectional one-to-one associations. As above, but now there is a link from the Account to the Advertiser as well as the other way around. The constructor for the Account is still called as a byproduct of constructing an Advertiser, but now it contains a link back to the Advertiser with which it is associated:
One-to-many associations. If the Advertiser can have multiple accounts, then the Advertiser needs to have some sort of collection ( Set, List, HashMap, etc. ) to refer to the Account(s). The Account is no longer created by the Advertiser constructor, but rather must be created separately and added to the Advertiser as a separate step. Note that the code below has a problem if Advertiser.addAccount( ) is called before Account.setOwer( ).
Many-to-many associations. Now both ends of the association need to use collections, with appropriate checks to maintain the consistency of the associations, and to prevent circular recursion. ( The example above needs this check in Advertiser.addAccount( ). )
Qualified associations. The idea behind a qualified association is to reduce a to-many association into a to-one association. For example, a directory can contain many files, but it can only have one with a given filename. In the example below, each Player is given a nickname that is unique within a given League. Now a League can have multiple players, but only one with any given nickname. Note that the collection has now been changed from a Set to a Map. ( League names are also unique, which makes the to-one relationship work from Player to League as well as League to Player.
Associations classes.
10.4.3 - Mapping Contracts to Exceptions
- Check pre-conditions at the beginning of methods, and check post-conditions and invariants at the end of methods.
- Encapsulate checking code into methods that will be inherited properly by sub-classes ( which inherit the contracts. )
- Unfortunately it is not always practical to check for every possible contract violation in all cases, as shown in the next Figure. Some of the problems associated with attempting to check every violation include:
- Coding effort - The amount of code required to check for contract violations can easily exceed the actual work performed by the method.
- Increased opportunity for defects - The error-checking code may itself have errors, and the more code that exists the more code has to be inspected and tested for possible defects.
- Obfuscated code - Error checking code can often be very complex and hard to understand.
- Performance reduction - Executing error checking can slow down the program.
10.4.4 - Mapping Object Models to a Persistent Storage Schema
- Only an issue with flat files or relational databases, not with object-oriented databases.
- With relational tables, each class corresponds to a separate table, each instance to a row of the table, and each column to an attribute.
- A primary key is a unique identifier used to uniquely identify a particular record in a table. Candidate keys are unique identifiers that could serve as primary keys.
- A foreign key is an attribute in one table that refers to a record in another table, ( via the primary key of the other table. ) The following example identifies the owner of each league via a reference to the User table.
Mapping classes and attributes
- Each attribute in the class needs to be associated with a data type in the database table, which may impose new constraints. For example in the diagram below, the length of the firstName has become limited to 25 characters, which must now be enforced in all methods that set or change the firstName field.
- The primary ID can be either a data field that is already present in the class, or a new unique identifier assigned to each instance when it is created. The latter approach requires more storage space, but provides more flexibility in changing the design of the class.
Mapping associations
Buried associations.
Associations can be kept simply by having a field in one table refer to a field in another. Note in the following that the association can only be reconstructed from the League table to the LeagueOwner table, and not the other way around. However both associations ( bidirectional ) can be established when the relevant objects are created.
Separate table.
Alternatively a separate table can be used to record all associations between members of two classes. This is necessary in the case of many-to-many associations.
Mapping inheritance relationships
There are two main options for storing inherited data in tables.
Vertical mapping.
- The parent class can have its own table, where all the attributes defined in the parent class are stored for all objects of either the parent or derived classes.
- A field in the parent class refers to additional information for each object found in the descendant class tables.
- Note that the ID field is unique for the same object across multiple tables.
Horizontal mapping.
- Alternatively the derived class tables can include all attributes for the object, whether inherited or derived.
- This causes the same field to be recorded in multiple tables, but since any given object only appears in a single table, it does not really take up any more space.
- The parent class table can now be eliminated, unless objects are created which are purely of the parent class.
- Horizontal mapping keeps all the data for an object together in a single table, but requires more tables to be changed if any attributes change in the parent class.
10.5.1 - Documenting Transformations
When transformations are made to code, ( refactoring ), inconsistincies can easily develop between the design model and the code implementation model. Over time these consistencies can become significant, to the point where the existing design model no longer reflects the implemented code accurately.
Reverse engineering attempts to re-create a design model that matches the current implementation, but information gets lost in this process through several reasons:
- Association multiplicity and collections. One-to-many and many-to-many associations map from the design model to the implementation model as the same source code. Reverse engineering tools tend to take the conservative approach and model all of these as many-to-many when going the other way.
- Association multiplicity and buried associations. Database schema with buried associations suffer from the same problem. Worse, if separate tables are used, then all multiplicity information is lost.
- Postconditions and invariants. When mapping contracts to exceptions, not all of the constraints are implemented, as described above. This information is then lost completely when reverse engineering.
The challenge then is to retain consistency between the design model and the implementation, and to retain full design information, through all transformations. The following principles help, when applied consistently:
- For a given transformation, use the same tool. I.e. do not mix-and-match different CASE tools. Many CASE tools will retain design information in the form of comments, but plain-text editors generally do not.
- Keep the contracts in the source code, not in the design model. It may not be practical to implement all of the relevant constraints, but there is no reason not to document them all in the form of comments.
- Use the same names for the same objects, on both sides of the transformation. Especially in the transformation from classes to database schema.
- Make transformations explicity. Every organization should have standardized procedures gudelines, so that all developers implement the same transformations the same way. In an ideal world the exact same code would be generated from the same design model, regardless of which specific programmer did the transformations. What the standards are is not nearly as important as the fact that everyone follow them. ( Compliance can be improved by involving the deveopers in the development of the standards, so that they have some ownership in the standards guidelines. )
10.5.2 - Assigning Responsibilities
- The core architect selects the transformations to be systematically applied.
- The architecture liaison is responsible for documenting the contracts associated with subsystem interfaces.
- The developer is responsible for following the conventions set by the core architect and actually applying the transformations and converting the object design model into source code.
10.6.1 - ARENA Statistics
Statistics ( counters ) are to be maintianed for six scopes:
- All Matches in a Game
- All Matches in a Game played by a specific Player
- All Matches in a League
- All Matches in a League played by a specific Player
- All Matches in a Tournament
- All Matches in a Torunament played by a specific Player
Statistics objects are to be produced by the Game Abstract Factory
A default Statistics class is provided for Games that do not require any special statistics.
Statistics is related to the other classes as an n-ary association, as shown below. The relationship is that a given Statistics object is associated with exactly one of ( a Game or a League or a Tournament ), and may or may not also be associated with a Player. One Game ( or League or Tournament ) only has a single Statistics associated with it, but a Player may have multiple associated Statistics, one for each Game / League / Tournament with which (s)he was involved.
10.6.2 - Mapping Associations to Collections
The StatisticsVault class is introduced to maintain the associations between Statistics objects and the things they are associated with. As first designed this requires two calls to get staticstics data - One to the Vault to acquire a Statistics object, and a second to get the data.
Reworking this as a Facade pattern simplifies the interface and requires only a single call to access Statistics.
10.6.3 - Mapping Contracts to Exceptions
10.6.4 - Mapping the Object Model to a Database Schema
Because each Statistics object is associated with EITHER a Game, League, or Tournament, the associations can be combined. In the first table, the ID is the number of the Statistics object, the scope is a reference to the ID of the Game, League, or Tournament with which is is associated, and the scopetype indicates which of the three types of associations it is, i.e. which separate table the scope field refers to. The Statistics table holds the association information, and the StatisticCounters table holds the actual values.
10.6.5 - Lessons Learned