7.2 Polymorphism


7.2 Polymorphism

Polymorphism means the quality or state of being able to assume different forms. In the object-oriented context, polymorphism means the multitude of forms that object references (e.g., a variable or parameter) can take. We are interested in dynamic polymorphism, or the ability of an object reference to be bound to many different kinds of objects. It is "dynamic" precisely when the object's class cannot be determined at compile time, but only at runtime. The only thing that is determined is a type the object has to obey.

By invoking an operation on such a reference, we cannot previously determine the concrete method (implementation) that will eventually be executed, since method execution depends on both the operation invoked and the class of the invocation receiver. For example, if we have the variable myBook of type Book (see Section 7.1), then an instance of either the Book class or the FixedPriceBook class can hide behind it.

In this context, it is important to distinguish between the terms type and class. To ensure correct compilation of an operation call, such as myBook.sell(10.0), only the type of the objects is decisive. In Java, an object can embody many different types. A type is defined by its class and all of its superclasses, and additionally by all interfaces implemented by its class or one of its superclasses. [7] This means that instances of the class FixedPriceBook can assume the following forms: Object, Book, or FixedPriceBook. When an operation is invoked on a polymorphic object reference, then the signature of the operation—including its name and number, type, and the order of its parameters—determines the method that will actually be executed.

A running object-oriented program can be regarded as a world where client objects invoke operations on other objects. Those other objects provide some kind of service for the client; let's therefore call them server objects for the rest of this chapter. From the developer's perspective, polymorphic server objects facilitate the programming work, because they reduce the size and the complexity of the client code on the one hand, and the maintenance effort when adding or removing new server classes on the other hand.

From the tester's perspective, polymorphism represents the counter-part of inheritance. While we reused test suites of the superclass and special interface test classes in our previous example to ensure that all implementations of a type would obey the specification, we are now testing the interplay, or interaction, between the service classes under test and their clients.

Polymorphic operations represent a kind of case statement, rendering the control flow of the client code under test more complicated. The complexity of the logic is hidden behind syntactic simplicity. In addition, server code can be modified regardless of its clients, as long as it continues to formally meet the interface specification. Therefore, the optimistic assumption that it is sufficient to adequately test all server classes and the interaction of our client with one single server is wrong once again. The following things can go wrong:

  • Our reference object could be bound to a wrong server object.

  • The wrong method of the server is invoked. This is often caused by signatures that differ only in the type of their parameters.

  • The client code does not consider the full spectrum of a method's return values. This also includes correct handling of all possible exceptions.

  • Certain server classes violate the rules of the substitution principle, causing the client code to stumble.

  • The pre- or post-condition of a polymorphic method was changed, but the server was not adapted accordingly.

Let's use the Book example again to better explain these issues. Our two "servers"—Book and FixedPriceBook—were adequately tested. Now we proceed to programming the first client, an automated seller called Mr. BookSeller, with an interface that looks like this:

 public class BookSeller {    public void setBookToSell(Book book);    public Book getBookToSell();    public String sellFor(double price); } 

Probably the only big surprise here is that sell() should return a string telling us whether or not the sales transaction was successful. And we can build the following test suite swiftly; it is pretty neat, but not complete yet:

 public class BookSellerTest extends TestCase {    private BookSeller seller;    private Book book;    protected void setUp() {       seller = new BookSeller();       book = new Book("test book", 10.0, 12.0);       seller.setBookToSell(book);    }    public void testNormalSell() {       assertEquals(book, seller.getBookToSell());       String answer = seller.sellFor(11.0);       assertEquals("OK", answer);       assertEquals(11.0, book.getSoldFor(), 0.0);    }    public void testSellAboveRecommendedPrice() {       String answer = seller.sellFor(12.01);       assertEquals("Price too high", answer);       assertEquals(0.0, book.getSoldFor(), 0.0);    }    public void testSellBelowWholesalePrice() {       String answer = seller.sellFor(0.99);       assertEquals("Price too low", answer);       assertEquals(0.0, book.getSoldFor(), 0.0);    } } 

What's mainly missing in this test class are cases for repeated sales attempts after a success or failure, yet another practical exercise for our readers. However, the problem lies somewhere else. The following correct client code shows where we have a flaw:

 BookSeller seller = new BookSeller(); Book book = new FixedPriceBook("Pygmalion", 10.0, 12.0); seller.setBookToSell(book); String answer = seller.sellFor(11.0); System.out.println(answer); System.out.println(book.getSoldFor()); 

This code generates the output:

 OK 0.0 

Despite the "OK" there was no sale. We can see the reason if we take a closer look at the implementation of BookSeller.sellFor():

 public String sellFor(double price) {    if (price < bookToSell.getWholesalePrice()) {       return "Price too low";    }    if (price > bookToSell.getRecommendedPrice()) {       return "Price too high";    }    try {       bookToSell.sell(price);    } catch (PriceOutOfBoundsException impossible) {}    return "OK"; } 

Selling a fixed price book at a price not equivalent to its fixed price makes it throw a PriceOutOfBoundsException. The developer of the class BookSeller was not expecting that to happen. Calling the exception variable impossible shows that he started from the wrong assumption. Such a misconception can happen easily when fixed price books are introduced after the implementation of the BookSeller class.

And the moral of this story?

Never trust a single class!

Or, rephrased as a guideline: For polymorphic operations, design your interaction tests so that all possible implementations of the addressed type are tested. And because this may mean a very high effort, here is another (weaker) rule: When creating test cases, consider the possibility that a reference might be polymorphically bound; edit the interaction tests of all your clients when you modify the server class.

At least two additional test cases are needed in our current example:

 public void testNormalSellFPB() {    Book fpBook = new FixedPriceBook("FPB", 10.0, 12.0);    seller.setBookToSell(fpBook);    assertEquals(fpBook, seller.getBookToSell());    String answer = seller.sellFor(12.0);    assertEquals("OK", answer);    assertEquals(12.0, fpBook.getSoldFor(), 0.0); } public void testSellFPBBelowRecommendedPrice() {    Book fpBook = new FixedPriceBook("FPB", 10.0, 12.0);    seller.setBookToSell(fpBook);    String answer = seller.sellFor(11.99);    assertEquals("Price too low", answer);    assertEquals(0.0, fpBook.getSoldFor(), 0.0); } 

In the general case, the effort for adequate testing of polymorphic interactions can be much higher. This is the price we have to pay for the flexibility and apparent simplicity of our multifaceted object meshes. There's no such thing as a free lunch!

[7]Depending on the programming language, types are identified in a totally different way. For example, Smalltalk defines the type solely by the implemented methods.




Unit Testing in Java. How Tests Drive the Code
Unit Testing in Java: How Tests Drive the Code (The Morgan Kaufmann Series in Software Engineering and Programming)
ISBN: 1558608680
EAN: 2147483647
Year: 2003
Pages: 144
Authors: Johannes Link

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net