Don’t learn the tricks of the trade. Learn the trade.
—Anonymous
This chapter shows how to translate the collaboration patterns into programming code in the Java and Squeak Smalltalk languages. The next chapter shows how to build on this code to implement business rules. While there are many ways to implement an object model, and many styles for coding objects, this chapter shows the advantages of using a consistent approach. By consistently using collaboration patterns and consistently organizing business rules into property and collaboration rules, it is rather straightforward to construct a coding process for implementing the patterns and rules.
The benefits of using a consistent collaboration-based coding process are:
• Faster code development
• Efficient code reviews
• Sound business rule management
There is no magic bullet for faster code development. In general, coding is difficult and requires much thought and organization; however, occasionally we uncover coding tasks that are repetitive and mechanical, and these lend themselves to a repeatable process. Two such tasks are coding an object definition and setting up collaboration rules. These tasks can be systematized into repetitive processes, thus enabling a developer to quickly lay the infrastructure needed to build the real guts of the system—the business services.
Code reviews are something lots of people will tell you are needed, but few of those people actually do them. If everyone is following a consistent approach for defining objects and setting up collaboration rules, then the code produced can be reviewed methodically and thoroughly by peers familiar with the standard. By having something concrete to compare against the code, the reviews can be more straightforward and less uncomfortable.
Business rules change as markets change and e-businesses evolve. Having a consistent methodology for coding collaboration rules allows us to quickly scan the code to find rules that need updating and locate where new rules should be added. We can then make changes without upsetting the rest of the code base. Additionally, since the rules are organized in a consistent manner, the rules can be extracted into a more flexible, pluggable architecture.
An “object definition” is the code needed to define the properties, services, and collaborations for a single kind of object in the object model. Object definitions are implemented in non-object-oriented programming languages by mapping the object paradigm onto the languages’ constructs. Regardless of how a language supports objects, the “parts” of the object definition are remarkably the same. This consistency means that a standard checklist for defining objects will work within multiple languages.
The “DIAPER” process is a six-step process for creating an object definition with the minimum infrastructure to support collaboration rules and business services. We devised the mnemonic while teaching Smalltalk to COBOL programmers. The name helped them grasp the object-oriented programming paradigm. We have translated the DIAPER process to C++ and Java and have used it to great effect in our own development efforts. The DIAPER steps are described in Table 7.1, and a summary version is included in Appendix B.
As discussed earlier, all 12 collaboration patterns can be summarized by three fundamental patterns: (1) generic – specific, (2) whole – part, and (3) transaction – specific. Using the three fundamental patterns along with the DIAPER steps gives us three coding templates capable of implementing all 12 collaboration patterns. The remainder of this chapter presents the three coding templates.
This section discusses guidelines for implementing generic – specific collaborations. Some of these apply to other fundamental patterns, and some only apply to generic – specific collaboration. Subsequent sections apply the guidelines to Java and Squeak implementations.
Collaboration patterns described by the generic – specific pattern are:
• actor – role
• item – specific item
• composite transaction – line item
Any coding template for the generic – specific pattern must accommodate the object inheritance mechanism, which specifies the properties and services in the generic that are accessible from the specific object. What object inheritance really means is that certain determine mine and analyze transactions services available in the generic are also available in the specific.1 Restating this in implementation terms, object inheritance requires the specific’s object definition to include some of the same services in the generic’s object definition. The code is not the same in each object definition, but services with the same names have the same inputs and outputs in each object definition. This description of a service in terms of its name, inputs, and outputs is called a “service signature.” A service that is object inherited has the same service signature in the child object definition as it does in the parent object definition.
EXAMPLE—A person can play different roles—chair, admin, or member—on many different teams, and each person has a name, title, and email. Using the collaboration patterns, a person object (actor) serves as the parent to some number of team member objects (roles). See Figure 7.1.
Object inheritance requires that the object definitions for a Person and for a TeamMember both contain methods with the following signatures:
In the Person object definition, the getTitle service is described in pseudo-code to return a Person object’s title property:
String getTitle { return my title }
In the TeamMember object definition, the getTitle service is described in pseudo-code to return the result of the TeamMember object asking its Person object to get its title.
String getTitle { ask my person to return its title }
While the previous chapter presented strategies for determining which services are object inherited, the Person – TeamMember example highlights the need for a mechanism capable of specifying method signatures for those object inherited services. In object-speak, the construct for specifying expected behavior is an “interface.”2 Technically, an interface is nothing more than a collection of method signatures. Some people understand interfaces, and some people don’t so be careful when using them in your analysis object models. When an object definition is associated with an interface, the object definition must implement a matching method for each method signature in the interface. In object-speak, the object definition implements the interface, and its objects exhibit the interface.
Object inheritance uses an interface to specify parent behaviors object inherited by its child objects. This interface is called a profile interface because any child exhibiting the interface takes on the appearance of its parent. Both parent and child object definitions implement the profile interface.
EXAMPLE—In the Person – TeamMember collaboration, the IPersonProfile interface contains all the object inheritable services of Person. The Person and TeamMember object definitions show that they implement the IPersonProfile interface by including it in their service sections.3 This shorthand implies that all the services in the interface are also included in the Person and TeamMember object definitions. See Figure 7.2.
Interfaces help achieve object pluggability, which means that different objects can be plugged into the same system component. The profile interface helps object pluggability by allowing a child object to pass itself off as its parent in read-only scenarios. Either a parent or a child object can be plugged into a system component designed to request the services described in the profile interface. In other words, the component is not hardwired to work with only one type of object. Instead, the system component is built to use an interface and can work with any object exhibiting the interface. A system component built to use a profile interface has read-only pluggability.
Read-only pluggability is not enough for frameworks and systems expected to grow over time. These require pluggability in handling collaborations and performing conduct business services. To achieve full object pluggability, all the services of the objects must be described with interfaces.4 Interfaces that include conduct business services are called conduct business interfaces. Any object exhibiting a conduct business interface can be plugged into a system component built to use that interface. Business rules permitting, such a component can edit the object, change its collaborations, and kick off conduct business services. In object inheritance, the parent’s conduct business interface extends its profile interface. For a reference table highlighting the different responsibilities of the profile and conduct business interfaces, see Appendix B.
EXAMPLE—Define an interface, IPerson, that contains all the conduct business services and the non-profile determine mine and analyze transactions services. The arrow indicates that the IPerson interface extends the IPersonProfile interface and that IPerson enforces all the method signatures enforced by IPersonProfile. See Figure 7.3.
In many parent – child collaborations, the child object may later become a parent to another kind of object. This is especially common with systems involving many types of roles where a role serves as the parent to a more specialized role. To prepare for this, frameworks and evolving systems make child objects parent-ready by defining two interfaces for them, a profile interface and a conduct business interface. With a profile interface of its own, the child object definition can support object inheritance of its own properties and services.
EXAMPLE—Define profile and conduct business interfaces for TeamMember (see Figure 7.4). The TeamMember profile interface extends the Person profile interface. If TeamMember ever becomes a parent, the new child profile interface will extend the TeamMember profile interface.
Often, a type of object cannot exist without certain data or a certain collaborator. Rules specifying information an object needs to exist are called initialization rules. A specific always has an initialization rule requiring a generic, because a specific cannot be created without a generic parent. A generic may have an initialization rule for properties it needs to exist.
EXAMPLE—A person cannot exist without a valid name. A videotape cannot exist without its video title, which is also its parent object.
Object construction is the process by which objects are created and initialized. Initialization sets the newly created object’s properties to default values or to given values supplied when the object is created. Because objects frequently have many properties, it is impractical to allow given values to be supplied for all the properties when creating a new object. We use object initialization rules to select which properties can be supplied given values during object creation. With this approach, an object construction method can only accept given values for the properties and collaborators mentioned in the initialization rules. This approach cuts down on “constructor explosion,” where an object definition has lots of construction methods based on many permutations of the object’s properties and collaborators.
Objects require storage to hold their properties and collaborations, and they get their storage allocations from the variables specified within their object definitions. The decision about whether a property is stored or derived is made either during the design or during the implementation of the object definition. Derived properties will be determined by a method; stored properties will be held in a variable specified in the object definition.
An object definition usually includes references to its collaborators.5 In the generic – specific collaboration, how many specific collaborators a generic knows can vary, so there are three approaches for referencing specific collaborators of a generic. First, if a generic knows only one specific of a particular type, then it includes a variable to store a reference to that specific. Second, if a generic knows multiple specifics of the same type, it can store a homogeneous collection of those. Third, if a generic knows multiple specifics of different types, it can store a non-homogeneous collection of specifics. This book uses the first and second approaches. While the third approach has merit in its flexibility and extensibility, it requires too much searching to locate an appropriate specific. In general, the object definition of a generic includes a collection of specifics only if its specifics are all the same type. Otherwise, a generic should have a distinct variable for each distinct specific type. Define a specific with a variable to contain its generic.
EXAMPLE: actor – role—A domain allows a person to take on employee, customer, and broker roles. A person can have at most one employee role, one active customer role, and any number of broker roles. The person object definition includes separate collaboration variables for each role type. The employee variable, if set, references a single employee object. The customer variable references a collection of customer objects, but only one can be active. The broker variable references a collection of broker objects. Each role includes a collaboration variable that references its person object.
EXAMPLE: item – specific item—A video store with both DVDs and videotapes for rent describes each movie with a movie title description. A movie title may have several video versions: wide-screen, standard format, Spanish subtitles, etc. Each video version is described with its own video title, which is a child of the movie title. DVD titles describe different DVD versions (see Figure 7.5). The movie title object definition has two collaboration variables: one references its collection of video title child objects, and the other references a collection of DVD titles. Each video title and DVD title includes a collaboration variable that references its movie title.
EXAMPLE: composite transaction – line item—A point-of-sale transaction for a retail clothing store includes both sale and return line items. The point-of-sale transaction object definition has two collaboration variables: one references its collection of sale line items objects, and the other references a collection of return line items. Each line item includes a collaboration variable that references its point-of-sale transaction.
Collaborator accessors add, remove, and get collaborators.6 All generics add and remove specifics with the following accessors:
A generic with a single specific has the following accessor:
A generic with multiple specifics has the following accessors: The second one is optional, and used as needed by the domain.
A specific has the following accessor methods:
Don’t confuse printing an object with displaying an object. Print methods are used primarily for debugging and testing. A useful print method returns a description of an object in terms of its properties and collaborators.
The smart technique for printing an object’s collaborators is to ask each collaborator to print itself; however, with two-way collaborations, print methods calling other print methods leads to infinite loops. Yikes! Clearly, only one collaborator should be using the print service of the other collaborator, but which one?
Here is a good rule of thumb: Put work in the most specific collaborator. The specific has the most knowledge and so putting the work there places it closer to the data. Also, a generic may have many kinds of specifics and each may require a different implementation of the work.
A generic should print its own properties, but should not ask its specific to print itself. On the other hand, a specific should print its own properties, and ask its generic to print itself.
Frequently, object models have collaboration rules that prohibit duplicate objects. Most systems don’t want to add an object to a collection twice, or at least want smart processing to handle it when it happens. The trick is having a way to compare the object being added against the ones already there.
What makes two objects equal depends on collaboration rules and design considerations. Two persons with the same Social Security Number may be considered equal, but two with the same date of birth are not. Business rules and design considerations determine which properties and collaborations come into play when comparing objects for equality.
Good object think dictates that an object can determine for itself if it is equal to another object. Within the equals method, the object doing the work compares its properties and collaborators against those of the other object. Much like the print method, infinite loops in the equals method are a possibility with two-way collaborations. Again, let the specific do the work.
When a generic compares itself to another generic, their specifics don’t enter into it. However for a specific, the generic holds a significant portion of its state. So when a specific compares itself to another specific, the generics must also be compared.
Test objects are objects populated with realistic property values, and are, when possible, associated with collaborators with realistic data. Test objects do not replace full-fledged test cases and testing harnesses, but they do provide the following:
• Quick tests of the object infrastructure
• Building blocks for growing the system incrementally
• Instructive examples for understanding the object
Test objects live with the code, so they can be used by a developer to test code as it is written and when it is modified. Think unit testing. The point of test objects is to avoid the big-bang-test-at-the-end syndrome, and to allow testing even when test databases are not available.
After unit testing, another value of test objects is that they can be linked together according to the collaboration patterns in the object model. Connecting collaborators together allows testing of the collaboration rules and non-trivial business services.
New people coming on-board an expanding project need to ramp up quickly. Sometimes that means reading other people’s code. If that code contains test objects, then they can see examples as they read the code, and can, with little additional effort, write test cases to exercise the code.
Create test objects to illustrate interesting scenarios, to test state changes and border conditions on property values, and to create stereotypical objects for collaborations. Use the “Most Specific Carries the Load” principle (74) to code the test objects. Define a generic test object with realistic property values but without a specific collaborator; define a specific test object with realistic property values and a generic test object collaborator.
Putting all this together gives a concise coding template for generic – specific collaborations. What follows is an example of that template applied to an actor – role collaboration.
A quick note on terminology: From here on, we will refer to object definitions as classes, call the class inheritance generalization class the superclass, call the specialization class a subclass, and refer to coded services as methods.
For our generic – specific example, we implement the Person – TeamMember object model shown in Figure 7.4. This model is an example of a generic (Person) with many specifics (TeamMembers) of the same type.
Objects resist accepting an illegal property value, violating a collaboration rule, or performing a service while in an invalid state. These actions would violate business rules. How objects handle business rule violations is an important design consideration when translating object models into code. Objects in object models do not have presentation logic, so they cannot beep, flash, or present a dialog box. Rather, if asked to break a business rule, the appropriate response for one of these objects would be to stop processing and raise an exception describing the violation. Other objects within the system can decide how to handle the business exception, by notifying a user, logging it in a report, or notifying some external system.
This example defines a simple subclass of the Java Exception
class to throw messages when illegal values are encountered or business rules are violated (see Listing 7.1).
Listing 7.1. Java class for BusinessRuleException.
public class BusinessRuleException extends Exception
{
public BusinessRuleException() { super(); }
public BusinessRuleException( String message) { super(message);
}
}
In Squeak, business exception classes are subclasses of the Error
class hierarchy. No extra methods are necessary as the inherited ones will do everything we need (see Listing 7.2).
Listing 7.2. Squeak class for BusinessRuleException.
Error subclass: #BusinessRuleException
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Nomination-Example'
This section uses Java to apply our coding template for implementing a generic object definition. The class implemented is the Person modeled in Figure 7.4. Immediately after this section, the same example is presented in Squeak.
A person has the following initialization rule: A person must have a name to exist.
The IPersonProfile
interface is implemented by any class that has objects that assume a person profile. In Listing 7.3, both person objects and team member objects exhibit this interface.
Listing 7.3. Java profile interface for Person.
public interface IPersonProfile
{
// ACCESSORS -- get properties
public String getName();
public String getTitle();
public String getEmail();
// DETERMINE MINE
public boolean hasValidEmail();
}
Only the Person
class implements the IPerson
interface. It consists of: (1) the methods that change the state of the object by setting properties or adding collaborators, and (2) the collaboration accessors that are not object inherited (see Listing 7.4). The accessors to get the team members are included in the conduct business interface because child objects do not object inherit their parent’s other children.7 Since these methods change the object, they throw business rule exceptions, except the collaboration get accessor.8 Also, according to the “How I See You” principle (71), collaborators refer to each other through their conduct business interfaces, so the collaboration accessors expect the team member conduct business interface as a parameter.
Listing 7.4. Java conduct business interface for Person.
public interface IPerson extends IPersonProfile
{
// ACCESSORS -- set properties
public void setName( String newName) throws BusinessRuleException;
public void setEmail( String newEmail) throws BusinessRuleException;
public void setTitle( String newTitle) throws BusinessRuleException;
// ACCESSORS – collaborations
public List getTeamMembers();
public void addTeamMember( ITeamMember aTeamMember)
throws BusinessRuleException;
public void removeTeamMember( ITeamMember aTeamMember)
throws BusinessRuleException;
}
To define the Person
class, specify the superclass and interfaces implemented, and define the property and collaboration variables (see Listing 7.5). A Java collection object, ArrayList
, is used to contain the person’s team members. To represent the person’s email address, we defined our own utility class, EmailAddress
, with rules for validating email addresses.9
Listing 7.5. Java class specification for Person.
public class Person extends Object implements IPerson
{
// DEFINE
private String name;
private String title;
private EmailAddress email;
private ArrayList teamMembers;
}
Initialization occurs when an object is created and sets the object’s properties to default or given values.10 Following the “Minimum Parameter Rule” principle (73), we use the person initialization rules to determine the information necessary to create a person object. Because these rules state that a person object cannot exist without a name, two facts become clear: (1) all constructors require a name parameter, and (2) every constructor must validate the name. Consequently, there is no default constructor for the class, and every constructor must be capable of throwing a business rule exception.
The Person
class has one constructor that takes a name value and throws a business rule exception if the name is invalid (see Listing 7.6). The accessor method for setting the name is used to check the validity of the name, and this method throws the business rule exception if the name is bad. Business rule checking is not necessary for the other properties since the object is setting the property values itself. The email property is set to an “empty” email address.
Listing 7.6. Java constructor for Person.
public Person( String newName) throws BusinessRuleException
{
this.setName(newName);
this.title = new String();
this.email = new EmailAddress();
this.teamMembers = new ArrayList();
}
Property accessors are public methods that get and set property values. The set accessors validate the new values and throw business rule exceptions when an illegal value is supplied. A person has accessors for its name, title, and email properties (see Listing 7.7). To the outside world, the email address is a string, thus the get accessor returns a string and the set accessor takes a string. Internally, the string is placed within an email address object that raises an exception if the string is not a valid email address. A special “remove” accessor sets the email address back to its “empty” value.
Listing 7.7. Jave property accessors for Person.
// ACCESSORS - get properties
public String getName()
{
return this.name;
}
public String getTitle()
{
return this.title;
}
public String getEmail()
{
return this.email.getAddress();
}
// ACCESSORS - set properties
public void setName( String newName) throws BusinessRuleException
{
if ((newName == null) || (newName.length() == 0))
throw new BusinessRuleException("Name cannot be null or empty.");
this.name = newName;
}
public void setTitle( String newTitle) throws BusinessRuleException
{
if (newTitle == null)
throw new BusinessRuleException("Title cannot be null.");
this.title = newTitle;
}
public void setEmail( String newEmail) throws BusinessRuleException
{
this.email.setAddress(newEmail);
}
public void removeEmail( ) throws BusinessRuleException
{
this.email.setAddressEmpty();
}
Collaboration accessors add, remove, and get collaborators. A person has collaboration accessors for its team member collaborators (see Listing 7.8). Collaboration rules are not being checked here; those checks will be added in the next chapter. However, this code does check the logic validation rules. For example, the add accessor throws an exception if there is an attempt to add a null team member object because that is a sneaky way of getting around the remove method. Objects don’t like sneaky code.
Similarly, the getTeamMembers
method returns the team member objects in a list that cannot be modified.11 Returning an “unmodifiable” list prevents attempts to add or remove team members without using the accessor methods and checking the collaboration rules. For subclasses that may legitimately need the ability to add and remove from the list, a protected accessor returns a list iterator on the array list. Using a list iterator gives subclasses edit abilities, but not the ability to reset the variable.
Finally, for maximum flexibility, the parameters and return values are typed using interfaces rather than hardwiring them to be a particular class.
Following normal Java coding standards, an object prints its properties and collaborations in its toString
method. Under the “Most Specific Carries the Load” principle (74), the person object, which is a generic, does not print its team member collaborator, which is a specific (see Listing 7.9).
Listing 7.8. Java collaboration accessors for Person.
// ACCESSORS – collaborations
public List getTeamMembers()
{
return Collections.unmodifiableList(this.teamMembers);
}
protected ListIterator getTeamMemberList()
{
return this.teamMembers.listIterator();
}
public void addTeamMember( ITeamMember aTeamMember)
throws BusinessRuleException
{
if (aTeamMember == null)
throw new BusinessRuleException("Cannot add null team member.");
this.teamMembers.add(aTeamMember);
}
public void removeTeamMember( ITeamMember aTeamMember)
throws BusinessRuleException
{
if (aTeamMember == null)
throw new BusinessRuleException("Cannot remove null team member.");
this.teamMembers.remove(aTeamMember);
}
Listing 7.9. Java method for printing Person.
// PRINT
public String toString()
{
StringBuffer buffy = new StringBuffer(60);
buffy.append("Person: ");
buffy.append("\nName: " + this.name);
buffy.append("\nTitle: " + this.title);
buffy.append("\nEmail: " + this.email);
return buffy.toString();
}
Again, following normal Java coding standards, an object checks if it is equal to another object in its equals
method, which takes a parameter of type Object
. Under the “Most Specific Carries the Load” principle (74), the person, as a generic, does not compare its team member collaborator to that of the other person. See Listing 7.10.
Listing 7.10. Java method for checking for equal Persons.
// EQUALS
public boolean equals( Object anObject)
{
if (anObject instanceof Person)
{
Person other = (Person)anObject;
if (!this.name.equals(other.name)) return false;
if (!this.email.equals(other.email)) return false;
if (!this.title.equals(other.title)) return false;
return true;
}
return false; // not instance of Person
}
Run services create objects in the class, pre-loaded with data for testing business services and collaboration rules. We implement services that create objects in the class as static methods (see Listing 7.11).
Listing 7.11. Java test object for Person.
// RUN
public static Person testPerson() throws BusinessRuleException
{
Person aPerson = new Person("Alfred E. Neuman");
aPerson.setEmail("al@neuman.com");
aPerson.setTitle("President");
return aPerson;
}
Once the DIAPER process is complete, a simple test case (see Listing 7.12) ensures the test object works, and exercises some of the accessors with business rules.12 Code that asks an object to perform a service that can potentially throw an exception must be wrapped in try - catch
blocks to handle the exceptions.
Listing 7.12. Java code to run test object.
class TestCase
{
public static void main( String args[])
{
Person aPerson = null;
/** Try to create Person test object.
* If fail print business rule exception message.
* If pass print person test object.
*/
try { aPerson = Person.testPerson(); }
catch (BusinessRuleException ex)
{
System.out.println("BOOM: " + ex.getMessage());
}
System.out.println("\n" + aPerson);
System.out.println("\nPress ENTER to exit");
try { System.in.read(); }
catch (java.io.IOException e) { return; }
} // end main
} // end class TestCase
This example was implemented in Squeak 3.0.
To define the Person
class, specify the superclass and the property and collaboration variables. Using the Model
dependency mechanism, all our problem domain classes are subclasses of Model
(see Listing 7.13).
Listing 7.13. Squeak class definition for Person.
Model subclass: #Person
instanceVariableNames: 'name title email teamMembers '
classVariableNames: ''
poolDictionaries: ''
category: 'Collaboration-Examples'
Similar to Java constructors, Squeak has class methods for creating objects. These methods must be made to respect the business rule that a person object cannot exist without a name; here is how: (1) override the default object creation method, new
, to throw an exception and prevent a person from being created without a valid name; and (2) write a newWith:
method for creating a person with a name (see Listing 7.14). In the newWith:
method, the inherited new
method is invoked to create an uninitialized person object, and the newly created object is asked to initialize
itself. As with the Java constructors, the set accessor for the name property throws a business rule exception if the proposed name is bad.
Here is a quick note on our parameter naming conventions: Since Squeak does type checking only at run-time, method arguments should be named to indicate the kind of object expected.13 When an argument is to be used for a particular purpose, such as representing a person’s name or address, then include the purpose in the argument name, too.
Listing 7.14. Squeak methods for creating Persons.
Person class methodsFor: 'instance creation'
new
BusinessRuleException signal: 'Cannot create person without a name.'
newWith: aNameString
| aPerson |
aPerson ← super new initialize.
aPerson name: aNameString.
↑aPerson
Following this convention, property accessors have two-part arguments:
• The first part indicates what the argument represents (“aName
”).
• The second part indicates the object type (“String
”).
The initialize
method sets a person’s properties to default values, which in this case, are empty strings, and sets the collaboration variable to an empty ordered collection (see Listing 7.15). Usually an object only initializes itself once, when it is created.
Listing 7.15. Squeak method for initializing a Person.
Person methodsFor: 'initialize-release'
initialize
name ← String new.
title ← String new.
email ← String new.
teamMembers ← OrderedCollection new
Using the Model
dependency mechanism, each set accessor sends a change notification when the property value changes. For validating email addresses, the email set accessor uses the Squeak library class, MailAddressParser
(see Listing 7.16).
Collaboration accessors are placed into their own message category to distinguish them from property accessors. As with the Java implementation, these accessors are not yet checking the collaboration rules. See Listing 7.17.
Collaboration accessors follow the same argument naming conventions as property accessors; however, with collaboration accessors, the argument’s type is usually the same as what it represents. For example, a person’s team member collaborator is an object of type TeamMember
. So, the argument for a collaboration accessor is named after the type of the collaborator (“aTeamMember
”).
Listing 7.16. Squeak property accessors for Person.
Person methodsFor: 'accessing'
email
↑email
email: anEmailAddressString
| aTrimmedEmailAddressString |
[MailAddressParser addressesIn:
(aTrimmedEmailAddressString ← anEmailAddressString withBlanksTrimmed)]
on: Error do: [:ex | BusinessRuleException signal: 'Bad email address.'].
email ← aTrimmedEmailAddressString.
self changed: #email
name
↑name
name: aNameString
| aTrimmedNameString |
(aNameString isNil or:
[(aTrimmedNameString ← aNameString withBlanksTrimmed) isEmpty])
ifTrue: [BusinessRuleException signal: 'Person name cannot be nil or empty.'].
name ← aTrimmedNameString.
self changed: #name
title
↑title
title: aTitleString
aTitleString ifNil: [BusinessRuleException signal: 'Person cannot have nil title.'].
title ← aTitleString withBlanksTrimmed.
self changed: #title
Listing 7.17. Squeak collaboration accessors for Person.
Person methodsFor: 'collaboration-accessing'
teamMembers
↑teamMembers
addTeamMember: aTeamMember
aTeamMember
ifNil: [BusinessRuleException signal: 'Tried to add nil team member.'].
self teamMembers add: aTeamMember.
self changed: #teamMembers
removeTeamMember: aTeamMember
aTeamMember
ifNil: [BusinessRuleException signal: 'Tried to remove nil team member.'].
self teamMembers remove: aTeamMember ifAbsent: [ ].
self changed: #teamMembers
Following normal Squeak coding standards, an object prints its properties and collaborations in its printOn
: method. Under the “Most Specific Carries the Load” principle (74), the person object, which is a generic, does not print its team member collaborator, which is a specific. See Listing 7.18.
Listing 7.18. Squeak method for printing Person.
Person methodsFor: 'printing'
printOn: aStream
aStream nextPutAll: 'Person:'.
aStream cr; nextPutAll: self name.
aStream cr; nextPutAll: self title.
aStream cr; nextPutAll: self email
Squeak allows the = operator to be overridden in any class (see Listing 7.19). This operator enables an object to check if it is equal to another. Under the “Most Specific Carries the Load” principle (74), the person, as a generic, does not compare its team member collaborator to that of the other person.
Listing 7.19. Squeak method for checking for equal Persons.
Person methodsFor: 'comparing'
= anObject
self species = anObject species ifFalse: [↑false].
self name = anObject name ifFalse: [↑false].
self title = anObject title ifFalse: [↑false].
self email = anObject email ifFalse: [↑false].
↑true
Many Squeak library classes have examples that create sample objects to assist with learning about the class. In a similar vein, the examples
message category contains one or more class methods for building person objects pre-loaded with data. See Listing 7.20.
Squeak has such an interactive environment, programmers often unit test by inspecting the test object and sending it messages from the inspector.
Listing 7.20. Squeak test object for Person.
Person class methodsFor: 'examples'
testPerson
"Person testPerson"
| aPerson |
aPerson ← Person newWith: 'Alfred E. Neuman'.
aPerson email: 'al@neuman.com'.
aPerson title: 'President'.
↑aPerson
This section applies our coding template for implementing a specific object definition. The class implemented is TeamMember
, which is modeled in Figure 7.4, and both the Java and Squeak code are shown in this section.
A team member has the following initialization rules:
• A team member must have a person to exist.
• A team member has its initial security level set to “low.”
• A team member has its initial role set to “member.”
To specify object inheritance between a team member and its person, the team member profile interface extends the person profile interface. The team member profile interface also describes the get accessors and determine mine services of team members. The implementation of this interface in Java is shown in Listing 7.21.
In part, because Squeak is a late-binding language that does not do compile-time type checking, there is no Squeak equivalent to the Java interface. A Squeak object exhibits its interface at run-time; it either understands a message or it doesn’t.
The team member conduct business interface extends the team member profile interface and includes the set accessors and conduct business services particular to team members. Because a team member’s privileges are coupled to the value of its role property, special conduct business services—makeAdmin
, makeMember
, and makeChair
—set both properties together. Set accessors for the role property exist, but are not included in the conduct business interface because using them alone could put a team member in an invalid state. The Java code for this interface follows in Listing 7.22.
Listing 7.21. Java profile interface for TeamMember.
public interface ITeamMemberProfile extends IPersonProfile
{
// ACCESSORS -- get properties
public SecurityLevel getSecurityLevel();
// ACCESSORS - get property values
public boolean isRoleAdmin();
public boolean isRoleChair();
public boolean isRoleMember();
// ACCESSORS -- get collaborators
public IPerson getPerson();
// DETERMINE MINE
public boolean hasNominatePrivilege();
public boolean hasDeletePrivilege();
}
Again, there is no Squeak equivalent. From this point forward, we will assume the reader knows that interfaces are not used in Squeak, and therefore only the Java code for them will be presented.
The TeamMember
class has instance variables for its properties and its person collaborator. Both the Java and Squeak code implement a utility class, SecurityLevel
, to represent the security levels for team members. Later, we will see that a document also has a security level, and it plays a part in the collaboration rules that govern the document’s nomination process. Since SecurityLevel
must be reused among several different domain classes, it is a standalone class and not implemented as an inner class. The complete code for this class is on the CD available with this book.
In Java, the TeamMember
class implements the team member conduct business interface (see Listing 7.23). The role property is implemented using an inner class, TeamRole
, whose objects have an integer code to efficiently represent, compare, and store the different role values and a string to cleanly print and display the role value14 (see Listing 7.24). Three team role objects, representing the admin, chair, and member role values, are created and stored in static variables within the TeamMember
class.15 These are used in the property value accessors defined later.16
Listing 7.22. Java conduct business interface for TeamMember.
public interface ITeamMember extends ITeamMemberProfile
{
// ACCESSORS -- add collaborators
public void addPerson( IPerson aPerson ) throws BusinessRuleException;
// CONDUCT BUSINESS
public void makeAdmin() throws BusinessRuleException;
public void makeChair() throws BusinessRuleException;
public void makeMember() throws BusinessRuleException;
public void grantNominatePrivilege() throws BusinessRuleException;
public void grantDeletePrivilege() throws BusinessRuleException;
public void revokeNominatePrivilege() throws BusinessRuleException;
public void revokeDeletePrivilege() throws BusinessRuleException;
}
Listing 7.23. Java class specification for TeamMember.
public class TeamMember extends Object implements ITeamMember
{
// DEFINE
private IPerson person;
private byte privileges;
private TeamRole role;
private SecurityLevel securityLevel;
}
In Squeak, TeamMember
goes into the same class category as Person
(see Listing 7.25). The role property is implemented using Squeak association objects. Each association has a symbol key to efficiently represent, compare, and store the different role values and a string to cleanly print and display the role value. Values for the role property are returned by methods in the constants
message category (see Listing 7.26).
Listing 7.24. Java inner class definition for TeamRole.
private static class TeamRole
{
private int code;
private String role;
TeamRole(int roleCode, String roleString)
{
this.code = roleCode;
this.role = roleString;
}
public int getCode() { return this.code; }
public String toString(){ return this.role; }
public boolean equals(Object anObject)
{
if (anObject instanceof TeamRole)
return (this.code == ((TeamRole)anObject).getCode());
else return false;
}
}
Listing 7.25. Squeak class definition for TeamMember.
Model subclass: #TeamMember
instanceVariableNames: 'person team privileges role securityLevel '
classVariableNames: ''
poolDictionaries: ''
category: 'Collaboration-Examples'
Listing 7.26. Squeak constants for TeamMember role.
TeamMember methodsFor: 'constants'
roleAdmin
↑Association key: #admin value: 'Admin'
roleChair
↑Association key: #chair value: 'Chair'
roleMember
↑Association key: #member value: 'Member'
Applying the initialization rules and the “Minimum Parameter” principle (73), TeamMember
requires one constructor in Java and one class new
method in Squeak; each takes only a single parameter. The following Java and Squeak methods initialize the new object’s properties before making the collaboration. Often, collaboration rules check property values, and if the properties are not yet initialized, then errors can occur.
The default constructor is not implemented in the Java version because the person collaboration is required for the parent – child relationship. The collaboration accessor for adding a person can throw a business rule exception; therefore, the constructor is declared to throw an exception. See Listing 7.27.
Listing 7.27. Java constructor for TeamMember.
public TeamMember(IPerson aPerson) throws BusinessRuleException
{
this.makeMember();
this.securityLevel = new SecurityLevel();
this.addPerson(aPerson);
}
The Squeak version’s default new
method is overridden to throw an exception, and a newWith:
method requiring a person object provides the only means for creating team member objects (see Listing 7.28).
To support object inheritance TeamMember has get accessors for three of its person’s properties: name, title, and email. Notice there are no set accessors for these properties. Also, the security level cannot be set. Instead, the security level has its own accessors for changing its level.
In Java, the accessors for object inherited properties return the same types as similarly named accessors in the Person
class. See Listing 7.29.
Listing 7.28. Squeak methods for creating and initializing TeamMembers.
TeamMember class methodsFor: 'instance creation'
new
BusinessRuleException signal: 'Cannot create team member without a person.'
newWith: aPerson
| aTeamMember |
aTeamMember ← super new initialize.
aTeamMember addPerson: aPerson.
↑aTeamMember
TeamMember methodsFor: 'initialize-release'
initialize
person ← nil.
self makeMember.
securityLevel ← SecurityLevel new
Listing 7.29. Java property accessors for TeamMember.
// ACCESSORS -- get properties
public SecurityLevel getSecurityLevel()
{
return this.securityLevel;
}
// ACCESSORS -- get inherited properties
public String getName()
{
return this.person.getName();
}
public String getTitle()
{
return this.person.getTitle();
}
public String getEmail()
{
return this.person.getEmail();
}
The Squeak implementations of the accessors for the inherited properties go in the same message category as the team member accessors to emphasis the transparency of these properties (see Listing 7.30).
Listing 7.30. Squeak property accessors for TeamMember.
TeamMember methodsFor: 'accessing'
securityLevel
↑securityLevel
privileges
↑privileges
role
↑role
title
↑self person title
email
↑self person email
name
↑self person name
Instead of simple accessors for the role and privileges properties, TeamMember
has special get and set accessor services to protect values of these properties and encapsulate their implementations. The special get accessors check the properties for particular values, and the special set accessors assign the properties particular values. In both cases, the name of the particular value is encoded in the accessor name.
The naming convention supports the evolution of these properties into full-blown role history objects.17 If that should happen, then all the methods with “role” in their name would be delegated to services in the role history object.18
Also shown here are the conduct business services that ensure that the proper privileges are assigned when the role property is set. Putting that code in the role set property value accessor would be doing too much work. For example, having the setRoleAdmin
accessor also changing privileges would be creating a side-effect in addition to setting a property, and programming by side-effects has bad karma. To discourage use of the role property value set accessors, such as setRoleAdmin
, we prefix them differently to distinguish them. Our convention for accessors that bypass business rules is to prefix them with “doSet
.” Similarly, get accessors that return raw data values, such getPrivileges
, are prefixed with “doGet
.” For more on why we have these doSet
methods and why they are public, see in the following chapter, “Methods for Enforcing Property Rules.”
In the Java code, static variables (shown in all capital letters) contain the possible values for the role and privileges properties 19 (see Listing 7.31).
Listing 7.31. Java property value accessors for TeamMember.
// ACCESSORS - test for property values
public boolean isRoleAdmin()
{
return this.role.equals(ROLE_ADMIN);
}
public boolean isRoleChair()
{
return this.role.equals(ROLE_CHAIR);
}
public boolean isRoleMember()
{
return this.role.equals(ROLE_MEMBER);
}
public boolean hasNominatePrivilege()
{
return (this.privileges & PRIVILEGES_NOMINATE_MASK) > 0;
}
public boolean hasDeletePrivilege()
{
return (this.privileges & PRIVILEGES_DELETE_MASK) > 0;
}
// ACCESSORS - set properties to special values
public void doSetRoleAdmin()
{
this.role = ROLE_ADMIN;
}
public void doSetRoleChair()
{
this.role = ROLE_CHAIR;
}
public void doSetRoleMember()
{
this.role = ROLE_MEMBER;
}
// CONDUCT BUSINESS
public void makeMember() throws BusinessRuleException
{
this.doSetRoleMember();
this.privileges = PRIVILEGES_DEFAULT_MASK;
}
public void makeAdmin() throws BusinessRuleException
{
this.doSetRoleAdmin();
this.grantNominatePrivilege();
this.revokeDeletePrivilege();
}
public void makeChair() throws BusinessRuleException
{
this.doSetRoleChair();
this.grantNominatePrivilege();
this.grantDeletePrivilege();
}
public void grantNominatePrivilege()
{
this.privileges |= PRIVILEGES_NOMINATE_MASK;
}
public void grantDeletePrivilege()
{
this.privileges |= PRIVILEGES_DELETE_MASK;
}
public void revokeNominatePrivilege()
{
if (this.hasNominatePrivilege())
this.privileges ^= PRIVILEGES_NOMINATE_MASK;
}
public void revokeDeletePrivilege()
{
if (this.hasNominatePrivilege())
this.privileges ^= PRIVILEGES_DELETE_MASK;
}
In the Squeak code, additional instance methods that are not shown here return the possible values for the role and privileges properties 20 (see Listing 7.32).
Listing 7.32. Squeak property value accessors for TeamMember.
TeamMember methodsFor: 'accessing'
isRoleAdmin
↑self doGetRole = self roleAdmin
isRoleChair
↑self doGetRole = self roleChair
isRoleMember
↑self doGetRole = self roleMember
TeamMember methodsFor: 'testing'
hasDeletePrivilege
↑self getPrivileges anyMask: self privilegeDeleteMask
hasNominatePrivilege
↑self getPrivileges anyMask: self privilegeNominateMask
TeamMember methodsFor: 'domain services'
makeAdmin
self doSetRoleAdmin.
self grantNominatePrivilege.
self revokeDeletePrivilege
makeChair
self doSetRoleChair.
self grantNominatePrivilege.
self grantDeletePrivilege
makeMember
self doSetRoleMember.
self doSetPrivileges: self privilegeDefaultMask
grantDeletePrivilege
self doSetPrivileges: (self doGetPrivileges bitOr: self privilegeDeleteMask)
grantNominatePrivilege
self doSetPrivileges: (self doGetPrivileges bitOr: self privilegeNominateMask)
revokeDeletePrivilege
self hasDeletePrivilege
ifTrue: [self doSetPrivileges: (self doGetPrivileges bitXor: self privilegeDeleteMask)]
revokeNominatePrivilege
self hasNominatePrivilege
ifTrue: [self doSetPrivileges: (self doGetPrivileges bitXor: self privilegeNominateMask)]
TeamMember methodsFor: 'private'
doSetRoleAdmin
role ← self roleAdmin.
self changed: #role
doSetRoleChair
role ← self roleChair.
self changed: #role
doSetRoleMember
role ← self roleMember.
self changed: #role
doSetPrivileges: bits
privileges ← bits.
self changed: #privileges
Since a child cannot exist without its parent object, only the get and set accessors are written for the person collaborator. In Listings 7.33 and 7.34 the code is not checking collaboration rules. Can a team member change its person? No, but we will enforce that along with other business rules when we implement them in the next chapter.
Using the “Most Specific Carries the Load” principle (74), a team member has the responsibility of printing its properties and asking its generic collaborations to print it (see Listings 7.35 and 7.36).
Listing 7.33. Java collaboration accessors for TeamMember.
// ACCESSORS – collaborators
public IPerson getPerson()
{
return this.person;
}
public void addPerson( IPerson aPerson) throws BusinessRuleException
{
if (aPerson == null)
throw new BusinessRuleException("Tried to add null person");
this.person = aPerson;
}
Listing 7.34. Squeak collaboration accessors for TeamMember.
TeamMember methodsFor: 'collaboration-accessing'
person
↑person
addPerson: aPerson
aPerson
ifNil: [BusinessRuleException signal: 'Tried to add nil person.'].
person ← aPerson.
self changed: #person
Listing 7.35. Java method for printing a team member.
// PRINT
public String toString()
{
StringBuffer buffy = new StringBuffer(60);
buffy.append("Team Member: ");
buffy.append("\nRole: " + this.role);
buffy.append("\n" + this.securityLevel);
buffy.append(this.person.toString());
return buffy.toString();
}
Listing 7.36. Squeak method for printing a team member.
TeamMember methodsFor: 'printing'
printOn: aStream
aStream nextPutAll: 'TeamMember:'.
aStream cr; nextPutAll: self doGetRole value.
self securityLevel printOn: aStream cr.
self person printOn: aStream cr.
self team printOn: aStream cr
Using the “Most Specific Carries the Load” principle (74), a specific team member has the responsibility of checking equality for itself and asking its generic collaborator, person, to check itself (see Listings 7.37 and 7.38).
Listing 7.37. Java method for checking for equal TeamMembers.
// EQUALS
public boolean equals (Object anObject)
{
if (anObject instanceof TeamMember)
{
TeamMember other = (TeamMember)anObject;
if (!this.role.equals(other.role)) return false;
if (this.person == null && (other.person != null)) return false;
if (this.person != null && (!this.person.equals(other.person)))
return false;
return true;
}
else return false; // not instance of TeamMember
}
Listing 7.38. Squeak method for checking for equal TeamMembers.
TeamMember methodsFor: 'comparing'
= anObject
self species = anObject species ifFalse: [↑ false].
self doGetRole = anObject doGetRole ifFalse: [↑ false].
self person = anObject person ifFalse: [↑ false].
↑ true
Specific test objects require generic test objects. TeamMember
test objects use Person
test objects. The following TeamMember
test objects are created:
• A team member with chair role status and a high security level
• A team member with admin role status and default (low) security level
• A team member with member role status, default security, and no nominate privileges
• A team member with member role status, nominate privileges, and secret security level
Code for only one test object is shown in Listings 7.39 and 7.40. See the CD for the others.
Listing 7.39. Java test object for TeamMember.
// RUN
public static TeamMember testChair() throws BusinessRuleException
{
TeamMember aTeamMember = new TeamMember(Person.testPerson());
aTeamMember.makeChair();
SecurityLevel sLevel = aTeamMember.getSecurityLevel();
sLevel.setLevelHigh();
return aTeamMember;
}
Listing 7.40. Squeak test object for TeamMember.
TeamMember class methodsFor: 'examples'
testChair
"TeamMember testChair"
| aTeamMember |
aTeamMember ← self newWith: Person testPerson and: Team testTeam.
aTeamMember makeChair.
aTeamMember securityLevel setLevelHigh.
↑aTeamMember
Collaboration patterns described by the whole – part pattern are:
• container – content
• assembly – part
• group – member
• outer place – place
Building whole – part collaborations is very similar to building generic – specific collaborations if the whole is considered like the generic and the part is considered like the specific. In fact, coding the whole – part requires only these few modifications to the generic – specific DIAPER:
• Profile interfaces are unnecessary.
• The whole always allows for multiple parts.
• A part may have multiple wholes (e.g., group – member collaborations).
• A part may exist without a whole.
Putting the work in the specific was a major determinant in coding generic – specific collaborations. Applying that same principle when coding whole – part collaborations impacts these steps in the DIAPER:
Unlike the generic – specific collaborations, profile interfaces are unnecessary in whole – part collaborations because whole – part does not use object inheritance. Conduct business interfaces are still recommended when flexibility and extensibility are desired.
This example continues the implementation of the domain we started in the previous section. Here we are implementing the group – member collaboration.
EXAMPLE—A team member belongs to exactly one team. Each team has a description and format, which can be one of the following: no chair, single chair, or multiple chairs (see Figure 7.6). The team’s format defines the following collaboration rule: A team member cannot be added to a team if the team member has a chair role and the team’s format cannot support a chair being added to the team.
In the Team – TeamMember object model, the team member object is playing the specific in a generic – specific collaboration and it is playing the part in a whole – part collaboration.
In terms of implementation, the interesting aspects of the Team
class are the format property and the collection of team member collaborators.
The format property is implemented like the role property of the TeamMember
class, by encapsulating its representation inside the class, and providing special property value accessors. The team member collaboration is implemented similarly. The object definition has an instance variable for a collection object and accessors for adding and removing team members. A get accessor returns a view on the collection, and a special get accessor returns a single team member for a person, assuming the person is on the team.
Determine mine services are included in this conduct business interface because no profile interface is implemented. See Listing 7.41.
Listing 7.41. Java conduct business interface for Team.
public interface ITeam
{
// ACCESSING - get /set properties
public String getDescription();
public void setDescription( String newDescription)
throws BusinessRuleException;
// ACCESSING - get property values
public boolean isFormatMultipleChair();
public boolean isFormatSingleChair();
public boolean isFormatNoChair();
// ACCESSING - set property values
public void setFormatNoChair() throws BusinessRuleException;
public void setFormatSingleChair() throws BusinessRuleException;
public void setFormatMutlipleChair() throws BusinessRuleException;
// ACCESSING -- collaborations
public List getTeamMembers();
public void addTeamMember( ITeamMember aTeamMember)
throws BusinessRuleException;
public void removeTeamMember( ITeamMember aTeamMember)
throws BusinessRuleException;
// DETERMINE MINE
public List getChairs();
public ITeamMember getTeamMember(IPersonProfile aPerson);
}
The Team
class has an instance variable to hold the collection of team members, plus instance variables for the description and format properties.
For the Java version, a static inner class, TeamFormat
, which is not shown here but is in the CD code listings, is used to represent the three format values—no chair, single chair, and multiple chairs (see Listing 7.42).
Listing 7.42. Java class specification for Team.
public class Team extends Object implements ITeam
{
// DEFINE
private String description;
private ArrayList teamMembers;
private TeamFormat format;
}
Listing 7.43. Squeak class definition for Team.
Model subclass: #Team
instanceVariableNames: 'description teamMembers format '
classVariableNames: ''
poolDictionaries: ''
category: 'Collaboration-Examples'
Team initialization code creates the collection object and sets the format to the default of “multiple chairs.”
In Java, the initialization of the array list object and setting the format to the default occurs in the default constructor. See Listing 7.44. No other constructors are needed. In Squeak this initialization occurs in the initialize method. See Listing 7.45.
Listing 7.44. Java constructor for Team.
public Team()
{
this.description = new String();
this.format = FORMAT_MULTIPLE;
this.teamMembers = new ArrayList();
}
The description instance variable has the usual get and set methods, returning and taking a string value, respectively. The format instance variable has get property value accessors of the form “isFormatX” for each X value of the format, and has set property value accessors of the form “setFormatX” for each X value of the format. Both the Java and Squeak code examples are very straightforward, so the code is not included here. As usual, the code is on the CD.
Listing 7.45. Squeak methods for creating and initializing a team.
Team class methodsFor: 'instance creation'
new
↑super new initialize
Team methodsFor: 'initialize-release'
initialize
description ← String new.
format ← self setFormatMultipleChair.
teamMembers ← OrderedCollection new
The team has two get accessors for the team member collaboration: one returns a view containing all the team members, and the second returns a team member for a given person.
The Java methods for managing the team member array list are the same as in person. A public method returns an “unmodifiable” version of the array list, and a protected method returns a list iterator on the array list. Methods to establish and dissolve a team member collaboration add and remove the team member from the array list, respectively. These methods are listed on the CD.
What is new in this Java implementation is the method that locates the team member of the team for a given person. Searching a collection for the first element that returns true for a given test is such a routine occurrence that we created our own utility class, CollectionDetector
, with a detect
method containing code that iterates through a collection. For each element in the collection or list, the detect
method calls the detectBlock
method with the list element and the value that is the subject of the search. If the detect-Block
returns true for any list element, then the search stops and that list element is returned as the result. To use the CollectionDetector
, define a subclass implementing the detectBlock
method with the code needed to locate the desired object in the list. Next, create an object in the subclass, and ask that object to detect within a given list for a given object. The getTeam-Member
method defines the CollectionDetector
subclass as an anonymous inner class, and creates an object in this class to detect a person within the list of team members (see Listing 7.46). Code for the CollectionDetector
class is included on the CD.
Listing 7.46. Java get TeamMember for person collaboration accessor for Team.
public ITeamMember getTeamMember(IPersonProfile aPerson)
{
CollectionDetector personDetector = new CollectionDetector()
{
public boolean detectBlock(Object listElement, Object keyValue)
{
return ((ITeamMember)listElement).getPerson().equals(keyValue);
}
}; // end anonymous inner class definition
return (ITeamMember)personDetector.detect(this.teamMembers, aPerson);
}
In Squeak, collections can be asked to find and return an object that satisfies some test; the message to the collection is detect:ifNone:
, and it returns nil
if no object within the collection satisfies the test (see Listing 7.47).
Listing 7.47. Squeak get teamMemberFor person collaboration accessor for Team.
Team methodsFor: 'collaboration-accessing'
teamMemberFor: aPerson
↑self teamMembers
detect: [:aTeamMember | aTeamMember person = aPerson]
ifNone: [ ]
Using the “Part Carries the Load” principle (76), a team does not ask its parts to print. Instead, the team prints only its own properties.
In Java, Team
implements the usual toString
method to print the description and format properties of a team. In Squeak, Team
implements the printOn:
method to do the same. Code listings are on the CD.
Using the “Part Carries the Load” principle (76), a team compares only its own properties and does not try to compare its team members to those of the other team.
In Java, the equals
method compares the properties of a team. Squeak allows using the =
operator for this comparison. Code listings are on the CD.
By default, a team allows multiple chairs, so two other test objects are required, one for each remaining format value. Code is shown for the single chair team test object in Listings 7.48 and 7.49. Code for other test objects is shown on the CD.
Listing 7.48. Java test object for a team with a single chair.
public static Team testSingleChairTeam() throws BusinessRuleException
{
Team aTeam = new Team();
aTeam.setFormatSingleChair();
aTeam.setDescription("Executive Strategy Team");
return aTeam;
}
Listing 7.49. Squeak test object for a team with a single chair.
Team class methodsFor: 'examples'
testSingleChairTeam
"Team testSingleChairTeam"
| aTeam |
aTeam ← Team new.
aTeam setFormatSingleChair.
aTeam description: 'Executive Strategy Team'.
↑aTeam
This section shows only the parts of the TeamMember
implementation that need to be updated to support the TeamMember - Team collaboration.21
Because a team member represents a person’s participation in a particular team, a team member cannot exist without both a person and a team. Team-Member
thus requires an additional initialization rule: A team member must have a team to exist.
Give TeamMember
an instance variable to reference its team collaborator.
Using the “How I See You” principle (71), the team instance variable in the Java implementation is typed with the team conduct business interface (see Listing 7.50).
In Squeak, we just add a new instance variable to the TeamMember
definition (see Listing 7.51).
Listing 7.50. Java revised class specification for TeamMember.
public class TeamMember extends Object implements ITeamMember
{
// DEFINE
private IPerson person;
private ITeam
team;
private TeamRole role;
private byte privileges;
private SecurityLevel securityLevel;
}
Listing 7.51. Squeak revised class definition for TeamMember.
Model subclass: #TeamMember
instanceVariableNames: 'person team role privileges securityLevel '
classVariableNames: ''
poolDictionaries: ''
category: 'Collaboration-Examples'
The new initialization rule changes how team member objects are created. The team member object inherits some of its properties from its person. The team checks collaboration rules for the team member that may include these object inherited properties, so the collaboration between team member and person must be set before the Team – TeamMember collaboration is established.
Object creation methods that establish two or more collaborations need some extra care. While the first collaboration may pass the rules and be established, the second could fail, in which case, the first must be dissolved, too. In this example, the team member must be removed from the person object if the team member cannot be a part of the team.22
The team object is added to the parameter list of the TeamMember
Java class constructor, and code is added to break the person collaboration if the team collaboration fails (see Listing 7.52).
Listing 7.52. Java revised constructor for TeamMember.
public TeamMember( IPerson aPerson, ITeam aTeam) throws BusinessRuleException
{
this.makeMember();
this.securityLevel = new SecurityLevel();
this.addPerson(aPerson);
try {this.addTeam(aTeam);}
catch(BusinessRuleException anException)
{
aPerson.removeTeamMember(this);
throw anException;
}
}
The existing Squeak object creation method needs to be converted into a method that takes two parameters: a person and a team. Code is added to dissolve the person collaboration if the team collaboration fails (see Listing 7.53).
Listing 7.53. Squeak revised method for creating a team member.
TeamMember class methodsFor: 'instance creation'
newWith: aPerson and: aTeam
| aTeamMember |
aTeamMember ← super new initialize.
aTeamMember addPerson: aPerson.
[aTeamMember addTeam: aTeam]
on: BusinessRuleException
do: [:ex|
aPerson removeTeamMember: aTeamMember.
ex signal].
↑aTeamMember
The new collaboration requires new accessors to get, add, and remove the collaborator.
The Java accessors are similar to those for the person collaborator, so only the method signatures for the accessors are shown in Listing 7.54.
The Squeak methods in Listing 7.55 are added to the “collaboration-accessing” message category.
Listing 7.54. Java method signatures for team collaborator accessors of TeamMember.
public ITeam getTeam();
public void addTeam( ITeam aTeam) throws BusinessRuleException;
public void removeTeam( ITeam aTeam) throws BusinessRuleException;
Listing 7.55. Squeak method signatures for team collaborator accessors of TeamMember.
team
addTeam: aTeam
removeTeam: aTeam
Applying the “Part Carries the Load” principle (76), a team member prints its own details, and asks its team to print its own details as well. Code should be added to the Java toString
method and the Squeak printOn:
method to print the team. These methods are on the CD.
Applying the “Part Carries the Load” principle (76), a team member compares its team to that of another team member. The Java equals
method and the Squeak =
operator are modified to compare the teams. See Listings 7.56 and 7.57.
Listing 7.56. Java revised method for checking for equal TeamMembers.
public boolean equals(Object anObject)
{
if (anObject instanceof TeamMember)
{
TeamMember other = (TeamMember)anObject;
if (!this.role.equals(other.role)) return false;
if (this.person == null && (other.person != null)) return false;
if (this.person != null && (!this.person.equals(other.person)))
return false;
if (this.team == null && (other.team != null)) return false;
if (this.team != null && (!this.team.equals(other.team)))
return false;
return true;
}
else return false; // not instance of TeamMember
}
Listing 7.57. Squeak revised method for checking for equal TeamMembers.
TeamMember methodsFor: 'comparing'
= anObject
self species = anObject species ifFalse: [↑false].
self getRole = anObject getRole ifFalse: [↑false].
self person = anObject person ifFalse: [↑false].
self team = anObject team ifFalse: [↑false].
↑true
Applying the “Part Carries the Load” principle (76), a team member test object uses the team test object. The purpose of the test object is to test the collaboration, not to create a team filled with lots of team members for extensive testing. Tests of that magnitude require more sophisticated test techniques and test harnesses. For our purposes, we need to update all the test methods in both TeamMember
classes to create team members with the new constructor and using the person and team test objects. See Listings 7.58 and 7.59.
Listing 7.58. Java test object for TeamMember as chair of team.
public static TeamMember testChair() throws BusinessRuleException
{
ITeam aTeam = Team.testTeam();
TeamMember aTeamMember = new TeamMember(Person.testPerson(),aTeam)
aTeamMember.makeChair();
SecurityLevel sLevel = aTeamMember.getSecurityLevel();
sLevel.setLevelHigh();
return aTeamMember;
}
Listing 7.59. Squeak test object for TeamMember as chair of team.
TeamMember class methodsFor: 'examples'
testChair
"TeamMember testChair"
| aTeamMember |
aTeamMember ← self newWith: Person testPerson and: Team testTeam.
aTeamMember makeChair.
aTeamMember securityLevel setLevelHigh.
↑ aTeamMember
Collaboration patterns described by the transaction – specific pattern are:
• transaction – role
• transaction – specific item
• transaction – place
• follow-up transaction – transaction
• line item – specific item
Transaction – specific collaborations look deceptively like generic – specific collaborations; however, a transaction is a more active collaborator than a generic. A single transaction often unites multiple collaboration patterns (see Figure 7.7). Because transactions coordinate between many collaboration patterns, they are called the “glue” of the object model.
Here are the important facts about transaction – specific collaborations that shape how they are implemented:
• No object inheritance is involved.
• No collaborator is more “specific” than another.
• A transaction can have multiple specifics, but each is of a different type.
• A specific can have multiple transactions of the same type.
• A specific can have different types of transactions.
As with whole – part, profile interfaces are not necessary; conduct business interfaces are always recommended.
The specific work principle does not apply with transaction – specific. Instead, the relevant rule of thumb arises from the fact that a transaction exists to record the interaction of a person and a thing at a given place. This implies that a transaction cannot exist without its specifics, namely its role, specific item, and place.23 The reverse is clearly not true because a person, place, and thing can exist independently of any transactions.
Read that principle carefully. It does not say to put all the work into a central controller object. Each object is still responsible for performing its own share of the work, but only when cued from the coordinator.
Here is how the directing coordinator principle applies to the transaction – specific DIAPER:
A transaction brings other objects, specifics, together. The specifics are not usually of the same type. Rather than forcing the different types into one collection of specifics, the recommended approach is to define a separate instance variable for each type.
Multiple transactions for a specific record that object’s history of interactions. Also, analyze transactions services do work across the transactions of a specific. The natural implementation defines an instance variable within the specific to hold the transactions.
This example illustrates the transaction – role and transaction – specific item collaborations.
EXAMPLE—A team member can nominate documents for publication on the corporate Web site. Using object think, a document nominates itself for a team member. The document nominate service creates a nomination (transaction) with the team member (role) and itself (specific item).24 See Figure 7.8.
Team member collaboration rules:
• A team member must have nominate privileges to nominate a document.
• A team member can only nominate a fixed number of times per 30 days:
• Five times for a team member whose role is member or admin.
• Ten times for a team member whose role is chair.
Document collaboration rules:
• A document must have a title to be nominated.
• A document cannot be nominated after it is published.
• A document cannot be nominated while it has a pending nomination.
• A document cannot be nominated by a team member with a security level less than the document’s security level.
With the addition of TeamMember – Nomination – Document to our object model, the team member object is now playing in three collaboration patterns. It is playing as the specific in a generic – specific collaboration, as the part in a whole – part collaboration, and as a specific in a transaction – specific collaboration.
Interesting implementation aspects of the document are the security level, publication date properties, and the collection of nominations. We have seen the security level before. This is the same security level object used in the TeamMember
class implementation. Every document is created with an initial security level of “low.”
Publication date is only set if the document is published; otherwise, the date is null. When the state of the object determines whether a property exists or is accessible, it is a business rule violation to request the property when the object is not in the proper state. Accordingly, asking an unpublished document for its publication date is a business rule violation, and so the get accessor throws an exception.
Use a collection to hold collaborators. This collection will be searched to find the approved nomination and to tally the number of nominations within a range of dates. A document has an analyze transactions service to return its approved nomination, because while the document may have any number of pending, in review, and rejected nominations, it can only have one approved nomination. Also, once a document has an approved nomination, its behavior changes. However, asking an unapproved document for its approved nomination is similar to asking an unpublished document for its publication date; it generates a business rule violation.
A document has the following initialization rules:
• A document must have a title property to exist.
• A document by default has its security level set to “low.”
This interface is fairly straightforward. Because there is no profile interface, the conduct business interface has the accessors, determine mine services, and analyze transactions service. It also provides the conduct business services to publish and nominate a document. See Listing 7.60.
Listing 7.60. Java conduct business interface for Document.
public interface IDocument
{
// ACCESSORS -- get properties
public String getTitle();
public SecurityLevel getSecurityLevel();
public Date getPublicationDate() throws BusinessRuleException;
// ACCESSORS -- get collaborators
public List getNominations();
// ACCESSORS -- set properties
public void setTitle(String newTitle) throws BusinessRuleException;
// DETERMINE MINE
public boolean isPublished();
public boolean isApproved();
// ANALYZE TRANSACTIONS
public INomination getApprovedNomination()
throws BusinessRuleException;
// CONDUCT BUSINESS
public void publish() throws BusinessRuleException;
public void nominate(ITeamMember aTeamMember)
throws BusinessRuleException;
}
The Java code keeps the nominations sorted by storing them in a TreeSet
, which is a standard Java class that exhibits the SortedSet
interface. A TreeSet
sorts itself in ascending order with the lowest ordered elements first.
Listing 7.61. Java class specification for Document.
public class Document extends Object implements IDocument
{
private String title;
private Date publicationDate;
private SecurityLevel securityLevel;
private TreeSet nominations;
}
Listing 7.62. Squeak class definition for Document.
Modelsubclass: #Document
instanceVariableNames: 'title publicationDate securityLevel nominations '
classVariableNames: ''
poolDictionaries: ''
category: 'Collaboration-Examples'
Document initialization code creates the collection object, sets the security level to the default, sets the string property to an empty string, and sets the publication date to null. A string title is required for the document to exist, and the set title accessor throws an exception if the title is null. See Listings 7.63 and 7.64.
Listing 7.63. Java constructor for Document.
public Document( String newTitle) throws BusinessRuleException
{
this.setTitle(newTitle);
this.publicationDate = null;
this.securityLevel = new SecurityLevel();
this.nominations = new TreeSet();
}
Listing 7.64. Squeak methods for creating and initializing a document.
Document class methodsFor: 'instance creation'
new
BusinessRuleException signal: 'Cannot create document without a title.'
newWith:aTitleString
| aDocument |
aDocument ← super new initialize.
aDocument title: aTitleString.
↑aDocument
Document methodsFor: 'initialize-release'
initialize
title ← String new.
publicationDate ← nil.
securityLevel ← SecurityLevel new.
nominations ← OrderedCollection new
The usual suspects apply here. The publication date has some interesting accessors. The get accessor throws a business exception if the property is not set. To avoid this unhappy event, an additional property value accessor tests if the publication date is set. These accessors are shown in Listings 7.65 and 7.66; the others are on the CD.
Listing 7.65. Java publicationDate property accessors for Document.
public Date getPublicationDate() throws BusinessRuleException
{
if (this.publicationDate == null)
throw new BusinessRuleException("Document is unpublished.");
else return this.publicationDate;
}
public boolean isPublished()
{
try {this.getPublicationDate();}
catch(BusinessRuleException ex){return false;}
return true;
}
Listing 7.66. Squeak publicationDate property accessors for Document.
Document methodsFor: 'accessing'
publicationDate
publicationDate ifNil: [BusinessRuleException signal: 'Document is unpublished.'].
↑publicationDate
Document methodsFor: 'testing'
isPublished
↑publicationDate notNil
A document’s nominations help determine its state. A document is “approved” for publication when it has an approved nomination. Along with the usual collaboration accessors, there is a service to return the approved nomination. This service raises an exception if the document lacks an approved nomination. To guard against this exception, the document includes an “is approved” service that checks whether the document has an approved nomination without raising an exception.
For the Java version, four collaboration accessors are needed: (1) a public
accessor to return an “unmodifiable” list of nominations, (2) a protected
accessor to return a list iterator for the nominations, (3) a public
method to return the approved nomination, and (4) a public
method to ask if a document is approved. The first two accessors are left to the reader. The “get approved nomination” method locates an approved nomination in the list of nominations by using an anonymous inner class programmed to detect an approved nomination. This technique was explained earlier (see Listing 7.46). The “is approved” method provides a check before trying to get the approved nomination (see Listing 7.67).
The Squeak version requires similar accessors as shown in Listing 7.68.
Listing 7.67. Java collaboration accessors for the approved nomination of a document.
public INomination getApprovedNomination() throws BusinessRuleException
{
INomination aNomination = null;
CollectionDetector approvedDetector = new CollectionDetector()
{
public boolean detectBlock(Object listElement, Object keyValue)
{
return ((INomination)listElement).isStatusApproved();
}
};
aNomination =(INomination)approvedDetector.detect(this.nominations);
if (aNomination == null)
throw new BusinessRuleException("No approved nomination.");
return aNomination;
}
public boolean isApproved()
{
try { this.getApprovedNomination(); }
catch(BusinessRuleException ex) { return false; }
return true;
}
Listing 7.68. Squeak collaboration accessors for the approved nomination of a document.
Document methodsFor: 'collaboration-accessing'
approvedNomination
↑ self nominations
detect: [:aNomination | aNomination isStatusApproved]
ifNone: [BusinessRuleException signal: 'Document has no approved nomination.']
Document methodsFor: 'testing'
isApproved
↑self nominations anySatisfy: [:aNomination | aNomination isStatusApproved]
Printing and comparison (equals) are implemented using the “Let the Coordinator Direct” principle (78): the document prints and compares its own properties, but not those of its nominations. See the CD for the Java and Squeak code.
For testing, we want to be thorough. We will create at least two document test objects: one with a default security level, and another with a secret security level. Only the secret document test object is shown in Listings 7.69 and 7.70.
Listing 7.69. Java test object for a document with secret security.
public static Document testSecret() throws BusinessRuleException
{
Document aDocument =
new Document("Food and Beverage Industry Surveillance Tips" );
SecurityLevel sLevel = aDocument.getSecurityLevel();
sLevel.setLevelSecret();
return aDocument
}
Listing 7.70. Squeak test object for a document with secret security.
Document class methodsFor: 'examples'
testSecret
"Document testSecret"
| aDocument |
aDocument ← self newWith: 'Food and Beverage Industry Surveillance Tips'.
aDocument securityLevel setLevelSecret.
↑aDocument
A nomination goes through various lifecycle states:
• Pending
• In review
• Approved
• Rejected
A nomination’s lifecycle state transitions are governed by business rules. A nomination cannot:
• Transition to the “pending” state unless in review or already pending
• Transition to the “in review” state unless pending or already in review
• Transition to the “approved” state unless in review or already approved
• Transition to the “rejected” state unless in review or already rejected
These rules are enforced in the property value accessor methods, but we will defer rule checking until the next chapter.
The nomination has the following initialization rules:
• A nomination must have a document to exist.
• A nomination must have a team member to exist.
• A nomination has a nomination date set to the date and time when the nomination was created.
To allow sorting of nominations by date in Java, a nomination must exhibit the Comparable
interface as shown in Listing 7.71.
Listing 7.71. Java conduct business interface for Nomination.
public interface INomination extends Comparable
{
//ACCESSORS -- get properties
public String getComments();
public Date getNominationDate();
//ACCESSORS -- get property values
public boolean isStatusApproved();
public boolean isStatusRejected();
public boolean isStatusPending();
public boolean isStatusInReview();
//ACCESSORS -- set properties
public void setComments(String newComments)
throws BusinessRuleException;
//ACCESSORS -- set property values
public void setStatusPending() throws BusinessRuleException;
public void setStatusInReview() throws BusinessRuleException;
public void setStatusApproved() throws BusinessRuleException;
public void setStatusRejected() throws BusinessRuleException;
//ACCESSORS -- get collaborators
public IDocument getDocument();
public ITeamMember getTeamMember();
// ACCESSORS -- add collaborators
public void addTeamMember(ITeamMember aTeamMember)
throws BusinessRuleException;
public void addDocument(IDocument aDocument)
throws BusinessRuleException;
//DETERMINE MINE
public boolean isBefore(Date aDate);
public boolean isAfter(Date aDate);
}
In Java, a Date
object is used to represent the nomination date property, and an inner class is defined to encapsulate the representation of the status property.25 Both collaborator variables, document and team member, are referenced by their conduct business interfaces (see Listing 7.72).
Listing 7.72. Java class specification for Nomination.
public class Nomination extends Object implements INomination
{
// DEFINE
private NominationStatus status;
private Date nominationDate;
private String comments;
private IDocument document;
private ITeamMember teamMember;
}
A Date
object is also used in Squeak to represent the nomination date property, and an association key-value pair is used to represent the status property (see Listing 7.73).
Listing 7.73. Squeak class definition for Nomination.
Model subclass: #Nomination
instanceVariableNames: 'status nominationDate comments document teamMember '
classVariableNames: ''
poolDictionaries: ''
category: 'Collaboration-Examples'
A nomination requires two objects to exist. The implementation pattern here is similar to creating team members: if the second collaboration fails, then the first one must be dissolved.
In Java, there is no default constructor, and in Squeak, we override the default new
operation to signal an exception. See Listings 7.74 and 7.75.
Listing 7.74. Java constructor for Nomination.
public Nomination( IDocument aDocument, ITeamMember aTeamMember)
throws BusinessRuleException
{
this.status = STATUS_PENDING;
this.nominationDate = new Date();
this.comments = new String();
this.addTeamMember(aTeamMember);
try { this.addDocument(aDocument); }
catch(BusinessRuleException ex)
{
aTeamMember.doRemoveNomination(this);
throw ex;
}
}
Accessors include the regular get accessors, and a set accessor for the comments property. The nomination date cannot change, so it does not have a set accessor. There are also special property value accessors for the status, isStatusApproved
, setStatusApproved
, etc. These follow the conventions used to define the property value accessors for the team member role property.26 One difference is that these set property value accessors check the state transition rules and throw an exception if any rule is violated. Since these tests are implemented again with the rule checking in the next chapter, we omit the listings here.
Listing 7.75. Squeak methods for creating and initializing a nomination.
Nomination class methodsFor: 'instance creation'
new
BusinessRuleException
signal: 'Cannot create nomination without a team member and a document.'
newWith: aTeamMember and: aDocument
| aNomination |
aNomination ←super new initialize.
aNomination addTeamMember: aTeamMember.
[aNomination addDocument: aDocument]
on: BusinessRuleException
do: [:ex|
aTeamMember doRemoveNomination: aNomination.
ex signal].
↑aNomination
Nomination methodsFor: 'initialize-release'
initialize
status ←self statusPending.
nominationDate ←Date today.
comments ←String new.
document ←nil.
teamMember ←nil
For the team member accessor, only add accessors are needed because a nomination cannot exist without its team member and document. Nothing is different here from past collaboration accessors.
Applying the “Let the Coordinator Direct” principle (78), a nomination prints its own properties, and also directs its collaborators to print themselves. This is implemented in Java using the DateFormat
class to format the nomination date for printing (see Listing 7.76).
In Squeak, instead of getting the printString
of the collaborators, a slightly more efficient approach is to ask each collaborator to print on the same stream (see Listing 7.77).
Listing 7.76. Java method for printing a nomination.
public String toString()
{
DateFormat aDateFormat = DateFormat.getDateTimeInstance();
StringBuffer buffy = new StringBuffer(30);
buffy.append("Nomination on: ");
buffy.append(aDateFormat.format(this.nominationDate));
buffy.append("\nStatus: " + this.status);
buffy.append("\n" + this.document);
buffy.append("\n" + this.teamMember);
return buffy.toString();
}
Listing 7.77. Squeak method for printing a nomination.
Nomination methodsFor: 'printing'
printOn: aStream
aStream nextPutAll: 'Nomination:'.
self nominationDate printOn: aStream cr.
aStream cr; nextPutAll: self doGetStatus value.
self teamMember printOn: aStream cr.
self document printOn: aStream cr
Applying the “Let the Coordinator Direct” principle (78), a nomination compares its own properties, plus it directs its collaborators to compare themselves.
In Java, along with the equals
method, a nomination has compareTo
methods to support sorting by Java library classes. The compareTo
method returns –1, 0, or 1 according to whether the receiver is more recent, at the same date and time, or after the other nomination, respectively. This implementation ensures nominations are sorted with the most recent first. The compareTo
methods are shown in Listing 7.78; the equals
method is listed on the CD.
The Squeak code is on the CD.
As the coordinating collaborator, the nomination test object uses the other collaborator test objects. See Listings 7.79 and 7.80.
Listing 7.78. Java methods for comparing Nominations.
public int compareTo( Object anObject)
{
return this.compareTo((Nomination)anObject);
}
public int compareTo( Nomination aNomination)
{
return aNomination.nominationDate.compareTo(this.nominationDate);
}
Listing 7.79. Java Nomination test object.
public static Nomination testNomination() throws BusinessRuleException
{
ITeamMember aTeamMember = TeamMember.testTeamMember();
Nomination aNomination =
new Nomination(Document.testDocument(), aTeamMember);
return aNomination;
}
Listing 7.80. Squeak Nomination test object.
Nomination class methodsFor: 'examples'
testNomination
"Nomination testNomination"
| aNomination |
aNomination ←self newWith: TeamMember testChair and: Document testNormal.
↑aNomination
The TeamMember
class needs the following definition updates to collaborate with nomination objects:
• Add a variable to the TeamMember
class for holding the collection of nominations.
• Add to the team member profile interface a collaboration accessor for the team member’s nominations.
• Add to the team member conduct business interface collaboration accessors to add and remove a nomination.
We will also update the constructor or initialize methods to create a collection object for the nominations when a team member is created.
Accessors must be added to get all nominations, add a single nomination, and remove a particular nomination.
Because TeamMember
is not the coordinator (Principle 78 again), no changes are needed to allow for printing, comparing, or testing.
The next chapter completes the code started in this chapter by showing how to implement the business rules for the properties and collaborations.