Missing Opportunities for Polymorphism

From WikiContent

(Difference between revisions)
Jump to: navigation, search
Line 1: Line 1:
-
It is not without irony that one of the best examples of polymorphism is also a sign that you are missing opportunities to exploit polymorphism. The example is best expressed in Smalltalk where the ''if-then-else'' control structure is implemented as method call. If you send an <code>if: aBlock else: anotherBlock message</code> to <code>true</code>, it's implementation of <code>if:else:</code> will execute <code>aBlock</code>. Sending that same message to <code>false</code> will cause <code>anotherBlock</code> to be executed.
+
Polymorphism is one of the grand ideas that is fundamental to OO. The word taken, taken from Greek, means many (poly) forms (morph). In the context of programming polymorphism refers to many forms of a particular class of objects or method. But polymorphism isn’t simply about alternate implementations. Used carefully, polymorphism creates tiny localized execution contexts that lets us work without the need for verbose if-then-else blocks. Being in a context allows us to do the right thing where as being outside of that context forces us to reconstruct it so that we can do the right thing. With careful use of alternate implementations, we can capture context that can help us produce less code that is more readable. This is best demonstrated with some code.
-
For <code>True</code>:
+
<code>
 +
public class ShoppingCart {
 +
private HashMap<Item,Item> cart;
 +
public Bag() { cart = new HashMap<Item,Item>(); }
 +
public void put(Item item) { bag.cart(item,item); }
 +
public Item take( Item itemToTake) { return cart.remove( itemToTake); }
 +
public boolean look( Item itemToMatch) { return cart.containsKey( itemToMatch); }
 +
public Item next() { .. find and item and “take” it. }
 +
public boolean isEmpty() { return cart.size() == 0; }
 +
}
 +
</code>
-
if: aBlock else: anotherBlock
+
Listing 1. Unrealistically simple ShoppingCart
-
^aBlock execute.
+
-
For <code>False</code>:
+
Lets say our webshop offers items that can be downloaded and items that need to be shipped. Lets build another object that supports these operations.
-
if: aBlock else: anotherBlock
+
<code>
-
^anotherBlock execute.
+
public class Shipping {
 +
public boolean ship(Item item, GroundAddress address) {…. }
 +
public boolean ship(Item item, EMailAddress address { … }
 +
}
 +
</code>
-
By unifying the API for these Booleans, we can write code such as <code>result := someBoolean if: aBlock else: anotherBlock.</code> and end up with the expected results. However, the fact that we had to execute and ''if-then-else'' in the first place was because at some point in the past we lost or did not properly captured some vital piece of information. Now that this bit of information is missing, we have to recreate that information and the tool that is most commonly used to do that is ''if-then-else''.
+
Listing 2. Shipping
-
How does this relate to polymorphism? One of the beautiful features of capturing state and behavior into an object is that these objects act as mini execution contexts. If we revisit the code above, we can see that we don't have to make any decisions. We can use the fact that our execution context is either true or false allows us to get straight to the block that is to executed. If we didn't completely understand the context in which we were working in, we'd be forced to some how rebuild that context in order for us to know what to do.
+
When a client is finished checkout we need to ship the purchases.
-
Let's consider the case where we are modeling bank accounts. Now, we could define all accounts as just that, <code>Account</code>. We could distinguish between a checking and savings account an attribute, <code>accountType</code>, which could be an <code>enum</code>. We could also capture the distinction by creating two classes, <code>CheckingAccount</code> and <code>SavingsAccount</code>. Let's say we now implement an <code>accrueInterest()</code> method where each type of account has a different interest rate. With the former case we will have to write some logic to match the type of account with the rate of interest. With the later, each object knows what it is and hence can just directly acquire the interest rate. If can do this because of how it is defined without using ''if-then-else'' to rebuild that information.
+
<code>
 +
while ( ! cart.isEmpty()) {
 +
Item item = cart.next();
 +
shipping.ship( item, ??? );
 +
}
 +
</code>
-
Classes designed to exploit polymorphism have a much reduced dependency on ''if-then-else''. They also tend to be more readable and contain less code. Each of these attributes show that improved polymorphism is more than just a neat coding trick, it's a technique that leads to better code.
+
Listing 3. Shipping the order.
 +
 
 +
The parameter ??? isn’t some new fancy Java syntax, it’s an indication that we’ve got a problem. Should I email or snail mail the item? By the time I get to this point in the code, I’ve lost the context I need to resolve this question. We could capture method of shipment in a boolean or enum but then we’d need and if-then-else to reconstruct our understanding of which form of ship needs to be called. Another solution would be create two classes that both extend Item. Lets call these EItem and GroundItem. Now lets write some code. I’ll promote Item to be an interface that supports a single method public boolean ship( Shipping shipper). To ship the contents of the cart, we will call item.ship(shipper). Classes EItem and GroundItem will both implement ship.
 +
 
