This article is the continuation of the topic “Subtyping and Java” that we started in Part 1 of the series Solid Code Design. To start with we will touch upon the concept of specification and implementation aspects of a class. We will limit our focus only within Java Classes.
A class’s specification is a detailed description of what it exactly does. In other words, a specification clearly describes all the features visible to the clients. And a class’s implementation focuses on the representations and mechanisms to achieve those features. When we talk about type, we focus on the specification. Java interfaces hold the specifications, but they lack implementations.
Client code depends only on the specifications. Therefore, your classes or interfaces must be consistent with their specifications. If that happens, your class will be correct and the client code will be happy.
When writing the specifications, you should be clear about three things without ambiguity: preconditions, postconditions, and invariants. Preconditions and preconditions are related to methods declared inside Java classes and interfaces. And invariants are related to global properties applicable to all instances of a class. Every method implementation must respect the invariants.
We will understand the concepts of preconditions, postconditions, and invariants with an simple example. Consider the below class.
/**
* The SpecialSubtractor class provides a subtract operation that returns
* the difference of two given integers. This class has a global property:
* the state of every object of SpecialSubtractor class will never be
* negative. Every newly created SpecialSubtractor object starts with
* state 0.
* Every invocation of the subtract operation will make sure class invariant
* is intact.
*
*/
public class SpecialSubtractor {
// The value of state will never be negative
protected int state;
public SpecialSubtractor() {
this.state = 0;
}
/**
* Subtracts two integers
* Precondition: Both operands are greater than zero
* and operand1 is greater than operand2 and operand1
* must be greater than 5.
* Postcondition: Returns the difference of right operand
* from left operand
* @param operand1 the left operand
* @param operand2 the right operand
* @throws IllegalArgumentException if either of the operands
* is negative or operand1 < operand2 or operand1 <= 5
*/
public int subtract(int operand1, int operand2)
throws IllegalArgumentException {
if (operand1 < 0 || operand2 < 0) {
throw new IllegalArgumentException(
"Expected: operands cannot be negative");
}
if ((operand1 < operand2) || (operand1 <= 5)) {
throw new IllegalArgumentException(
"Expected: operand1 > operand2 AND operand1 > 5");
}
this.state = operand1 - operand2;
return this.state;
}
}
The precondition is something that must hold whenever a method is called. The postcondition is something that the method guarantees when it returns. Consider the below client code:
SpecialSubtractor ss = new SpecialSubtractor();
System.out.println(ss.subtract(4, 5));
The above client code will fail with an exception because the client code didn’t give respect to the method contract by violating the precondition. Client code needs to satisfy the precondition. What about the below client code-snippet?
SpecialSubtractor ss = new SpecialSubtractor();
System.out.println(ss.subtract(6, 5));
In this case, client will get expected result because client code satisfies the precondition. When client code satisfies the precondition, it is the service implementer’s responsibilities to satisfy the postcondition.
If you observe carefully the implementation of the class SpecialSubtractor, you will see that all the methods (here only subtract) of the class SpecialSubtractor preserve the invariant (aka class invariant) that state of the SpecialSubtractor object is never negative.
Always consider invariants when you are designing method contract (precondition and postcondition).
Now consider the below subclass of SpecialSubtractor.
/**
* VerySpecialSubtractor is a subclass of SpecialSubtractor.
*
*/
class VerySpecialSubtractor extends SpecialSubtractor {
public VerySpecialSubtractor() {
super();
}
/**
* Subtracts two integers
* Precondition: Both operands are greater than zero
* and operand1 is greater than operand2 and operand1
* must be greater than 1.
* Postcondition: Returns the difference of right operand
* from left operand
* @param operand1 the left operand
* @param operand2 the right operand
* @throws IllegalArgumentException if either of the operands
* is negative or operand1 < operand2 or operand1 <= 1
*/
@Override
public int subtract(int operand1, int operand2)
throws IllegalArgumentException {
if (operand1 < 0 || operand2 < 0) {
throw new IllegalArgumentException(
"Expected: operands cannot be negative");
}
if ((operand1 < operand2) || (operand1 <= 1)) {
throw new IllegalArgumentException(
"Expected: operand1 > operand2 AND operand1 > 1");
}
this.state = operand1 - operand2;
return this.state;
}
}
Since any Java subclass is Java subtype, we can say that the subclass VerySpecialSubtractor is a subtype of the supertype SpecialSubtractor.
Now we will investigate if an object of VerySpecialSubtractor can be substituted where an object of SpecialSubtractor is expected.
From the above design, we can say that the invariant(s) of SpecialSubtractor is maintained in VerySpecialSubtractor. Moreover, there is no change in postcondition. But the subtype VerySpecialSubtractor has slightly changed the contract of subtract method of supertype SpecialSubtractor.
Supertype Precondition: Both operands are greater than zero and operand1 is greater than operand2 and operand1 must be greater than 5
Subtype Precondition: Both operands are greater than zero and operand1 is greater than operand2 and operand1 must be greater than 1
If you observe, you will see that the subtype precondition is weaker than the supertype precondition. By ‘weaker,’ we mean that the subtype precondition is less strict than the supertype. In subtype precondition, one expectation from client (operand1 must be greater than 5) is weakened (operand1 must be greater than 1). In this case, subtype method asks less from the client than the supertype method does. But subtype implementation needs to handle new inputs (2,3,4,5) that were previously excluded by the spec.
Consider the below client code based on supertype (SpecialSubtractor) specification.
The above client code will produce 3 as output.
Next we will replace the SpecialSubtractor object with an object of VerySpecialSubtractor type as shown below.
In this case, the client code will also produce 3 as output.
Important points…
- Instances of VerySpecialSubtractor subtype won’t surprise client code by requiring “more” than the supertype SpecialSubtractor. We have already checked that subtype method precondition is weaker than that of supertype. In subtype, precondition is slightly relaxed.
- Instances of VerySpecialSubtractor subtype won’t surprise client code by returning “less” than its supertype SpecialSubtractor. We have already checked that subtype method postcondition is equal to supertype method postcondition. But we can consider stronger postcondition (promising more) in subtype, if required.
- Also a VerySpecialSubtractor object can be substituted where an SpecialSubtractor object is expected
From the above observations, we can say that Java subtype VerySpecialSubtractor has a stronger specification than SpecialSubtractor. Hence, we can consider subtype VerySpecialSubtractor as true subtype. Remember, Java subtype is not same as true subtype. As a software designer, your goal is to design Java subtypes that are true subtypes.
That’s all for Part 2. We will continue the topic “Subtyping and Java” in Part 3 of the series Solid Code Design.
I have shared knowledge based on my understanding and experiences. If you find any significant errors or want to give me some feedback, feel free to contact me at maliksanjoykumar[@]gmail.com.
References
https://courses.cs.washington.edu/courses/cse331/10au/lectures/subtypingexamples.pdf
http://web.mit.edu/6.031/www/sp19/classes/07-designing-specs/
https://www.cs.mcgill.ca/~cs202/2011-01/lectures/maja/subtyping-and-inheritance-in-java.pdf
https://courses.cs.washington.edu/courses/cse331/16wi/L12/L12-Subtyping.pdf
https://cs.wellesley.edu/~cs251/f19/slides/oo-subtypes-4up.pdf
https://courses.cs.washington.edu/courses/cse331/10sp/lectures/lect06-subtyping.pdf
Sanjoy Kumar Malik is an experienced software architect and technologist. He is passionate about Cloud Computing, Software Architecture, and System Design. Apart from technology and software, he is an avid LinkedIn networker. You can join his 5.5+ lacs supporters on LinkedIn.