A Software Engineering Lecture by Steven Choy
Lecture Overview:
In the previous lectures, we learned to refine a system design model into an object design model. In this lecture, we are ready to turn the object design model into executable code.
We have been working on models. We transform a model by improving certain aspects within it. Following the direction of normal transformation--from model to code--we are said to be forward engineering a system. Reversing the direction of normal transformation--for example, recovering a model from executable code--we are said to be reverse engineering a system. If we transform existing code into better quality code, we are said to be refactoring the code.
In this lecture, you will also learn how to write code to implement associations in the object models, how to code the contracts with Java exceptions, and how to implement object models in a relational database.
Reading: Chapter 10 of the textbook Mapping Models to Code
Where are we now?
Mapping Concepts
Model Transformation
Refactoring
- Operate on source code, ie. Improving a single aspect of the system without changing its functionality
Example
|
Before Refactoring
|
After Refactoring
|
public class Player {
private String email;
//...
}
public class LeagueOwner {
private String eMail;
//...
}
public class Advertiser {
private String email_address;
//...
}
|
public class User {
private String email;
}
public class Player extends User {
//...
}
public class LeagueOwner extends User {
//...
}
public class Advertiser extends User {
//...
}
|
Forward Engineering
- Object design model before transformation
- Source code after transformation
public class User {
private String email;
public String getEmail() {
return email;
}
public void setEmail(String value){
email = value;
}
public void notify(String msg) {
// ....
}
/* Other methods omitted */
}
|
public class LeagueOwner extends User {
private int maxNumLeagues;
public int getMaxNumLeagues() {
return maxNumLeagues;
}
public void setMaxNumLeagues(int value) {
maxNumLeagues = value;
}
/* Other methods omitted */
}
|
Reverse Engineering
- Applied to a set of source code elements and results in a set of model elements
- Recreate the model for an existing system
Mapping models to code: Overview on Mapping Associations
Unidirectional One-to-One Assoication
|
public class Student {
private Account account;
public Student() {
account = new Account();
}
public Account getAccount() {
return account;
}
}
|
public class Account {
public Account() {
}
}
|
Bidirectional One-to-One Association
|
public class Student {
private Account account;
public Student() {
account = new Account(this);
}
public Account getAccount() {
return account;
}
}
|
public class Account {
private Student owner;
public Account(Student student) {
owner = student;
}
public Student getOwner() {
return student;
}
}
|
One-to-Many Association
|
public class Student {
private Set accounts;
public Student() {
accounts = new HashSet();
}
public void addAccount(Account account) {
accounts.add(account);
}
public Account removeAccount(Account account) {
accounts.remove(account);
}
}
|
public class Account {
public Account() {
}
}
|
Mapping Activities
- Optimizing the Object Design Model
- Mapping Associations
- Mapping Contracts to Exceptions
- Mapping Object Models to Database Tables
Optimizing the Object Design Model:
Collapsing an object without interesting behavior
Optimizing the Object Design Model:
Delaying expensive computations
Realization of a unidirectional, one-to-one association
- Object design model before transformation
- Source code after transformation
|
public class Advertiser {
private Account account;
public Advertiser() {
account = new Account();
}
public Account getAccount() {
return account;
}
}
|
Bidirectional one-to-one association
- Object design model before transformation
- Source code after transformation
The account field is initialized
in the constructor and never modified.
|
The owner field is initialized during
the constructor and never modified.
|
public class Advertiser {
private Account account;
public Advertiser() {
account = new Account(this);
}
public Account getAccount() {
return account;
}
}
|
public class Account {
private Advertiser owner;
public Account(owner:Advertiser) {
this.owner = owner;
}
public Advertiser getOwner() {
return owner;
}
}
|
Bidirectional, one-to-many association
- Object design model before transformation
public class Advertiser {
- private Set accounts;
public Advertiser() {
accounts = new HashSet();
}
public void addAccount(Account a) {
accounts.add(a);
a.setOwner(this);
}
public void removeAccount(Account a) {
accounts.remove(a);
a.setOwner(null);
}
}
|
public class Account {
private Advertiser owner;
public void setOwner(Advertiser newOwner) {
if (owner != newOwner) {
Advertiser old = owner;
owner = newOwner;
if (newOwner != null)
newOwner.addAccount(this);
if (oldOwner != null)
old.removeAccount(this);
}
}
}
|
Exceptions as building blocks for contract violations
- Many object-oriented languages, including Java, do not include built-in support for contracts.
- However, we can use their exception mechanisms as building blocks for signaling and handling contract violations
- In Java we use the try-throw-catch mechanism
- Example:
- Let us assume the acceptPlayer() operation of TournamentControl is invoked with a player who is already part of the Tournament.
- In this case acceptPlayer() should throw an exception of type KnownPlayer.
- See source code on next slide
The try-throw-catch mechanism in Java
|
public class TournamentControl {
private Tournament tournament;
public void addPlayer(Player p) throws KnownPlayerException {
if (tournament.isPlayerAccepted(p)) {
throw new KnownPlayerException(p);
}
//... Normal addPlayer behavior
}
}
|
|
public class TournamentForm {
private TournamentControl control;
private ArrayList players;
public void processPlayerApplications() {
// Go through all the players
for (Iteration i = players.iterator(); i.hasNext();) {
try { // Delegate to the control object.
control.acceptPlayer((Player)i.next());
} catch (KnownPlayerException e) {
// If an exception was caught, log it to the console
ErrorConsole.log(e.getMessage());
}
}
}
}
|
Implementing a contract
- For each operation in the contract, do the following
- Check precondition: Check the precondition before the beginning of the method with a test that raises an exception if the precondition is false.
- Check postcondition: Check the postcondition at the end of the method and raise an exception if the contract is violoated. If more than one postcondition is not satisfied, raise an exception only for the first violation.
- Check invariant: Check invariants at the same time as postconditions.
- Deal with inheritance: Encapsulate the checking code for preconditions and postconditions into separate methods that can be called from subclasses.
Mapping an object model to a relational database
- UML object models can be mapped to relational databases:
- Some degradation occurs because all UML constructs must be mapped to a single relational database construct - the table.
- UML mappings
- Each class is mapped to a table
- Each class attribute is mapped onto a column in the table
- An instance of a class represents a row in the table
- A many-to-many association is mapped into its own table
- A one-to-many association is implemented as buried foreign key
- Methods are not mapped
Mapping the User class to a database table
Primary and Foreign Keys
- Any set of attributes that could be used to uniquely identify any data record in a relational table is called a candidate key.
- The actual candidate key that is used in the application to identify the records is called the primary key.
- The primary key of a table is a set of attributes whose values uniquely identify the data records in the table.
- A foreign key is an attribute (or a set of attributes) that references the primary key of another table.
Example for Primary and Foreign Keys
Buried Association
- For one-to-many associations we add the foreign key to the table representing the class on the "many" end.
- Associations with multiplicity "one" can be implemented using a foreign key. Because the association vanishes in the table, we call this a buried association.
- For all other associations we can select either class at the end of the association.
Realizing Inheritance
- Relational databases do not support inheritance
- Two possibilities to map UML inheritance relationships to a database schema
- With a separate table (vertical mapping)
- The attributes of the superclass and the subclasses are mapped to different tables
- By duplicating columns (horizontal mapping)
- There is no table for the superclass
- Each subclass is mapped to a table containing the attributes of the subclass and the attributes of the superclass
Realizing inheritance with a separate table
Realizing inheritance by duplicating columns
Comparison: Separate Tables vs Duplicated Columns
- The trade-off is between modifiability and response time
- How likely is a change of the superclass?
- What are the performance requirements for queries?
- Separate table mapping
- We can add attributes to the superclass easily by adding a column to the superclass table
- Searching for the attributes of an object requires a join operation.
- Duplicated columns
- Modifying the database schema is more complex and error-prone
- Individual objects are not fragmented across a number of tables, resulting in faster queries
Thanks for Reading
If you would rather like to have this lecture note in printed format, please click the print action link in the top right corner.
If you find any problem in this lecture note, please feel free to tell Steven by steven@findaway.hk