So the type is always a property of the class, albeit a hidden property. What if the type is the only property of the class? What if there are no other fields or methods in this class? One school of thought is very adamant that no class should ever be empty. The folks in that school will fight to the end that a design is wrong if it has an empty class.
Is the class empty?
No class is truly empty when we recognize that type is a property. That property has meaning when I can identify what an object instance is. Granted, client code that examines the type of objects is brittle code. We always stress that we should design to the abstraction, "code to the interface." That means we should aspire to create client code should be blind to the actual data types it is using and treat them all as instances of an abstraction:
You may recognize this example comes from the Just-in-Time-Shipping system (JITS) that is built in some of the nTier Training boot camps. The JITS class uses the abstraction of Parcel to store and manage Box and Letter instances. Our focus is on the class Letter: it does not have any properties other than the ones it inherits from Parcel, so it is an empty class.
Another choice to avoid that empty class is to only use the Parcel class with a "type" property that identifies if it is a box or a letter:
So this example is a truly brittle design. The methods of Parcel will have to check the parcel type in order to decide how to perform their tasks. When a new type is added, the methods of Parcel will have to be adjusted to support that new type. That violates Robert Martin's Open for Extension, Closed for Modification principle in his First Five Principles (Martin). True polymorphism is thrown out the window.
Is-a vs. has-a
The underlying problem in the previous example is that using a property creates a has-a relationship between a Parcel and the type it is. The relationship needs to be an is-a, not a has-a. A Box and Letter are similar, yet different. The only way to build an is-a relationship in our strongly typed languages is to extend a class or implement an interface.
So the next argument would be to extend Parcel for Box, but instead of using a Letter just use the superclass Parcel. That eliminates the empty class, right?
Unfortunately this just does not smell right. A Box is always Parcel. But a Parcel is not always a letter, it could be a Box! Using a Parcel to stand in when we are looking for a letter is not acceptable. In fact, we should go back to these diagrams and make the Parcel class abstract.
So the argument about an empty class that does not do anything is completely squashed by the is-a relationship trumping the has-a relationship! Once the is-a relationship is established the empty class sets up the design for polymorphism and allows for future adaptations. Perhaps a Letter will eventually have properties the other parcels do not.
But wait, we already use empty classes!
To really trump the empty-class argument it turns out there is prevalent example of empty classes that we already use: exceptions! If you are using C++, Java, C#, or any strongly-typed OOP language I dare you to show me an exception class where you extended the functionality of a basic exception.
public class ParcelValidationException extends RuntimeException {
@tab;public ParcelValidationException() {
@tab;@tab;super();
@tab;}
@tab;public ParcelValidationException(String message) {
@tab;@tab;super(message);
@tab;}
@tab;...
}
@tab;public ParcelValidationException() {
@tab;@tab;super();
@tab;}
@tab;public ParcelValidationException(String message) {
@tab;@tab;super(message);
@tab;}
@tab;...
}
It could happen, but the real point of creating new exception classes is to encapsulate a new type. The catch blocks following a try block can identify the type of exception that is thrown and adjust what happens based on that type:
try {
@tab;// Here is some code that throws exceptions. }
catch (ParcelValidationException e) {
@tab;// Do something for validation problems.
}
catch (Exception e) {
@tab;// Do something for all other problems.
}
@tab;// Here is some code that throws exceptions. }
catch (ParcelValidationException e) {
@tab;// Do something for validation problems.
}
catch (Exception e) {
@tab;// Do something for all other problems.
}
And there you have it: encapsulating type is very important in the design of applications. We use it with exceptions. Sometimes we will use what appear to be empty classes in our own code just to enforce the application of an is-a relationship!
See the references page.
References
See the references page.
No comments:
Post a Comment