3
\$\begingroup\$

I have a simple two-class hierarchy to represent U.S. ZIP (12345) and ZIP+4 (12345-1234) codes. To allow clients to allow both types for a field/parameter or restrict it to one type or the other, the specific types inherit from a common generic ZipCode interface.

Update: A ZIP code is five digits. A ZIP+4 code consists of the primary five-digit ZIP code plus a four-digit plus-4 (+4) code. You can create a ZIP+4 from a regular ZIP and get the primary ZIP from a ZIP+4. Thus, the interface which the two classes implement knows about the two classes, and they know about each other.

interface ZipCode
    boolean isPlusFour()
    ZipPlusFour plusFour(String code)
    Zip primary()
    boolean hasSamePrimary(ZipCode other)

class Zip implements ZipCode
    String code

class ZipPlusFour implements ZipCode
    Zip primary
    String plusFourCode

Introducing Null Object Pattern

To avoid duplicating code that checks for null values throughout the application, I would like to introduce the Null Object pattern. However, I'm afraid the only way to do so that supports the features above is to add three new classes instead of just one:

class NullZipCode implements ZipCode
class NullZip extends Zip
class NullZipPlusFour extends ZipPlusFour

Worse, since the last two extend concrete classes they will need to pass special values to their superclass that will pass validation but not block the possibility of using real values. For example, NullZip would call super("00000").

Here are my main questions, though please don't hesitate to throw out any suggestions you have.

  1. Is there a way to solve this with just one new class to cover all bases?
  2. Is it worth extracting interfaces from Zip and ZipPlusFour so that NullZip won't extend the concrete Zip implementation (same for ZipPlusFour)?

    ZipCode
      Zip
        NullZip
        RealZip
    
  3. Another option is to forego separate classes altogether and check for a special value in Zip and ZipPlusFour to signify a missing ZIP code.

    class Zip
        boolean isNone() {
            return code.equals("00000");
        }
    
        boolean hasSamePrimary(ZipCode other) {
            if (isNone() || other.isNone())
                return false;
            else
                return code.equals(other.primary().code);
        }
    

    At least the complicated checks are encapsulated in these classes which is the whole point of introducing the pattern. The downside is that this logic can be more fragile than inheritance. Is this a good trade-off?

\$\endgroup\$
  • \$\begingroup\$ I'm not sure I understand your interface. Why does the interface know about the concrete objects that implement it, and have functions that return both? Who is using this that they'll be able to differentiate between the two classes - other developers? This might be a place to use composition instead of inheritance. Is there ever a scenario where you would want to allow a base zipcode and not allow a Zip+4? \$\endgroup\$ – Bobson Apr 8 '13 at 20:02
  • \$\begingroup\$ @Bobson - ZipPlusFour is composed of a Zip (the primary) and a four-digit +4 code. You can get a ZIP+4 from a ZIP by adding a +4 code, and you can get the primary ZIP from a full ZIP+4. I'll explain this more in the question. \$\endgroup\$ – David Harkness Apr 8 '13 at 20:36
  • 1
    \$\begingroup\$ If you can always transform one into another, then why have them be separate in the first place? Just have one class, and provide a HasPlusFour() function to differentiate them, and a ToString() which returns the appropriate string representation depending on whether the +4 is there or not. \$\endgroup\$ – Bobson Apr 8 '13 at 20:53
  • \$\begingroup\$ @Bobson - I want clients to be able to declare via the parameter type that they accept or return a single form. A Customer has a Zip since we do not store the +4 if provided when parsing, but Dealer has a ZipCode because it may be either a ZIP or ZIP+4 code. Phone, on the other hand, accepts an optional extension using a single class as you describe. \$\endgroup\$ – David Harkness Apr 8 '13 at 21:01
  • \$\begingroup\$ @DavidHarkness where are you using boolean isPlusFour()? Can we see some example code? \$\endgroup\$ – abuzittin gillifirca Apr 8 '13 at 21:42
2
\$\begingroup\$

This is a preliminary answer:

Assume for the sake of this question that going that route isn't practical.