 +
<code>
 +
public class EItem implements Item {
 +
public boolean ship(Shipping shipper) {
 +
shipper.ship( this, customer.getEmailAddress());
 +
}
 +
}
 +
 
 +
public class GroundItem implements Item {
 +
public boolean ship(Shipping shipper) {
 +
shipper.ship( this, customer.getGroundAddress());
 +
}
 +
}
 +
</code>
 +
Listing 4. Specialization of Item
 +
 
 +
Each item knows if it can be downloaded or if ground shipment is required. There is no need to reconstruct lost context. This information has been captured at the time of order. In this example, EItem and GroundItem provide a micro execution context that lets us get on with it.
 +
 
 +
The code above is representative of the command pattern and double dispatch pattern. Effective use of both of these pattern rely on careful use of polymorphism. This is why they often play well together. What is noticeably missing are if-then-else blocks.
 +
 
 +
While there are cases where it’s much more practical to use if-then-else instead of polymorphism, it is more often the case that a more polymorphic coding style will yield a smaller, more readable and less fragile code base. The number of missed opportunities lies in the number of if-then-else statements we can find in our code.
By [[Kirk Pepperdine]]
By [[Kirk Pepperdine]]

Revision as of 09:52, 28 April 2009

Polymorphism is one of the grand ideas that is fundamental to OO. The word taken, taken from Greek, means many (poly) forms (morph). In the context of programming polymorphism refers to many forms of a particular class of objects or method. But polymorphism isn’t simply about alternate implementations. Used carefully, polymorphism creates tiny localized execution contexts that lets us work without the need for verbose if-then-else blocks. Being in a context allows us to do the right thing where as being outside of that context forces us to reconstruct it so that we can do the right thing. With careful use of alternate implementations, we can capture context that can help us produce less code that is more readable. This is best demonstrated with some code.

public class ShoppingCart {

   private HashMap<Item,Item> cart;
   public Bag() { cart = new HashMap<Item,Item>(); }
   public void put(Item item) { bag.cart(item,item); }
   public Item take( Item itemToTake) { return cart.remove( itemToTake); }
   public boolean look( Item itemToMatch) { return cart.containsKey( itemToMatch); }
   public Item next() { .. find and item and “take” it. }
   public boolean isEmpty() { return cart.size() == 0; }

}

Listing 1. Unrealistically simple ShoppingCart

Lets say our webshop offers items that can be downloaded and items that need to be shipped. Lets build another object that supports these operations.

public class Shipping {

   public boolean ship(Item item, GroundAddress address) {…. }
   public boolean ship(Item item, EMailAddress address { … }

}

Listing 2. Shipping

When a client is finished checkout we need to ship the purchases.

while ( ! cart.isEmpty()) {

   Item item = cart.next();
   shipping.ship( item, ??? );

}

Listing 3. Shipping the order.

The parameter ??? isn’t some new fancy Java syntax, it’s an indication that we’ve got a problem. Should I email or snail mail the item? By the time I get to this point in the code, I’ve lost the context I need to resolve this question. We could capture method of shipment in a boolean or enum but then we’d need and if-then-else to reconstruct our understanding of which form of ship needs to be called. Another solution would be create two classes that both extend Item. Lets call these EItem and GroundItem. Now lets write some code. I’ll promote Item to be an interface that supports a single method public boolean ship( Shipping shipper). To ship the contents of the cart, we will call item.ship(shipper). Classes EItem and GroundItem will both implement ship.

public class EItem implements Item {

   public boolean ship(Shipping shipper) {
       shipper.ship( this, customer.getEmailAddress());
   }

}

public class GroundItem implements Item {

   public boolean ship(Shipping shipper) {
       shipper.ship( this, customer.getGroundAddress());
   }

} Listing 4. Specialization of Item

Each item knows if it can be downloaded or if ground shipment is required. There is no need to reconstruct lost context. This information has been captured at the time of order. In this example, EItem and GroundItem provide a micro execution context that lets us get on with it.

The code above is representative of the command pattern and double dispatch pattern. Effective use of both of these pattern rely on careful use of polymorphism. This is why they often play well together. What is noticeably missing are if-then-else blocks.

While there are cases where it’s much more practical to use if-then-else instead of polymorphism, it is more often the case that a more polymorphic coding style will yield a smaller, more readable and less fragile code base. The number of missed opportunities lies in the number of if-then-else statements we can find in our code.

By Kirk Pepperdine

This work is licensed under a Creative Commons Attribution 3


Back to 97 Things Every Programmer Should Know home page

Personal tools