CSC/ECE 517 Fall 2010/ch7 7g mr
Square-Rectangle Anti-Pattern
The Square-Rectangle Anti-Pattern 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; Rectangle() {} Rectangle(int x, int y) { this.x = x; this.y = y; } void setX(int x) { this.x = x; } void print() { System.out.println(this.getClass().getName() + " has sides of length " + x + " and " + y); } } public class Square extends Rectangle { 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.
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.
Absent 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.
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.
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. 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.
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.
Return instance of new class
If the constructor is called with different values for x and y, or if the setx method is called, the implementation on Square could instantiate an instance of Rectangle and return that instead. This requires the methods to declare a return type of the base class. This could have unexpected consequences on the program.
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.
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.
See also
- Circle-Ellipse Problem (Wikipedia)
- Liskov Substitution Principle (Wikipedia)