CSC/ECE 517 Fall 2010/ch7 7g mr

From Expertiza_Wiki
Jump to navigation Jump to search

Square-Rectangle Anti-Pattern

The Square-Rectangle Anti-Pattern, also known as the Circle-Ellipse problem is a software development problem that can occur in Object Oriented Programming with inheritance when a base class contains methods which invalidate the derived class, and thereby violate the Liskov Substitution Principle.

Description

Consider classes Rectangle and Square. By the 'is a' test, one might reasonably define the Square class as a specialization of the Rectangle class (Square is a Rectangle), where all sides are of equal length. But suppose Rectangle defines a constructor that takes two arguments, x and y, for the lengths of the sides. How can Square provide an implementation for this constructor? If the values passed for x and y are not the same, it would be unable to construct an instance of Square that satisfied the input criteria.

The situation is worse for modifier methods on the base class. Suppose Rectangle has a method setX which modifies only two sides of the rectangle. When called on a Square, it would cause the object to no longer be a square. This can be particularly problematic when new modifiers are added to an existing base class after it has been subclassed. If it has been subclassed improperly, the subclasses may be broken by the new functionality in the base class.

Example

The following example illustrates the problem. In this case, Square has decided not to implement a constructor defined in Rectangle. But there is still a problem with the setX modifier:

 class Rectangle 
 {
     protected int x, y;
 
     public Rectangle() {}
   
     public Rectangle(int x, int y) {
         this.x = x;
         this.y = y;
     }
 
     public void setX(int x) {
         this.x = x;
     }
 
     public void setY(int y) {
         this.y = y;
     }
 
     public void print() {
         System.out.println(this.getClass().getName() + " has sides of length " + x + " and " + y);
     }
 }
 
 public class Square extends Rectangle 
 {
     public Square(int x) {
         this.x = x;
         this.y = x;
     }
 
     public static void main(String[] args) {
         Square s = new Square(10);
         s.print();
         s.setX(5);
         s.print();
     }
 }

This produces the following output:

 Square has sides of length 10 and 10
 Square has sides of length 5 and 10

Alternatives

Reverse the inheritance

Instead of having Square be a subclass of Rectangle, one could make Rectangle a subclass of Square. Afterall, you could say a Rectangle is a specialization of Square in which the sides don't need to be of the same length. Thus Square would define a constructor that takes only argument x. Rectangle could support that constructor and also provide another constructor which takes x and y. Likewise, Square could have only a setx method whereas Rectangle could also define a sety method.

 public class Square
 {
     protected int x;
 
     public Square(int x) {
         this.x = x;
     }
 
     protected void setX(int x) {
         this.x = x;
     }
 }
 
 class Rectangle extends Square
 {
     protected int y;
 
     public Rectangle() {}
   
     public Rectangle(int x, int y) {
         super(x);
         this.y = y;
     }
 
     protected void setY(int y) {
         this.y = y;
     }
 }

This is somewhat of a contrived example since Square does not have any additional information than Rectangle. Such may not be the case in more realistic scenarios.

Hidden constructor

The derived Rectangle class could choose to not provide an implementation for the constructor that takes arguments x and y. This would prevent any caller from instantiating an instance of Square with arguments x and y. This design maybe controversial since one normally expects a subclass to support all the constructors of the base class.

 class Rectangle 
 {
     protected int x, y;
 
     public Rectangle() {}
   
     public Rectangle(int x, int y) {
         this.x = x;
         this.y = y;
     }
 }
 
 public class Square extends Rectangle 
 {
     public Square(int x) {
         this.x = x;
         this.y = x;
     }
 
     private Square(int x, int y) { }
 }

Throw exception

