Home > Software engineering >  How do I represent disjoint union types in Java?
How do I represent disjoint union types in Java?

Time:11-16

Let's say I have a data definition in a system:

A PaymentMethod is one of Cash or CreditCard(String accountNum).

One way of operating on this (disjoint union) data is with the visitor pattern:

interface IPaymentMethod {

    <R> R visit(IPaymentMethod.IVisitor<R> visitor);

    interface IVisitor<R> {

        R visitCash();

        R visitCreditCard(String accountNum);
    }
}

class Cash implements IPaymentMethod {

    <R> R visit(IPaymentMethod.IVisitor<R> visitor) {
        return visitor.visitCash();
    }
}

class CreditCard implements IPaymentMethod {

    String accountNum;

    // constructor here

    <R> R visit(IPaymentMethod.IVisitor<R> visitor) {
        return visitor.visitCreditCard(this.accountNum);
    }
}

Aside from the verbosity in both implementing and using visitors, it's too open to extension: if I expect consumers of my library to produce IPaymentMethods, I only expect Cash or CreditCards to be returned. However, they might return their own implementation, which wouldn't make any sense. Is there another pattern here that can better represent this data and can guarantee that I'm only ever dealing with Cash and CreditCards? (Of course, if it weren't for String accountNum, an enum would be great.)

CodePudding user response:

/**
 * A PaymentMethod is one of Cash or CreditCard(String accountNum).
 */
public abstract class PaymentMethod {

    private PaymentMethod() {}

    /**
     * Creates a Cash payment method.
     */
    public static PaymentMethod makeCash() {
        return new Cash();
    }

    /**
     * Creates a CreditCard payment method with an account number.
     */
    public static PaymentMethod makeCreditCard(String accountNum) {
        return new CreditCard(accountNum);
    }

    public abstract <R> R visit(IVisitor<R> visitor);

    public interface IVisitor<R> {

        R visitCash();

        R visitCreditCard(String accountNum);
    }

    private static class Cash extends PaymentMethod {

        @Override
        public <R> R visit(IVisitor<R> visitor) {
            return visitor.visitCash();
        }
    }

    private static class CreditCard extends PaymentMethod {

        private final String accountNum;

        private CreditCard(String accountNum) {
            this.accountNum = accountNum;
        }

        @Override
        public <R> R visit(IVisitor<R> visitor) {
            return visitor.visitCreditCard(this.accountNum);
        }
    }
}

This solution keeps the visitor pattern for performing operations on each subclass of PaymentMethod, but uses the factory pattern for the creation of those subclasses. The private PaymentMethod constructor prevents outside extension of the class, thus closing the union. Access modifiers could be relaxed (i.e. with a package-private constructor) to allow for extension only within a package and to permit extracting out the Cash and CreditCard classes.

It's verbose, yes, but it seems to be the best solution for Java <17 (see David Conrad's comment about sealed classes).

  • Related