Assuming this is about some group of similar value objects. Everything is meaningful in a context. So we need to determine what are the use cases for this object. Suppose we have the following use cases (I'm trying to guess how you could end up with the provided interfaces.):

  1. We need to display a ZipCode on screen
  2. We need to print ZipCode on envelopes
  3. We also need to lookup some external service for demographic data using the 5-digit ZIP as a key.

Simplest thing that could work would then be something like:

interface ZipCode
    Zip primary() // for service lookup
    String getScreenRepresentation()
    String getPrintRepresentation()
    // you may one to use Visitor pattern 
    // if you have much more different formats

Now we may try to design a Null Object that could work for all these situations. for ZIP and ZIP+4 their implementations are straight forward. For NullZipCode we can have getScreenRepresentation to return "No ZIP provided" and getPrintRepresentation to return empty string. You definitely would not want "00000" or some other invalid value on the label. (Unless in the improbable case that US postal service requires people put it to designate unknown ZIP, but you get what I mean)

My criticism of the below code

interface ZipCode
    boolean isPlusFour()
    ZipPlusFour plusFour(String code)
    Zip primary()
    boolean hasSamePrimary(ZipCode other)

class Zip implements ZipCode
    String code

class ZipPlusFour implements ZipCode
    Zip primary
    String plusFourCode

isPlusFour is the back door where all the null checks sneak back in.

plusFour(code) does not belong in the interface. It is a factory method. One sign it does not belong there is Zip and ZipPlusFour should not be able to provide different versions.

hasSamePrimary is unnecessary. Value objects should provide their equality, comparison, hash, display string etc. by overriding equals, hashCode etc in Java, (or deriving Show, Eq in Haskell and so on). By value object semantics; zipCode1.hasSamePrimary(zipCode2) should be equivalent to zipCode1.primary().equals(zipCode2.primary()), it is not an overridable behavior (Closed for modification principle). If it is there for brevity, it can be placed in an abstract base class or as a static method in a Utility class (or extension method if you are using C#, etc.)

What you should do for Dealer class cannot be a simple interface, if it is to be a value object also.

class ZipOrZipPlusFour implements ZipCode
     //fields depend on what sort of persistence you use
     // factory methods:
     newNullZipCode()
     newZip(String code)
     newZipPlusFour(Zip primary, String ext)

class Dealer
    ZipOrZipPlusFour zipCode
\$\endgroup\$
0
\$\begingroup\$

I still don't really understand why they have to be separate classes, but here's my take on it:

interface ZipCode
  boolean isPlusFour()
  boolean hasSamePrimary(ZipCode other)
  String code
  String display

interface ZipCodePlusFour extends ZipCode
  String plusFourCode
  override String display
  boolean hasSamePlusFour(ZipCodePlusFour other)

class ZipCode implements ZipCode
  // Nothing here that isn't in the interface
class ZipPlusFour extends ZipCode implements ZipCodePlusFour
  // Nothing here that isn't in the interface

class NullZip implements ZipCode
  // Null responses for everything
class NullZipPlusFour extends NullZip implements ZipCodePlusFour
  // Null responses for everything that NullZip doesn't already do.

Everything in your code should either require the ZipCode interface or the ZipCodePlusFour interface. If the former, it can accept any of the four classes. If the latter, it requires the plus four bit. Since a plus four can always be downgraded to a pure zipcode, this set of inheritance will let you pass a +4 wherever you are looking for a ZipCode.

Also, by always requiring the interface, rather than the concrete type, you can trivially implement the null object pattern you asked about.

\$\endgroup\$
  • \$\begingroup\$ They are separate so I can specify that one parameter allows a +4 code while another does not. When both are acceptable, use the interface. Certainly I could use a single class and perform validation checks as you would with nonNegativeLength. However, compile-time checks are les prone to error than runtime validation. \$\endgroup\$ – David Harkness Feb 6 '14 at 5:07
  • \$\begingroup\$ @DavidHarkness - It's been a long time, but I think what I meant by "separate" is "not inheriting". Having the +4 version inherit from the base one still allows you to have compile-time checking, but allows you to reuse some functionality. \$\endgroup\$ – Bobson Feb 6 '14 at 14:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.