The Square class could override any methods or constructors which violate the contract and throw an exception. In the constructor example above, it could throw an exception if the lengths provided for x and y are not equal. In the setx method example, it could check the value of y and throw an exception if the passed in value for x wasn't equal to y. This is somewhat dubious but does allow the setx method to be called if the value isn't changing.

 class Rectangle 
 {
     protected int x, y;
 
     public Rectangle() {}
   
     public Rectangle(int x, int y) {
         this.x = x;
         this.y = y;
     }
 }
 
 public class Square extends Rectangle 
 {
     public Square(int x) {
         this.x = x;
         this.y = x;
     }
 
     private Square(int x, int y) {
        throw new UnsupportedOperationException();
     }
 }

In Java the UnsupportedOperationException could be thrown, which is a RuntimeException and therefore does not need to be declared. If another type of exception was thrown, it would have to be declared.

Return new value

In this solution, the super class method would need to return its new value X when a method is called to change it.[1] The derived class reacts by stubbornly just returning its current value, thus forcing the caller to use a method supported for that class. This solution avoids throwing exceptions but could have undesireable consequences if the calling code is not careful to check the return value to make sure it was modified. But perhaps worse, it requires the method signatures to be changed in the base class to support the problem in the derived class.

 class Rectangle 
 {
     protected int x, y;
 
     public Rectangle() {}
   
     protected int setX(int x) {
         this.x = x;
         return this.x;
     }
 }
 
 public class Square extends Rectangle 
 {
     public Square(int x) {
         this.x = x;
         this.y = x;
     }
 
     protected int setX(int x) {
         return this.x;
     }
 }

Make method more powerful

The implementation for the setx method in Square could just change the value of y also. This results in the method becoming more powerful, and might not be what the caller intended.

 class Rectangle 
 {
     protected int x, y;
 
     public Rectangle() {}
   
     protected void setX(int x) {
         this.x = x;
     }
 }
 
 public class Square extends Rectangle 
 {
     public Square(int x) {
         super(x, x);
     }
 
     protected void setX(int x) {
         super(x);
         this.y = x;
     }
 }

Return instance of new class

When a method is called that will mutate the object, the implementation could instantiate an instance of the base class and return that instead[1]. This requires the methods to declare a return type of the base class. This could have unexpected consequences on the program.

 class Rectangle 
 {
     protected int x, y;
 
     public Rectangle() {}
   
     protected Rectangle setX(int x) {
         this.x = x;
         return this;
     }
 }
 
 public class Square extends Rectangle 
 {
     public Square(int x) {
         super(x, x);
     }
 
     protected Rectangle setX(int x) {
         if (x == this.y)
             return super(x);
         else
             return new Rectangle(x);
     }
 }

Use separate immutable and mutable classes

In this approach, the Rectangle class is immutable and therefore its methods cannot change derived instances, they can only return new instances of Rectangle. A separate MutableRectangle class could be defined and all the modifiers put into it.

 class Rectangle 
 {
     protected int x, y;
 
     public Rectangle() {}
   
     public Rectangle(int x, int y) {
         this.x = x;
         this.y = y;
     }
 
     public void setX(int x) {
         this.x = x;
     }
 
     public void setY(int y) {
         this.y = y;
     }
 }
 
 class ImmutableRectangle extends Rectangle
 {
     public Rectangle() { super(); }
   
     public Rectangle(int x, int y) {
         super(x, y);
     }
 
     private void setX(int x) { }
 
     private void setY(int y) { }
 }
 
 public class Square extends ImmutableRectangle 
 {
     ...
 }

As in some of the previous solutions, this has the disadvantage of requiring significant modifications to the design of the base class. It also requires the calling code to perform more assignments and may result in more memory being consumed by all the immutable instances around.

Initialize base class as mutable/immutable

This is similar to the above but not requiring separate mutable and immutable classes. Rectangle could be initialized, presumably by a constructor, with a state indicating if it can be modified. The modifier methods would check this state to see if the operation was allowed.

 class Rectangle 
 {
     protected int x, y;
 
     protected boolean mutable;
 
     public Rectangle(boolean mutable) {
         this.mutable = mutable;
     }
 
     public void setX(int x) {
         if (mutable)
             this.x = x;
     }
 }
 
 public class Square extends Rectangle 
 {
    ...
 }


See also

References