Designing with Patterns Workshop
Designing with Composition and Inheritance
Agenda
-
Use concrete classes to say "What objects are."
-
Use composition to enlist the help of other objects.
-
Use class extension to model permanent IS-A relationships.
-
Understand the difference between inheritance and composition.
What Objects Are
-
Guideline: Use concrete classes to say "What objects are."
-
A concrete class is a blueprint for objects
-
Because objects represent things, class names should be nouns
-
The class maps a name to a set of services offered by the methods
in the public interface
-
The set of services defines what it means to be an instance of the
named class
-
Behavior may be described in terms of implementation:
A
java.util.LinkedList
is a "Linked list implementation of the List
interface."
-
A class should "be" one thing -- be about one concept
An Account
Class
-
To be an
Account
means to offer three services:
deposit()
, withdraw()
, and getBalance()
1 package com.artima.examples.account.ex1;
2
3 /**
4 * Represents a bank account. Money is stored in this account in integral
5 * units. Clients can use this account to store any kind of value, such as
6 * money or points, etc. The meaning of the integral units stored in this
7 * account is a decision of the client that instantiates the account. The
8 * maximum amount of units that can be stored as the current balance of an
9 * <code>Account</code> is Long.MAX_VALUE.
10 */
11 public class Account {
12
13 /**
14 * The current balance
15 */
16 private long balance;
17
18 /**
19 * Withdraws exactly the passed amount from the <code>Account</code>.
20 * Subclasses must withdraw at least the passed amount, but may
21 * effectively withdraw more. For example, if a subclass includes a
22 * notion of a withrawal fee, the subclass's implementation of this
23 * method may charge that fee by decrementing it from the account at the
24 * time of withdrawal.
25 *
26 * @param amount amount to withdraw
27 * @return amount withdrawn from the <code>Account</code>
28 * @throws InsufficientFundsException if the <code>Account</code>
29 * contains insufficient funds for the requested withdrawal
30 * @throws IllegalArgumentException if requested withdrawal amount is
31 * less than or equal to zero.
32 */
33 public long withdraw(long amount)
34 throws InsufficientFundsException {
35
36 if (amount <= 0) {
37 throw new IllegalArgumentException();
38 }
39
40 if (amount > balance) {
41 throw new InsufficientFundsException(amount - balance);
42 }
43
44 balance -= amount;
45 return amount;
46 }
47
48 /**
49 * Deposits exactly the passed amount into the <code>Account</code>.
50 * Subclasses may effectively deposit more or less than the passed amount
51 * into the <code>Account</code>. For example, if a subclass includes a
52 * notion of funds matching, the subclass implementation of this method
53 * may match some or all of the deposited amount at the time of deposit,
54 * effectively increasing the deposited amount. Or, if a subclass
55 * includes the notion of a deposit fee, the subclass's implementation of
56 * this method may charge that fee by decrementing it from the account at
57 * the time of deposit, effectively reducing the deposited amount.
58 *
59 * @param amount amount to deposit
60 * @throws ArithmeticException if requested deposit would cause the
61 * balance of this <code>Account</code> to exceed Long.MAX_VALUE.
62 * @throws IllegalArgumentException if requested deposit is less than
63 * or equal to zero.
64 */
65 public void deposit(long amount) {
66
67 if (amount <= 0) {
68 throw new IllegalArgumentException();
69 }
70
71 long newBal = balance + amount;
72
73 if (newBal < 0) {
74 throw new ArithmeticException();
75 }
76
77 balance = newBal;
78 }
79
80 /**
81 * Gets the current balance of this <code>Account</code>
82 *
83 * @return the current balance
84 */
85 public long getBalance() {
86 return balance;
87 }
88 }
89
1 package com.artima.examples.account.ex1;
2
3 /**
4 * Exception thrown by <code>Account</code>s to indicate that a requested
5 * withdrawal has failed because of insufficient funds.
6 */
7 public class InsufficientFundsException extends Exception {
8
9 /**
10 * Minimum additional balance required for the requested withdrawal to
11 * succeed.
12 */
13 private long shortfall;
14
15 /**
16 * Constructs an <code>InsufficientFundsException</code> with the passed
17 * shortfall and no specified detail message.
18 *
19 * @param shortfall the amount in excess of available funds that caused a
20 * withdrawal request to fail.
21 * @throws IllegalArgumentException if passed shortfall is less than or
22 * equal to zero
23 */
24 public InsufficientFundsException(long shortfall) {
25
26 if (shortfall <= 0) {
27 throw new IllegalArgumentException();
28 }
29
30 this.shortfall = shortfall;
31 }
32
33 /**
34 * Constructs an <code>InsufficientFundsException</code> with the passed
35 * detail message and shortfall.
36 *
37 * @param message the detail message
38 * @param shortfall the amount in excess of available funds that caused a
39 * withdrawal request to fail.
40 * @throws IllegalArgumentException if passed shortfall is less than or
41 * equal to zero
42 */
43 public InsufficientFundsException(String message, long shortfall) {
44
45 super(message);
46
47 if (shortfall <= 0) {
48 throw new IllegalArgumentException();
49 }
50
51 this.shortfall = shortfall;
52 }
53
54 /**
55 * Returns the shortfall that caused a withrawal request to fail. The
56 * shortfall is the minimum additional balance required for the requested
57 * withdrawal to succeed.
58 *
59 * @returns shortfall the amount in excess of available funds that caused
60 * a withdrawal request to fail.
61 */
62 public long getShortfall() {
63 return shortfall;
64 }
65 }
66
67
-
An
Account
object on the heap:
Discussion
-
Guideline: Use concrete classes to say "What objects are."
Using Composition
-
Guideline: Use composition to enlist the help of other objects.
-
A "front-end" object enlists the help of "back-end" objects
-
Front-end object holds references to back-end objects in instance variables
-
Front-end object delegates to back-end objects via
explicit method invocations
OverdraftAccount
as Front-End Class
-
Front-end
OverdraftAccount
enlists the help of back-end
Account
1 package com.artima.examples.account.ex1;
2
3 /**
4 * Represents a bank account with overdraft protection. Instances of this
5 * class are instantiated with a specified maximum overdraft. If a client
6 * attempts to withdraw more than the current account balance, the bank may
7 * loan the amount in excess of the balance to the client. The overdraft
8 * maximum passed to an <code>OverdraftAccount</code>'s constructor is the
9 * maximum amount the bank will lend to the client in this manner. When a
10 * client makes a deposit, the bank will pay itself back first before
11 * increasing the account's balance.
12 *
13 * <p>
14 * Money is stored in this account in integral units. Clients can use this
15 * account to store any kind of value, such as money or points, etc. The
16 * meaning of the integral units stored in this account is a decision of the
17 * client that instantiates the account. The maximum amount of units that can
18 * be stored as the current balance of an <code>Account</code> is
19 * Long.MAX_VALUE.
20 */
21 public class OverdraftAccount {
22
23 /**
24 * Helper back-end <code>Account</code> object
25 */
26 private Account account = new Account();
27
28 /**
29 * The maximum amount the bank will loan to the client.
30 */
31 private final long overdraftMax;
32
33 /**
34 * The current amount the bank has loaned to the client which has not yet
35 * been repaid. This value must at all times be greater than or equal to
36 * zero, and less than or equal to <code>overdraftMax</code>. If this
37 * value is greater than zero, then the balance of the helper
38 * <code>Account</code> (referenced from the <code>account</code>
39 * instance variable) must be exactly zero.
40 */
41 private long overdraft;
42
43 /**
44 * Constructs a new <code>OverdraftAccount</code> with the passed
45 * overdraft maximum.
46 *
47 * @param overdraftMax the maximum amount the bank will loan to the
48 * client
49 */
50 public OverdraftAccount(long overdraftMax) {
51 this.overdraftMax = overdraftMax;
52 }
53
54 /**
55 * Returns the current overdraft, the amount the bank has loaned to the
56 * client that has not yet been repaid.
57 *
58 * @return the current overdraft
59 */
60 public long getOverdraft() {
61 return overdraft;
62 }
63
64 /**
65 * Returns the overdraft maximum, the maximum amount the bank will allow
66 * the client to owe it. For each instance of
67 * <code>OverdraftAccount</code>, the overdraft maximum is constant.
68 *
69 * @return the overdraft maximum
70 */
71 public long getOverdraftMax() {
72 return overdraftMax;
73 }
74
75 /**
76 * Gets the current balance of this <code>OverdraftAccount</code>.
77 *
78 * @return the current balance
79 */
80 public long getBalance() {
81 return account.getBalance();
82 }
83
84 /**
85 * Withdraws the passed amount from this <code>OverdraftAccount</code>.
86 * If the passed amount is less than or equal to the current balance, all
87 * withdrawn funds will be taken from the balance, and the balance will
88 * be decremented by the passed amount. If the passed amount exceeds the
89 * current balance, the bank may loan the client the difference. The bank
90 * will make the loan only if the difference between the passed amount
91 * and the balance (the shortfall) is less than or equal to the available
92 * overdraft. The available overdraft is equal to the current overdraft
93 * (the amount already loaned to the client and not yet repaid),
94 * subtracted from the overdraft maximum, which is passed to the
95 * constructor of any <code>OverdraftAccount</code>.
96 *
97 * <p>
98 * If the passed amount is less than or equal to the current balance, the
99 * <code>withdraw</code> method decrements the current balance by the
100 * passed amount and returns the passed amount. If the passed amount is
101 * greater than the current balance, but the passed amount minus the
102 * current balance is less than or equal to the available overdraft, the
103 * <code>withdraw</code> method sets the current balance to zero, records
104 * the loan, and returns the requested amount. Otherwise, the passed
105 * amount minus the current balance exceeds the available overdraft, so
106 * the <code>withdraw</code> method throws
107 * <code>InsufficientFundsException</code>.
108 *
109 * Subclasses must withdraw at least the passed amount, but may
110 * effectively withdraw more. For example, if a subclass includes a
111 * notion of a withrawal fee, the subclass's implementation of this
112 * method may charge that fee by decrementing it from the account at the
113 * time of withdrawal.
114 *
115 * @param amount amount to withdraw
116 * @return amount withdrawn from the <code>OverdraftAccount</code>
117 * @throws InsufficientFundsException if the
118 * <code>OverdraftAccount</code> contains insufficient funds for the
119 * requested withdrawal
120 * @throws IllegalArgumentException if requested withdrawal amount is
121 * less than or equal to zero.
122 */
123 public long withdraw(long amount)
124 throws InsufficientFundsException {
125
126 if (amount <= 0) {
127 throw new IllegalArgumentException();
128 }
129
130 long bal = account.getBalance();
131 if (bal >= amount) {
132
133 // Balance has sufficient funds, so just take the
134 // money from the balance.
135 return account.withdraw(amount);
136 }
137
138 long shortfall = amount - bal;
139 long extraAvailable = overdraftMax - overdraft;
140
141 if (shortfall > extraAvailable) {
142 throw new InsufficientFundsException(shortfall
143 - extraAvailable);
144 }
145 overdraft += shortfall;
146 account.withdraw(amount - shortfall);
147
148 return amount;
149 }
150
151 /**
152 * Deposits the passed amount into the <code>OverdraftAccount</code>. If
153 * the current overdraft is zero, the balance will be increased by the
154 * passed amount. Otherwise, the bank will attempt to pay off the
155 * overdraft first, before increasing the current balance by the amount
156 * remaining after the overdraft is repaid, if any.
157 *
158 * <p>
159 * For example, if the balance is 0, the overdraft is 100, and the
160 * <code>deposit</code> method is invoked with a passed
161 * <code>amount</code> of 50, the bank would use all 50 of those monetary
162 * units to pay down the overdraft. The overdraft would be reduced to 50
163 * and the balance would remain at 0. If subsequently, the client
164 * deposits another 100 units, the bank would use 50 of those units to
165 * pay off the overdraft loan and direct the remaining 50 into the
166 * balance. The new overdraft would be 0 and the new balance would be
167 * 50.
168 *
169 * Subclasses may effectively deposit more or less than the passed amount
170 * into the <code>Account</code>. For example, if a subclass includes a
171 * notion of funds matching, the subclass implementation of this method
172 * may match some or all of the deposited amount at the time of deposit,
173 * effectively increasing the deposited amount. Or, if a subclass
174 * includes the notion of a deposit fee, the subclass's implementation of
175 * this method may charge that fee by decrementing it from the account at
176 * the time of deposit, effectively reducing the deposited amount.
177 *
178 * @param amount amount to deposit
179 * @throws ArithmeticException if requested deposit would cause the
180 * balance of this <code>Account</code> to exceed Long.MAX_VALUE.
181 * @throws IllegalArgumentException if requested withdrawal amount is
182 * less than or equal to zero.
183 */
184 public void deposit(long amount) {
185
186 if (amount <= 0) {
187 throw new IllegalArgumentException();
188 }
189
190 if (overdraft > 0) {
191 if (amount < overdraft) {
192 overdraft -= amount;
193 }
194 else {
195 long diff = amount - overdraft;
196 overdraft = 0;
197 account.deposit(diff);
198 }
199 }
200 else {
201 account.deposit(amount);
202 }
203 }
204 }
-
An
OverdraftAccount
object on the heap:
Discussion
-
Guideline: Use composition to enlist the help of other objects.
Class Extension
-
Guideline: Use class extension to model permanent IS-A relationships.
-
Subclasses inherit the interface and, by default, the implementation of
their superclass.
-
A subclass should have some kind of specialized behavior (it should provide
the same service as the superclass, only differently)
-
A subclass may also offer new services not offered by the superclass
-
IS-A means "Is a specialized kind of," so watch out for:
Bootsie
IS-A Dog
Car
IS-AN Automobile
OverdraftAccount
as Subclass
-
An
OverdraftAccount
IS-AN Account
-
OverdraftAccount
has the same interface and fulfills the
same semantic contract as Account
-
But,
OverdraftAccount
fulfills the semantic contract
differently than Account
1 package com.artima.examples.account.ex2;
2
3 /**
4 * Represents a bank account with overdraft protection. Instances of this
5 * class are instantiated with a specified maximum overdraft. If a client
6 * attempts to withdraw more than the current account balance, the bank may
7 * loan the amount in excess of the balance to the client. The overdraft
8 * maximum passed to an <code>OverdraftAccount</code>'s constructor is the
9 * maximum amount the bank will lend to the client in this manner. When a
10 * client makes a deposit, the bank will pay itself back first before
11 * increasing the account's balance.
12 *
13 * <p>
14 * As described in superclass <code>Account</code>, money is stored in this
15 * account in integral units. Clients can use this account to store any kind
16 * of value, such as money or points, etc. The meaning of the integral units
17 * stored in this account is a decision of the client that instantiates the
18 * account. The maximum amount of units that can be stored as the current
19 * balance of an <code>OverdraftAccount</code> is Long.MAX_VALUE.
20 */
21 public class OverdraftAccount extends Account {
22
23 /**
24 * The maximum amount the bank will loan to the client.
25 */
26 private final long overdraftMax;
27
28 /**
29 * The current amount the bank has loaned to the client which has not yet
30 * been repaid. This must be zero to overdraftMax.
31 */
32 private long overdraft;
33
34 /**
35 * Constructs a new <code>OverdraftAccount</code> with the passed
36 * <code>overdraftMax</code>.
37 *
38 * @param overdraftMax the maximum amount the bank will loan to the
39 * client
40 * @throws IllegalArgumentException if the passed
41 * <code>overdraftMax</code> is less than zero
42 */
43 public OverdraftAccount(long overdraftMax) {
44
45 if (overdraftMax < 0) {
46 throw new IllegalArgumentException();
47 }
48
49 this.overdraftMax = overdraftMax;
50 }
51
52 /**
53 * Returns the current overdraft, the amount the bank has loaned to the
54 * client that has not yet been repaid.
55 *
56 * @return the current overdraft
57 */
58 public long getOverdraft() {
59 return overdraft;
60 }
61
62 /**
63 * Returns the overdraft maximum, the maximum amount the bank will allow
64 * the client to owe it. For each instance of
65 * <code>OverdraftAccount</code>, the overdraft maximum is constant.
66 *
67 * @return the overdraft maximum
68 */
69 public long getOverdraftMax() {
70 return overdraftMax;
71 }
72
73 /**
74 * Withdraws exactly the passed amount from the
75 * <code>OverdraftAccount</code>. If the passed amount is less than or
76 * equal to the current balance, all withdrawn funds will be taken from
77 * the balance, and the balance will be decremented by the passed amount.
78 * If the passed amount exceeds the current balance, the bank may loan
79 * the client the difference. The bank will make the loan only if the
80 * difference between the passed amount and the balance is less than or
81 * equal to the available overdraft. The available overdraft is equal to
82 * the current overdraft (the amount already loaned to the client and not
83 * yet repaid) subtracted from the overdraft maximum, that was passed to
84 * the constructor of the <code>OverdraftAccount</code>.
85 *
86 * <p>
87 * If the passed amount less the current balance is less than or equal to
88 * the available overdraft, the <code>withdraw</code> method returns the
89 * requested amount, sets the current balance to zero, and records the
90 * loan. Otherwise, if the passed amount less the current balance exceeds
91 * the available overdraft, the <code>withdraw</code> method throws
92 * <code>InsufficientFundsException</code>.
93 *
94 * @param amount the amount to withdraw
95 * @return amount withdrawn from the <code>OverdraftAccount</code>
96 * @throws InsufficientFundsException if the
97 * <code>OverdraftAccount</code> contains insufficient funds for the
98 * requested withdrawal. For an <code>OverdraftAccount</code>,
99 * insufficient funds means the requested amount to withrdraw
100 * exceeds the sum of the current balance and the available
101 * overdraft.
102 * @throws IllegalArgumentException if the passed <code>amount</code> to
103 * withdraw is less than or equal to zero.
104 */
105 public long withdraw(long amount) throws InsufficientFundsException {
106
107 if (amount <= 0) {
108 throw new IllegalArgumentException();
109 }
110
111 long bal = getBalance();
112 if (bal >= amount) {
113
114 // Balance has sufficient funds, just take the
115 // money from the balance.
116 return super.withdraw(amount);
117 }
118
119 long shortfall = amount - bal;
120 long extraAvailable = overdraftMax - overdraft;
121
122 if (shortfall > extraAvailable) {
123 throw new InsufficientFundsException(shortfall - extraAvailable);
124 }
125 overdraft += shortfall;
126 super.withdraw(amount - shortfall);
127
128 // Note that here I'm not checking that the return value of
129 // super.withdraw is equal to amount - shortfall, because I'm
130 // trusting class Account to fulfill its contract.
131 return amount;
132 }
133
134 /**
135 * Deposits exactly the passed amount into the <code>Account</code>. If
136 * the current overdraft is zero, the balance will be increased by the
137 * passed amount. Otherwise, the bank will attempt to pay off the
138 * overdraft first, before increasing the current balance by the amount
139 * remaining after the overdraft is repaid, if any.
140 *
141 * <p>
142 * For example, if the balance is 200, the overdraft is 100, and the
143 * <code>deposit</code> method is invoked with a passed
144 * <code>amount</code> of 50, the bank would use all 50 of those monetary
145 * units to pay down the overdraft. The overdraft would be reduced to 50
146 * and the balance would remain at 200. If subsequently, the client
147 * deposits another 100 units, the bank would use 50 of those units to
148 * pay off the overdraft loan and direct the remaining 50 into the
149 * balance. The new overdraft would be 0 and the new balance would be
150 * 250.
151 *
152 * @param amount amount to deposit
153 * @throws ArithmeticException if requested deposit would cause the
154 * balance of this <code>OverdraftAccount</code> to exceed
155 * Long.MAX_VALUE.
156 */
157 public void deposit(long amount) {
158 if (overdraft > 0) {
159 if (amount < overdraft) {
160 overdraft -= amount;
161 }
162 else {
163 long diff = amount - overdraft;
164 overdraft = 0;
165 super.deposit(diff);
166 }
167 }
168 else {
169 super.deposit(amount);
170 }
171 }
172 }
-
An
OverdraftAccount
object on the heap:
Roles and Attributes
-
IS-A is necessary but not sufficient
-
Watch out for impermanent roles (try composition):
Manager
IS-AN Employee
Peon
IS-AN Employee
-
Watch out for attributes (try a final instance variable):
RedCup
IS-A Cup
BlueCup
IS-A Cup
State and Class
- Behavior influenced by object's state (e.g.
Account
balance is $5000)
- Behavior influenced by object's class (e.g.
OverDraftAccount
)
- Non-permanent IS-A relationships (roles) must be modeled with state
(perhaps composition)
- Permanent IS-A relationships can be modeled either with state or with
subclasses
How do You Decide?
- To accomodate a likely vector of change by making it possible to plug in new subclasses in the future
- To define a tight, close-knit family, such as the
StampMachine.State
family
- To communicate important concepts in the design, where each subclass is about one concept
- To form a vocabulary for programmers: If there's an account in the problem domain, there should likely
be an
Account
in the solution.
- Be unsurprising:
Square
IS-A Rectangle
?
Why IS-A?
-
Inheritance gives you code reuse. (OO Claim: promotes code reuse)
-
Inheritance gives you polymorphism:
Account a = new OverdraftAccount(50000);
-
Don't use inheritance to get at code reuse or polymorphism without
IS-A
-
Why? Because that's what inheritance means
Discussion
-
Guideline: Use class extension to model permanent IS-A relationships.
Inheritance vs. Composition
-
Guideline: Understand the difference between inheritance and
composition.
Inheritance Yields (Slightly) Better Performance:
-
Composition's method forwarding/delegation will often be slower than
inheritance's dynamic binding
-
Composition results in more objects getting instantiated, which can
incur a performance cost at allocation,
<init>()
,
and GC time
Composition Yields Better Flexibility:
-
Interfaces of classes involved in a composition relationship need not be
compatible, so it's easier to change the interfaces
-
Composition allows you to delay creation of back-end objects until (and
unless) you need them
-
Composition allows you to change back-end objects throughout the lifetime
of the front-end object
-
Composition allows front-end objects to share the same back-end objects
But:
-
Composition's method forwarding/delegation results in more code that has to
be written, debugged, and maintained.
-
Easier to add new subclasses than new front-end classes, unless you
use composition with interfaces
Discussion
-
Guideline: Understand the difference between inheritance and
composition.