Do not fold, spindle, or mutilate.
—Anonymous, instructions on computer punch cards
This chapter shows how to implement business rules in programming code. In a sense, this chapter is what the whole book has been building toward. Previous chapters showed how to choose objects, define business rules, and code objects using the collaboration patterns. This chapter shows how to enforce real-world business rules with object methods organized by the collaboration patterns. Writing code without business rules is like solving physics problems without considering air resistance. This chapter considers air resistance. Sure, it takes a little more effort, but without it, your parachute would not work.
Business rules come from domain experts; usually these are external clients, but they can be team members with specialized in-depth knowledge. Domain experts know the concepts behind the business being modeled, and say things like: “Some teams can only have one chair; other teams can have multiple chairs; and some teams do not require a chair at all.” Business rules define domain-specified limits for property values and when two entities can collaborate. Logic rules come from the programmers or system developers. Programmers say things like: “The document get publication accessor throws an exception if the publication date is null.”
Both types of rules are important. Your project is unlikely to be called successful by the customer if there are glaring logic rule errors. However, your projects will definitely not be successful if there is no way to conduct business using the product. Because of this and because logic rules tend to be easier to determine, we will concentrate on the business rules.
Before implementing a business rule, you must express the rule in object-centric terms and specify how the rule is to be enforced. Rules expressed in object-centric terms fall into one of the following categories:
The rest of this chapter shows how objects enforce these business rules. Property rules are discussed first, because they are simpler. Also, many of the lessons learned for implementing property rules apply to the more complex and interesting collaboration rules.
Property rules specify domain-specific limits on a property’s values. This section describes strategies and techniques for implementing business rule checking of property values.
Property rules work in conjunction with property set accessors. Set accessors permit editing of an object’s properties, while property rules verify that the new values are valid. Checking the property rule inside the property’s set accessor ensures external methods cannot corrupt the property’s value. Within the set accessor, the rule can be embedded directly as lines of code, or extracted out as a separate method to maximize method cohesiveness.
Method cohesiveness essentially means that a method should do only one thing. Setting a property involves doing three things: (1) checking the logical validity of the new value, (2) checking the business rule validity of the new value, and (3) assigning the new value. To maximize the method cohesiveness of the property set accessor, isolate the business rule check and the value assignment into separate methods. This method-cohesive implementation has distinct advantages. Isolating the business rule check allows subclasses to extend and override property rules by writing their own test methods.1 Isolating the value assignment allows bypassing the business rules when necessary.2 Logic rules tend to be the same for generalizations and specializations, so these do not merit isolation into their own method. Organizing code this way keeps you from having to refactor it later on.
Using method cohesiveness to organize the property set accessor code produces the following three methods for every object property that can be externally set:
An extended version of this table, including methods for enumerated type properties, is shown in Appendix B.
The property set accessor generally goes in the conduct business interface, although the set accessor for the team member role property provided a good counter-example.3 On the other hand, the “test set” and “do set” methods do not belong in the conduct business interface, since these methods are internal mechanisms for preserving object integrity and managing property storage. In Java, all these methods are declared public
. Making the “do set” methods public
allows data management classes in other packages to reconstitute the object from persistent storage. Since “test set” methods do not alter the object, making them public
does no harm, and may prove useful when new collaborators are added to the system.
Different categories of properties give rise to different kinds of property rules and rule implementations. Recall the basic property categories from Chapter 5:
The following sections describe property rule implementations for the above property categories.
Descriptive properties are the “free-form” characteristics of an object, such as a person’s name, title, and email, a team’s description, or a document’s title. Time properties have timestamp and date values. Logical rules prevent setting time and descriptive properties to bad values, such as an empty or null string, or bogus times (e.g., 25:63) and dates (e.g., March 33, 2000). Business rules for time and descriptive properties come in two flavors: (1) state transition rules that prevent properties from changing, and (2) value limit rules that constrain property values to specific sets.
This example shows a value limit rule for a descriptive property.
EXAMPLE—A document cannot have a title longer than 255 characters.
Limiting the size of the document title is a business rule to be checked by a separate test method. On the other hand, checking if the title is null or empty is a logical rule and is kept inside the set property accessor. Refer to Listing 8.1.
The test title method goes in the “accessing-rules” message category in Squeak (see Listing 8.2).
Listing 8.1. Java methods for setting, testing, and assigning a descriptive property.
Document.java
// ACCESSORS - set properties
public void setTitle(String newTitle) throws BusinessRuleException
{
if ((newTitle == null) || (newTitle.length() == 0))
throw new BusinessRuleException(
"Document cannot have null or empty title.");
this.testSetTitle(newTitle);
this.doSetTitle(newTitle);
}
// ACCESSORS - property rules
public void testSetTitle(String newTitle) throws BusinessRuleException
{
if (newTitle.length() > 255)
throw new BusinessRuleException(
"Document title cannot exceed 255 characters");
}
// ACCESSORS - property do sets
public void doSetTitle(String newTitle){ this.title = newTitle; }
Listing 8.2. Squeak methods for setting, testing, and assigning a descriptive property.
Document methodsFor: 'accessing'
title: aTitleString
| aTrimmedTitleString |
(aTitleString isNil or:
[(aTrimmedTitleString ← aTitleString withBlanksTrimmed) isEmpty])
ifTrue: [BusinessRuleException signal:
'Document cannot have nil or empty title.'].
self testSetTitle: aTrimmedTitleString.
self doSetTitle: aTrimmedTitleString
Document methodsFor: 'accessing-rules'
testSetTitle: aTitleString
aTitleString size > 255
ifTrue: [BusinessRuleException signal:
'Document title cannot exceed 255 characters.']
Document methodsFor: 'private'
doSetTitle: aTitleString
title ← aTitleString.
self changed: #title
This example shows a state transition rule for a descriptive property.
EXAMPLE—A document cannot set its publication date if it does not have an approved nomination.
Publication date is a write-once, read-only property that gets set when the document is published. It serves double duty by indicating whether the document is published and, if so, when.4 These facts imply that the document does not have a property set accessor for the publication date, since this property cannot be externally edited. However, the document does have the following methods, which are shown in Listing 8.3:
• A test method to check if the publication date can be set
• An assignment method to put the value into the property
• A conduct business service that checks the property rule before assigning the publication date to the current date
Listing 8.3. Java conduct business service and state transition rule test method.
Document.java
// CONDUCT BUSINESS
public void publish() throws BusinessRuleException
{
this.testSetPublicationDate();
this.doSetPublicationDate(new Date());
}
// ACCESSORS - property rules
public void testSetPublicationDate() throws BusinessRuleException
{
if (this.isPublished())
throw new BusinessRuleException("Document already published.");
if (!this.isApproved())
throw new BusinessRuleException(
"Document not approved for publication.");
}
// ACCESSORS - property do sets
public void doSetPublicationDate(Date newDate)
{
this.publicationDate = newDate;
}
The Java “is published” and “is approved” services were shown in the previous chapter.5
Although there is no set accessor, the Squeak code includes a private method to consolidate in one place the property test, assignment, and change notification. As with the Java code, the property test calls a method that searches the nominations for an approved one and raises an exception if one is not found (see Listing 8.4). See the CD for all the code.
Listing 8.4. Squeak conduct business service and state transition rule test method.
Document methodsFor: 'domain services'
publish
self testSetPublicationDate.
self doSetPublicationDate: Date today
Document methodsFor: 'accessing-rules'
testSetPublicationDate
self isPublished ifTrue: [BusinessRuleException signal:
'Document already published.'].
self approvedNomination
ifNil: [BusinessRuleException signal: 'Document not approved for publication.']
Document methodsFor: 'private'
doSetPublicationDate: aPublicationDate
publicationDate ← aPublicationDate.
self changed: #publicationDate
State, role, and type properties have fixed sets of legal values that are supplied by domain experts. Such properties with fixed sets of values are also known as enumerated types, and require special property accessors to encapsulate their implementation. In the previous chapter, these accessors were called property value accessors.
EXAMPLE—A nomination with a status property has get accessors that ask if the status has a particular value:
isStatusPending
,isStatusInReview
,isStatusApproved
,isStatusRejected
. It also has set accessors that request the status to take on a particular value:setStatusPending
,setStatusInReview
,setStatusApproved
,setStatusRejected
.
For some enumerated type properties, the current property value can prohibit the property from taking on other values. This happens frequently with lifecycle state properties; the current state value restricts the next acceptable state value.
EXAMPLE—A nomination with a lifecycle state of “rejected” cannot transition to a lifecycle state of “pending,” “in review,” or “approved.”
EXAMPLE—A nomination has a status property with the following possible values:
• 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
Each lifecycle state transition rule is implemented by a distinct method. This provides more flexibility for specialization classes that may want to selectively alter the transition rules.
The Java accessors and test methods for the “rejected” value of the status property are shown in Listing 8.5. The other accessors and test methods are listed on the CD.
In Squeak, the test methods for the state transition rules are placed in the “accessing-rules” message category as shown in Listing 8.6.
Listing 8.5. Java property value accessor and business rule test method.
Nomination.java
// ACCESSORS -- set property values
public void setStatusRejected() throws BusinessRuleException
{
this.testSetStatusRejected();
this.doSetStatus(STATUS_REJECTED);
}
// ACCESSORS - property value rules
public void testSetStatusRejected() throws BusinessRuleException
{
if (this.isStatusInReview() || this.isStatusRejected()) return;
else throw new BusinessRuleException(
"Nomination cannot be rejected. Not under review");
}
// ACCESSORS - property do sets
public void doSetStatus(NominationStatus aStatus)
{
this.status = aStatus;
}
Listing 8.6. Squeak property value accessor and business rule test method.
Nomination methodsFor: 'accessing'
setStatusRejected
self testSetStatusRejected.
self doSetStatus: self statusRejected
Nomination methodsFor: 'accessing-rules'
testSetStatusRejected
(self isStatusInReview or: [self isStatusRejected])
ifFalse: [BusinessRuleException signal:
'Nomination cannot be rejected. Not under review.']
Cross-property validation occurs when a property rule includes another property from the same or a different object.
EXAMPLE—A team member cannot become a chair (i.e., set its role property to “chair”) if the team member belongs to a team whose format prevents it from having another chair.
When two objects are involved in a cross-property validation rule, each object requires its own test service. The first test is in the object trying to change its property value, and the second test is in the object capable of vetoing the property value. Often, this veto property test doubles as a property collaboration rule. For example, a team member cannot change its role property value to “chair” if its team cannot support another chair; however, it is also true that a team member cannot be added to the team if the team cannot support another chair. Isolating this veto test into its own method allows its use for cross-property validation and for collaboration rule checking. In the team member example, which is illustrated in Figure 8.1, the veto test is called from the test set method for the role property.
The team object definition includes a method for counting the number of chair team members. In Java, this method uses an anonymous inner class that tests elements from a list, selects those returning true, and returns them in another list. The test used here is whether a team member is a chair (see Listing 8.7). The code for the CollectionSelector
class is on the CD.
Listing 8.7. Java set property accessor and cross-validation property test methods.
TeamMember.java
// ACCESSORS -- set property value
public void doSetRoleChair() throws BusinessRuleException
{
this.testSetRoleChair();
this.doSet(ROLE_CHAIR);
}
// ACCESSORS - property value rules
public void testSetRoleChair() throws BusinessRuleException
{
if (this.isRoleChair()) return;
if (this.team != null)
this.team.testCanBeChair(this);
}
Team.java
// ACCESSORS - collaboration rules
public void testCanBeChair(ITeamMember aTeamMember)
throws BusinessRuleException
{
if (this.isFormatMultipleChair()) return;
if (this.isFormatNoChair())
throw new BusinessRuleException(
"Cannot add chair team member to no chairs team.");
if (this.getChairs().size() > 0)
throw new BusinessRuleException(
"Cannot add another chair team member to single chair team.");
}
// DETERMINE MINE
public List getChairs()
{
CollectionSelector chairSelector = new CollectionSelector()
{
public boolean selectBlock(Object listElement, Object keyValue)
{
return ((ITeamMember)listElement).isRoleChair();
}
};
return (List)chairSelector.select(this.teamMembers);
}
In Squeak, the two test methods belong in different message categories. Because it is validating a property, the team member test goes into the “accessing-rules” message category; and since it is validating for a collaborator, the team test method belongs in the “collaboration-rules” message category (see Listing 8.8).
Listing 8.8. Squeak set property accessor and cross-validation property test methods.
TeamMember methodsFor: 'private'
setRoleChair
self testSetRoleChair.
self doSetRole: self roleChair
TeamMember methodsFor: 'accessing-rules'
testSetRoleChair
self isRoleChair ifTrue: [↑ self].
self team ifNotNil: [self team testCanBeChair: self]
Team methodsFor: 'collaboration-rules'
testCanBeChair: aTeamMember
self isFormatMultipleChair ifTrue: [↑ self].
self isFormatNoChair ifTrue: [BusinessRuleException signal:
'Tried to add chair team member to no chairs team.'].
self chairs size = 1 ifTrue: [BusinessRuleException signal:
'Tried to add another chair team member to a single chair team.']
Team methodsFor: 'collaboration-accessing'
chairs
↑self teamMembers select: [:aTeamMember | aTeamMember isRoleChair].
Collaboration rules govern whether two objects can establish or dissolve a collaboration with each other. Because two objects are involved, the collaboration rule checking must be coordinated so that: (1) both objects can do rule checking, and (2) rule checking happens when either object establishes or dissolves the collaboration.
All the collaboration rules cannot be consolidated in one object. While putting all rules in one place may make the implementation easier in the short-term, it destroys pluggability, extensibility, and scalability.
Pluggability is the notion that an object in a collaboration can specify communication requirements between itself and its collaborator such that any other object meeting those requirements can collaborate with it. It also implies that different kinds of objects actually do meet the requirements.
Pluggability is lost if one collaborator takes on the responsibility of checking all the collaboration rules. This is because collaboration rules involve checking implementation level details—properties, state, and even other collaborators. By “knowing” the other’s collaboration rules, the “manager” object doing all the work is assuming a particular implementation for its “dumb” collaborator. This constrains the manager object to collaborate only with this particular object implementation. Pluggability is discussed more extensively later in this chapter.
Extensibility is the notion that any object can add new properties, services, and collaborations without impacting other objects. Again, if one manager object is doing the work for both objects, then extensions to the implementation of the dumb object could change the collaboration rules, which are being handled by the manager object.
Scalability is the notion that the existing set of objects can easily accommodate and work with new types of objects. Once pluggability is lost, scalability becomes nearly impossible since each existing object is constrained to work with only one particular implementation for each of its collaborators.
EXAMPLE—Consider these different implementations of the Team – TeamMember collaboration from the previous chapter.
Implementation 1: Team checks both its own and the team member’s collaboration rules.
• If an expiration date is added to a team member to accommodate short-term contractors, then team collaboration rule checking is impacted because the team must now check whether a team member has expired before approving it for collaboration.
Implementation 2: Team and team member both check their own collaboration rules.
• In this scenario, adding an expiration date to a team member does not impact the team. Instead, the collaboration rule checking for the team member is expanded to check whether it has expired before agreeing to collaborate with a team.
Collaboration rules do not care how objects were brought together or pulled apart. Sometimes, conduct business services specify precisely how the object interactions occur, and the order in which the collaborations are created. Other times, user interfaces allow users to selectively associate or disassociate objects. Collaboration rules should be implemented without any assumptions about how or by whom they will be called.
EXAMPLE—Consider the Team – TeamMember collaboration. Regardless of whether a team member is added to a team through the “add team member” accessor or a team is added to a team member through the “add team” accessor, all the collaboration rules need to be checked.
Establishing a collaboration between two objects is a scripted process during which each object is asked to check its own collaboration rules, and if these pass, then each object is asked to do whatever it takes to remember the other collaborator—set a collaboration variable or add the object reference to a collaboration collection.
As with the property accessors, we want our collaboration accessors to be highly cohesive. When implementing collaboration accessors, we isolate the rule check and collaboration assignment into separate methods. Isolating the rule check allows subclasses to extend collaboration rules, and isolating the assignment allows bypassing the business rules when necessary.7
An extended version of this table, including methods for dissolving collaborations, is shown in Appendix B (“Methods for Enforcing Collaboration Rules”).
Unlike the property rule methods, all the methods for establishing a collaboration belong in the conduct business interface. The difference here is that two objects are involved in establishing a collaboration, and the process requires communication and cooperation between them. Since collaborators refer to each other through their conduct business interfaces, any methods used to communicate while establishing a collaboration must go into this interface. Putting the “do add” methods in the conduct business interface means they are also declared as public
methods, but this is okay if your programmers are disciplined. Besides, these methods must be public
to allow the object to be reconstructed from persistent storage through a data manager. Declaring these methods anything other than public
constrains the data manager to reside in the same package as the business object. 8
Establishing a collaboration involves two collaboration add accessors, one for each collaborator. To satisfy the dual and commutative rule checking principles, both sets of collaboration rules must be checked by both collaboration add accessors.
EXAMPLE—Including collaboration rule-checking while establishing a Team – TeamMember collaboration requires the following steps in the collaboration add accessors:
Team >> add team member (a team member)
I test that “a team member” is logically valid (e.g., it is not null).
I test that I can add “a team member.”
I ask “a team member” to test whether he can add me as a team.
I do add “a team member.”
I ask “a team member” to do add me.
TeamMember >> add team (a team)
I test that “a team” is logically valid (e.g., it is not null).
I test that I can add “a team.”
I ask “a team” to test whether he can add me as a team member.
I do add “a team.”.
I ask “a team” to do add me.
While the commutative rule checking principle requires both collaboration add accessors to perform rule checking and assignment, code duplication is reduced by allowing one add accessor to delegate the responsibility of establishing the collaboration to the other add accessor.
EXAMPLE—The streamlined version of the Team – TeamMember collaboration accessors has the team accessor delegating to the team member accessor:
Team >> add team (a team member)
I test that “a team member” is logically valid (e.g., it is not null).
I ask “a team member” to add team (me).
TeamMember >> add team (a team)
I test that “a team” is logically valid (e.g., it is not null).
I test that I can add “a team.”
I ask “a team” to test whether he can add me as a team member.
I do add “a team.”
I ask “a team” to do add me.
Follow the usual rules for assigning work: “Most Specific Carries the Load” principle (74), and “Let the Coordinator Direct” principle (78). Applying these workload principles to the three fundamental patterns yields the following assignments:
• generic delegates to specific
• whole delegates to a part
• specific delegates to a transaction
The process for dissolving a collaboration between two objects is very similar to the process for establishing a collaboration. Again, the rule checking and collaboration assignment are isolated into separate methods, all the methods go into the conduct business interface, and the collaboration remove accessors are streamlined according to the “Choosing Your Director” principle (89).
An extended version of this table, including methods for establishing collaborations, is shown in Appendix B (“Methods for Enforcing Collaboration Rules”).
The previous chapter introduced the “DIAPER” process for implementing object definitions. This chapter expands the DIAPER process to include collaboration rule checking. Steps in the DIAPER process that are affected include the “Define” and “Accessing” steps.
In the “Define” step, collaboration rule checking requires including the “test add,” “test remove,” “do add,” and “do remove” methods in the conduct business interface for the object’s collaborations. Omitting the test methods is tempting when the object lacks collaboration rules; however, unless optimization is the preeminent goal, it is better to include the test methods and guarantee extensibility.
In the “Accessing” step, implement methods for testing collaboration rules and assigning the collaborator reference. Also, use the “Choosing Your Director” principle (89) to decide which collaborator directs and which delegates. Implement the directing collaboration accessor so that both collaborators check their collaboration rules and perform their assignments if the rules succeed.
This section shows how to implement collaboration rules in Java and Squeak using the Person – TeamMember and Team – TeamMember collaborations coded in the previous chapter.
As an actor, a person has no collaboration rules for its roles, which include team member.9 The team member plays as a specific in a generic – specific collaboration with its person, and plays as a part in a whole – part collaboration with its team. Because it acts as these pattern players, the team member carries most of the load for checking the collaboration rules.
EXPLANATION
Rule #1 is a multiplicity rule. A team member can collaborate with only one person, and cannot exchange the person for another.
Rule #2 is a property rule. A person lacking a valid email property cannot collaborate.
Rule #3 is a conflict rule. A team member cannot collaborate with a person if the person conflicts with the team member’s present team collaborator.
EXPLANATION
Rule #1 is a state rule. A team member on a team is in an improper state for dissolving its person collaboration.
EXPLANATION
Rule #1 is a multiplicity rule.
Rule #2 is a conflict rule. This same conflict rule applies when establishing a person collaborator.
Rule #1 is a state rule. A team member with nominations is in an improper state for dissolving its team collaboration.
EXPLANATION
Rule #1 is a property rule. This rule uses properties from both the team and team member. Because the team has the “standard” used for comparison, it owns the rule.10 This rule also applies for cross-property validation and was used in a previous example (see p. 236).
The plan is to update the person, team member, and team object definitions to include the above collaboration rules. The following changes to the object definitions are applied where needed:
• Expand the conduct business interfaces
• Implement the “test” and “do” methods
• Revise the collaboration accessors
Update the person, team member, and team conduct business interfaces to include the “do” and “test” methods for collaborations (see Figure 8.2). Include the method signatures shown in Listing 8.9 in the person and team member conduct business interfaces.
This section discusses the team member and team methods for checking collaboration rules (see Listing 8.10). Also included is a team member method to check for conflicts between the team member’s team and person collaborators. This method does not go into the conduct business interface because it is internal to the team member. Conflict rules will be discussed more thoroughly later in this chapter. The team collaboration rule for adding a team member collaborator makes use of the method defined earlier during the cross-property validation example. This method confirms that a team member who is a chair can be added to the team (see Listing 8.7).
Listing 8.9. Java conduct business interfaces updated with collaboration rule test methods.
IPerson.java
public interface IPerson extends IPersonProfile
{
<<snip>>
// ACCESSORS - collaboration do adds
public void doAddTeamMember(ITeamMember aTeamMember);
public void doRemoveTeamMember(ITeamMember aTeamMember);
}
ITeamMember.java
public interface ITeamMember extends ITeamMemberProfile
{
<<snip>>
// ACCESSORS - collaboration rules
public void testAddTeam(ITeam aTeam) throws BusinessRuleException;
public void testRemoveTeam(ITeam aTeam) throws BusinessRuleException;
public void testAddPerson(IPerson aPerson) throws BusinessRuleException;
public void testRemovePerson(IPerson aPerson) throws BusinessRuleException;
// ACCESSORS - collaboration do adds
public void doAddTeam(ITeam aTeam);
public void doAddPerson(IPerson aPerson);
public void doRemoveTeam(ITeam aTeam);
public void doRemovePerson(IPerson aPerson);
}
ITeam.java
public interface ITeam
{
<<snip>>
// ACCESSORS - collaboration do adds
public void doAddTeamMember(ITeamMember aTeamMember);
public void doRemoveTeamMember(ITeamMember aTeamMember);
// ACCESSORS - collaboration rules
public void testAddTeamMember(ITeamMember aTeamMember
throws BusinessRuleException;
public void testCanBeChair(ITeamMember aTeamMember)
throws BusinessRuleException;
}
In Squeak, the collaboration test methods go in their own message category, “collaboration rules.” See Listing 8.11.
These methods assign or remove the collaborator reference without rule checking. The Java code is shown in Listing 8.12.
In Squeak, these methods are placed in the private
message category to emphasize that only the collaboration accessors should use them (see Listing 8.13). These methods also handle change notification to dependents.
Both person and team delegate to team member to direct the establishment or dissolution of the collaboration. Only the add accessors are shown in Listings 8.14 and 8.15 because the remove accessors so closely resemble them.
Listing 8.10. Java “test” methods for checking collaboration rules.
TeamMember.java
public void testAddPerson(IPerson aPerson) throws BusinessRuleException
{
if (this.person != null)
throw new BusinessRuleException("Team member has a person.");
if (!aPerson.hasValidEmail())
throw new BusinessRuleException("Person has invalid email.");
if (this.team != null)
this.testAddPersonTeamConflict(aPerson, this.team);
}
public void testRemovePerson(IPerson aPerson)
throws BusinessRuleException
{
if (aPerson == null)
throw new BusinessRuleException("Tried to remove null person.");
if (!aPerson.equals(this.person))
throw new BusinessRuleException("Tried to remove wrong person.");
if (this.team != null)
throw new BusinessRuleException("Team member on team cannot remove person.");
}
public void testAddPersonTeamConflict(IPerson aPerson, ITeam aTeam)
throws BusinessRuleException
{
ITeamMember aTeamMember = aTeam.getTeamMember(aPerson);
if (aTeamMember != null)
throw new BusinessRuleException("Person already on team.");
}
Team.java
public void testAddTeamMember(ITeamMember aTeamMember)
throws BusinessRuleException
{
if (aTeamMember.isRoleChair())
this.testCanBeChair(aTeamMember);
}
Listing 8.11. Squeak “test” methods for checking collaboration rules.
TeamMember methodsFor: 'collaboration-rules'
testAddPerson: aPerson
self person
ifNotNil: [BusinessRuleException signal: 'Team member already has a person.'].
aPerson hasValidEmail
ifFalse: [BusinessRuleException signal: 'Tried to add person with invalid email.'].
self team ifNotNil: [self testAddConflictBetween: aPerson and: self team]
testRemovePerson: aPerson
self person = aPerson
ifFalse: [BusinessRuleException signal: 'Tried to remove different person.'].
self team ifNotNil: [BusinessRuleException signal:
'Team member on team cannot remove person.'].
Listing 8.12. Java low-level methods for connecting and disconnecting collaborators.
Person.java
public void doAddTeamMember(ITeamMember aTeamMember)
{
this.teamMembers.add(aTeamMember);
}
public void doRemoveTeamMember(ITeamMember aTeamMember)
{
this.teamMembers.remove(aTeamMember);
}
TeamMember.java
public void doAddPerson(IPerson aPerson)
{
this.person = aPerson;
}
public void doRemovePerson(IPerson aPerson)
{
this.person = null;
}
Team.java
public void doAddTeamMember(ITeamMember aTeamMember)
{
this.teamMembers.add(aTeamMember);
}
public void doRemoveTeamMember(ITeamMember aTeamMember)
{
this.teamMembers.remove(aTeamMember);
}
Listing 8.13. Squeak low-level methods for connecting and disconnecting collaborators.
Person methodsFor: 'private'
doAddTeamMember: aTeamMember
self teamMembers add: aTeamMember.
self changed: #teamMembers
doRemoveTeamMember: aTeamMember
self teamMembers remove: aTeamMember ifAbsent: [ ].
self changed: #teamMembers
TeamMember methodsFor: 'private'
doAddPerson: aPerson
person ← aPerson.
self changed: #person
doRemovePerson: aPerson
person ← nil.
self changed: #person
Listing 8.14. Java collaboration add accessors with collaboration rule testing.
Person.java
public void addTeamMember(ITeamMember aTeamMember)
throws BusinessRuleException
{
if (aTeamMember == null)
throw new BusinessRuleException("Tried to add null team member.");
aTeamMember.addPerson(this);
}
TeamMember.java
public void addPerson(IPerson aPerson) throws BusinessRuleException
{
if (aPerson == null)
throw new BusinessRuleException("Tried to add null person");
this.testAddPerson(aPerson);
this.doAddPerson(aPerson);
aPerson.doAddTeamMember(this);
}
public void addTeam(ITeam aTeam) throws BusinessRuleException
{
if (aTeam == null)
throw new BusinessRuleException("Tried to add null team.");
this.testAddTeam(aTeam);
aTeam.testAddTeamMember(this);
this.doAddTeam(aTeam);
aTeam.doAddTeamMember(this);
} Team.java
public void addTeamMember(ITeamMember aTeamMember)
throws BusinessRuleException
{
if (aTeamMember == null)
throw new BusinessRuleException("Tried to add null team member");
aTeamMember.addTeam(this);
}
Listing 8.15. Squeak collaboration accessors with collaboration rule testing.
Person methodsFor: 'collaboration-accessing'
addTeamMember: aTeamMember
aTeamMember
ifNil: [BusinessRuleException signal: 'Tried to add nil team member.'].
aTeamMember addPerson: self
TeamMember methodsFor: 'collaboration-accessing'
addPerson: aPerson
aPerson
ifNil: [BusinessRuleException signal: 'Tried to add nil person.'].
self testAddPerson: aPerson.
aPerson testAddTeamMember: self.
self doAddPerson: aPerson.
aPerson doAddTeamMember: self
Conflict rules come into play when business rules define restrictions between objects that collaborate through an intermediary object. In essence, conflict rules are collaboration rules between indirect collaborators, that is, in-laws. Conflict rules appear in all three of the fundamental patterns:
Conflict rules are implemented by following the same principles used for other collaboration rules: dual rule checking, commutative rule checking, and streamlined to a director. Conflict rules differ from collaboration rules in range and timing.
Conflict rules range over more objects than collaboration rules, which only involve two objects, a potential collaborator and a collaboration director. With conflict rules, a potential collaborator tests one or more existing collaborators of the collaboration director, and the existing collaborators test the potential one. Some conflict rules even depend on characteristics of the collaboration director. For maximum flexibility, methods for testing conflict rules should receive as input the directing collaborator and all its other collaborators.
Unlike collaboration rules, which must run before a collaboration is established, conflict rules may be deferred until the other collaborators are available for checking. Deferring conflict rules may be necessary, but it also introduces tricky considerations, such as determining which collaborators to roll back if the rules fail, and deciding how to gracefully kick out things that were accepted earlier. Generic – specific collaborations typically check conflicts immediately, when a potential specific is looking to collaborate with the generic. Rejection of the potential specific does not require rollback of the existing specifics, but is fatal to the rejected specific. Whole – part collaborations also tend to check conflicts immediately, and do not require rollback of other parts when a part fails. The exceptions may be certain assemblies that cannot exist if all parts are not present. As most parts can exist without a whole, rejection of a part is often not fatal to the part.
Deferred conflict rule checking occurs most commonly with transactions. A transaction cannot obtain a valid state until all its collaborators are present, and failure of the conflict rules is fatal to the transaction and results in the rollback of all its collaborators. On the other hand, composite transactions usually do not know when all their collaborators are available as they can have an unlimited number of line items with collaborating specific items. However, once a composite transaction knows its role and place, it can check conflict rules between these and its existing specific items or any specific items seeking to collaborate.
EXAMPLE—A video rental transaction involving one or more videotapes rejects a videotape with a restricted rating if the customer is underage. If the customer is required to show his credentials at the beginning of the rental transaction then conflict rules can be run as soon a videotape attempts to collaborate with the rental, and a restricted videotape can be rejected immediately. Delaying the verification of customer information to the end means that a conflict with a restricted videotape is only detected after it is part of the rental transaction, and the restricted videotape must be rolled back out of the rental.
To satisfy the dual and commutative rule checking principles, conflict test methods must run whenever the collaboration director adds or removes a potentially conflicting collaborator.
EXAMPLE—When a transaction (voter registration) adds a role collaborator (voter), it asks the role and its place collaborator (precinct) to check for conflicts (“test add voter registration conflict”). The same conflict checks are made when the voter is added first. In general, when a transaction adds a place collaborator, it checks it against its role collaborator and specific item collaborators, if they exist. The scenario in Figure 8.3 involves a transaction with only role and place collaborators, but the logic is the same when a specific item is involved. Regardless of the order in which the place and role are added to the transaction, the test conflict rules run once.
Conflict rules go into separate methods and are invoked by the appropriate “test add” or “test remove” method of the collaboration director.
Because they involve communication among collaborators, methods for checking conflict rules are public
and belong in the conduct business interface.
Conflict rules contribute to the test deciding whether a collaboration should be established or dissolved, and are invoked from the directing collaborator’s test method.
In the “Define” step, for each collaborator with conflict rules, include in its conduct business interface the relevant conflict test methods.
In the “Accessing” step, for objects with conflict rules, implement the appropriate test conflict methods for establishing and dissolving collaborations. Also, revise the test collaboration methods of the directing collaborator to include conflict rule checking with its existing and potential collaborators. If conflict rule checking is deferred, then each test method tests whether the potential collaborator is the last to be added, and if so, checks the conflict rules of its existing collaborators and the potential one.
This section shows how to implement conflict collaboration rules in Java and Squeak using the TeamMember – Nomination – Document collaboration coded in the previous chapter.
In this example, a document nominates itself with a team member. Applying the “do it myself” principle to this business process transforms it into a conduct business service of the document that creates a nomination with the document and participating team member (see Figure 8.4).
In this example, there are no remove collaboration rules because this document management system requires a historical record of all nominations, even failed ones. Also for this reason, there are no methods to remove nominations from a team member or document.
• Five times for a team member whose role is member or admin
Rule #1 is a property rule. Team members know what privileges they need to nominate. Rule #2 is an example of a dynamic multiplicity rule; its limits are not fixed but dependent on the state of the team member.
EXPLANATION
Rule #1 is a property rule. Rule #2 is a state rule. A document with a published status cannot be nominated. Rule #3 is also a state rule.
EXPLANATION
Rule #1 is a conflict rule that evaluates a team member according to his security level property. The document owns this rule because it has the standard of comparison.
In this example, the collaboration accessors are critical to a conduct business service because they contain the necessary collaboration and conflict rules. To implement the conduct business service, do the following:
• Expand conduct business interfaces
• Implement “test” and “do” methods for collaborations
• Revise collaboration accessors
• Implement conduct business services
As the transaction and the coordinator between the two specifics, the nomination is best suited to direct the testing for all the collaboration and conflict rules. In the implementation, the nomination will call the “test” and “test conflict” methods from its own “test” method (see Figure 8.6).
Listing 8.16 uses the minimalist approach, only adding test methods when the domain has an actual rule. Only the additional methods required are shown.
Update the three interfaces by adding the relevant “test” and “do” methods (see Figure 8.5).
Listing 8.16. Java updates to conduct business interfaces.
ITeamMember.java
public interface ITeamMember extends ITeamMemberProfile
{
<<snip>>
public void testAddNomination(INomination aNomination)
throws BusinessRuleException;
public void doAddNomination(INomination aNomination);
}
INomination.java
public interface INomination extends Comparable
{
<<snip>>
public void testAddDocument(IDocument aDocument)
throws BusinessRuleException;
public void testAddTeamMember(ITeamMember aTeamMember)
throws BusinessRuleException;
public void doAddDocument(IDocument aDocument);
public void doAddTeamMember(ITeamMember aTeamMember);
}
IDocument.java
public interface IDocument
{
<<snip>>
// ACCESSORS -- collaboration rules
public void testAddNomination(INomination aNomination)
throws BusinessRuleException;
public void testAddNominationConflict(
INomination aNomination, ITeamMember aTeamMember)
throws BusinessRuleException;
// ACCESSORS - do add
public void doAddNomination(INomination aNomination) ;
public void doRemoveNomination(INomination aNomination) ;
}
Listings 8.17 and 8.18 show the document method for checking conflicts with a team member within a nomination. The conflict rule compares the security levels of the team member and documents. In both the Java and Squeak implementations, the security level is an object that implements a “greater than” method for comparing levels. See the CD for code specifics.
Listing 8.17. Java conflict collaboration rule method.
Document.java
public void testAddNominationConflict(
INomination aNomination, ITeamMember aTeamMember)
throws BusinessRuleException
{
if (this.securityLevel.greaterThan(aTeamMember.getSecurityLevel()))
throw new BusinessRuleException(
"Security violation. Team member has improper security.");
}
Listing 8.18. Squeak conflict collaboration rule method.
Document methodsFor: 'collaboration-rules'
testAddNominationConflict: aNomination with: aTeamMember
(self securityLevel > aTeamMember securityLevel)
ifTrue: [BusinessRuleException signal:
'Security violation. Team Member has improper security.']
As the director of the conflict rules, the nomination is responsible for asking the document if it conflicts with the team member. To ensure commutativity, this check is included in both the “test add document” and “test add team member” methods, but it runs only when both document and team member collaborators are present. These “test” methods also enforce the nomination’s multiplicity rules of having only one team member and one document. See Listings 8.19 and 8.20.
Listing 8.19. Java collaboration rule “test” method using collaboration conflict “test” method.
Nomination.java
public void testAddTeamMember(ITeamMember aTeamMember)
throws BusinessRuleException
{
if (this.teamMember != null)
throw new BusinessRuleException("Team member already exists.");
if (this.document != null)
this.document.testAddNominationConflict(this, aTeamMember);
}
public void testAddDocument(IDocument aDocument)
throws BusinessRuleException
{
if (this.document != null)
throw new BusinessRuleException("Document already exists.");
if (this.teamMember != null)
aDocument.testAddNominationConflict(this, this.teamMember);
}
The team member’s “test” methods enforce the restrictions that a team member cannot nominate if its lacks the nominate privilege or if it has exceeded its maximum number of allowed nominations.
In Java, static variables define a length of time and the maximum number of documents allowed during that time for a chair and a non-chair team member.
Listing 8.20. Squeak collaboration rule “test” method using collaboration conflict “test” method.
Nomination methodsFor: 'collaboration-rules'
testAddDocument: aDocument
self document ifNotNil: [BusinessRuleException signal: 'Document already exists.'].
self teamMember ifNotNil: [aDocument testAddNominationConflict: self
with: self teamMember]
testAddTeamMember: aTeamMember
self teamMember
ifNotNil: [BusinessRuleException signal: 'Team member already exists.'].
self document
ifNotNil: [self document testAddNominationConflict: self with: aTeamMember]
The service that counts nominations within a given period of days uses a CollectionSelector
object to select a sub-list of nominations occurring within the date range. The size of this sub-list determines the number of nominations within the date range. See Listing 8.21.
Listing 8.21. Java TeamMember “test” methods for adding a nomination.
TeamMember.java
public void testAddNomination(INomination aNomination)
throws BusinessRuleException
{
if (!this.hasNominatePrivilege())
throw new BusinessRuleException(
"Security violation. Team member cannot nominate.");
if (this.countNominationsPerPeriod() >= this.maxNominationsAllowed())
throw new BusinessRuleException(
"Team member cannot nominate. Too many nominations.");
}
public int maxNominationsAllowed()
{
if (this.isRoleChair()) return MAX_CHAIR_DOCUMENTS;
else return MAX_DOCUMENTS;
}
public int countNominationsPerPeriod()
{
return this.countNominationsPerDays(NOMINATIONS_TIME_PERIOD);
}
public int countNominationsPerDays(int daysInPeriod)
{
if (this.nominations.isEmpty()) return 0;
Calendar myCalendar = Calendar.getInstance();
myCalendar.add(Calendar.DATE, -1 * daysInPeriod);
Date endDate = myCalendar.getTime();
CollectionSelector selectList = new CollectionSelector()
{
public boolean selectBlock(Object listElement, Object keyValue)
{
return ((INomination)listElement).isAfter((Date)keyValue);
}
};
Collection nomsInRange = selectList.select(this.nominations, endDate);
return nomsInRange.size();
}
In the Squeak version, instance methods in the “constants” message category return the values used for defining the length of the nomination period and the maximum number of documents allowed during that nomination period for a chair and a non-chair team member (see Listing 8.22). These methods are shown on the book CD.
Listing 8.22. Squeak TeamMember “test” methods for adding a nomination.
TeamMember methodsFor: 'collaboration-rules'
testAddNomination: aNomination
self hasNominatePrivilege
ifFalse: [BusinessRuleException signal: 'Team member cannot nominate.'].
self countNominationsPerPeriod >= self maxNominationsAllowed
ifTrue: [BusinessRuleException signal:
'Team member cannot nominate. Too many nominations.']
TeamMember methodsFor: 'accessing'
maxNominationsAllowed
self isRoleChair
ifTrue: [↑self maxChairDocuments]
ifFalse: [↑self maxDocuments]
TeamMember methodsFor: 'domain services'
countNominationsPerPeriod
↑self countNominationsPerDays: self nominationsTimePeriod
countNominationsPerDays: anInteger
| endDate |
self nominations isEmpty ifTrue: [↑ 0].
endDate ←Date today subtractDays: anInteger.
↑ (self nominations select:
[:aNomination | aNomination nominationDate > endDate]) size
The document “test” methods enforce the restrictions that a document cannot be nominated if it lacks a title, is already published, or already has an unresolved nomination.
Thanks to the sorted nature of the nomination collection, only the most recent nomination is checked to see if it is unresolved in Java (see Listing 8.23).
In Squeak, all nominations are searched to see if any are unresolved as shown in Listing 8.24.
Listing 8.23. Java document test methods for adding a nomination.
Document.java
public void testAddNomination(INomination aNomination)
throws BusinessRuleException
{
if (this.isPublished())
throw new BusinessRuleException("Document already published.");
INomination lastNomination = null;
try { lastNomination = this.getLatestNomination(); }
catch(BusinessRuleException ex){ return; }
if (lastNomination.isStatusPending() ||
lastNomination.isStatusInReview())
throw new BusinessRuleException("Document has unresolved nomination.");
}
public INomination getLatestNomination () throws BusinessRuleException
{
if (this.nominations.isEmpty())
throw new BusinessRuleException("Document has no nominations");
return (INomination)(this.nominations.first());
}
Listing 8.24. Squeak document “test” methods for adding a nomination.
Document methodsFor: 'collaboration-rules'
testAddNomination: aNomination
self isPublished
ifTrue: [BusinessRuleException signal: 'Document already published.'].
self hasUnresolvedNominations
ifTrue: [BusinessRuleException signal: 'Document has unresolved nomination.']
The nomination collaboration accessors direct the rule checking as shown in Listings 8.25 and 8.26 (see also Figure 8.6).
Listing 8.25. Java nomination methods for adding a team member and document.
Nomination.java
public void addTeamMember(ITeamMember aTeamMember) throws
BusinessRuleException
{
if (aTeamMember == null)
throw new BusinessRuleException("Tried to add null team member");
this.testAddTeamMember(aTeamMember);
aTeamMember.testAddNomination(this);
this.doAddTeamMember(aTeamMember);
aTeamMember.doAddNomination(this);
}
public void addDocument(IDocument aDocument) throws
BusinessRuleException
{
if (aDocument == null)
throw new BusinessRuleException("Tried to add null document");
this.testAddDocument(aDocument);
aDocument.testAddNomination(this);
this.doAddDocument(aDocument);
aDocument.doAddNomination(this);
}
Listing 8.26. Squeak nomination methods for adding a team member and document.
Nomination methodsFor: 'collaboration-accessing'
addDocument: aDocument
aDocument
ifNil: [BusinessRuleException signal: 'Tried to add nil document.'].
self testAddDocument: aDocument.
aDocument testAddNomination: self.
self doAddDocument: aDocument.
aDocument doAddNomination: self
addTeamMember: aTeamMember
aTeamMember
ifNil: [BusinessRuleException signal: 'Tried to add nil team member.'].
self testAddTeamMember: aTeamMember.
aTeamMember testAddNomination: self.
self doAddTeamMember: aTeamMember.
aTeamMember doAddNomination: self
The goal here is a pluggable object model for a particular domain. Clients do not usually want a reusable class library for handling systems beyond the scope of their business domain, but they do want a model that scales and grows along with their business. Keeping an eye on the domain and business constraints gives focus to the refactoring efforts and defines the limits of reuse.
EXAMPLE—In the TeamMember – Nomination – Document system, only persons with memberships on corporate teams can nominate documents, but the system may scale and evolve to multiple types of teams, team memberships, and documents. Less probable, but possible, are other types of nominations.
With these business constraints and assessments delivered from the client, the object modelers have limits and direction for refactoring the TeamMember – Nomination – Document object model. Nominations, as the most stable object in the model, need pluggable collaborations to accommodate new types of team members and documents.
A collaboration is pluggable if different types of objects can be fitted into one or both of the collaborator slots and still satisfy the communication requirements between the two. Communication requirements are diagrammed in object models by drawing an interface for each pluggable collaborator, listing the methods required of it by the other.
This book uses interfaces to highlight and contrast different types of services. Through its placement in an interface, a service is identified as object inherited, conduct business, or returning required information. Services not in an interface are inessential to collaboration and reflect design decisions.11 As a result, the TeamMember – Nomination – Document object model in Figure 8.5 represents collaborations with interfaces that include set property accessors for the team member, nomination, and document objects. Under this model, a nomination is constrained to work only with team members that have security level, role, and privilege properties, and that is not very pluggable.
To make the TeamMember – Nomination – Document object model more pluggable, the essential communication requirements must be factored out of conduct business interfaces and into separate collaboration interfaces. Communication requirements express essential properties as well as services.
EXAMPLE—The client for the TeamMember – Nomination – Document model has stipulated team memberships will always be individual people and that for legal reasons all team memberships and documents must include security levels that are checked during the nomination process.
The first step in moving the object model toward a more pluggable design is to re-express these property requirements in terms of interfaces.
The requirement that all team members must be individuals is restated as, “All team members exhibit the person profile.”12 Asserting that team members exhibit the person profile ensures that all team members have name, title, and email properties, but does not mandate any particular implementation or representation of those properties.
The requirement that all team members must have a security level is restated as, “All team members exhibit an interface with a security level accessor.” Other properties implemented in the example for the team member, namely role and privileges, reflect a particular mechanism for granting people the ability to nominate documents; other mechanisms are possible as long as they conform to the constraint that a team member possess the appropriate security level.
Extracting both these requirements into a single interface and plugging this interface into the nomination object establishes the essential properties of a nomination’s team member, but also allows any number of team member variations and implementations (see Figure 8.7).
Of course, the heart of collaboration is in the business rules, and this chapter has already shown a clear and simple pattern for implementing business rules as part of the process of adding and removing collaborations. Any refactoring of collaborations must include the methods to ensure rule checking is enforced for every variation of object plugged into the collaboration.13 Again, we look to the client to establish the limits of the collaboration methods.
EXAMPLE—The client for the TeamMember – Nomination – Document model has stipulated that nomination history must always be preserved, even for rejected and unresolved nominations.
Here, the client has given a firm rule that nominations will not be removed from team members or documents, so only the “add” collaboration methods need be included in the collaboration interface (see Figure 8.8).
The final step is to check the team member conduct business interface for any additional services required for the nomination collaboration, and extract these into the collaboration interface. This example has no other required services.
Note that while refactoring further might satisfy the design goals of the developers, it is unnecessary to support the business requirements, and begins to conflict with a higher priority: understandability of the model by the client. The collaboration interface shown here allows pluggability, scalability, and extensibility of the business object model while preserving its clarity. The team member implementation coded in this chapter and the previous chapter can easily be re-expressed in terms of the pluggable collaboration interface (see Figure 8.9).
Making collaborations pluggable while simultaneously respecting business constraints is a tall order because it involves allowing two parameters, the collaborators, to vary independently. If, however, one collaborator is fixed and the other varying, as was done in the previous TeamMember – Nomination example, then a desirable level of pluggability can be achieved without sacrificing model integrity. The trick is in deciding which collaborator should be fixed.
The solution is to fix the collaborator that is expected to vary the least. In other words, make pluggable the collaborator that is most likely to be specialized or change over time. Lacking firm guidelines from the client, the next best solution is to use good object sense. With generic – specific, make the specific pluggable. With transaction – specific, it is not as clear which varies the most, but events are rarely specialized. If you want new history against a thing, create a new kind of history, but don’t specialize an old kind. Whole – part is similar. Assemblies don’t get specialized; instead, new kinds of assemblies are created. Containers are the same; they vary less than their parts. Group – member can go either way.