Complexity-Free Factory Pattern

Factory pattern is one of the most used patterns in software architectural design. But once used, it usually comes with unwanted complexity. The reason for this, is the use of many conditional statements like if or switch.

In our everyday use of factories, when a parameter is passed to the factory’s creator method, implementation goes through n numbers of conditional blocks to figure out what to return. This approach not just increases the complexity but also causes a lack of readability. To prevent this situation, registering a reference type variable that holds the reference to the class into a map will save the code from any type of complexity such as cognitive or cyclomatic…

Here there are two examples written in java and python. In these examples we have a payment system. You can pay by credit card or debit card. In both cases the payment methods are registered into a map and resolved when needed – lazy initialization – without conditional statements.

Here is the java version. You can check out PaymentFactory.java line 12, 13 and 14 to see this kind of usage. 

import java.util.function.Supplier;

public class PaymentProcessor {

    public static void main(String[] args){
        Supplier<Card> creditCard = PaymentFactory.getInstance().get(PaymentFactory.PaymentType.CREDIT_CARD);
        Supplier<Card> debitCard = PaymentFactory.getInstance().get(PaymentFactory.PaymentType.DEBIT_CARD);

        creditCard.get().charge(10.0);
        debitCard.get().charge(10.0);
        creditCard.get().charge(14.0);
        debitCard.get().charge(12.0);
        creditCard.get().charge(11.0);
        debitCard.get().charge(9.0);
    }
}
import java.util.EnumMap;
import java.util.Map;
import java.util.function.Supplier;

class PaymentFactory {

    private Map<PaymentType, Supplier<Card>> paymentList;
    private static PaymentFactory instance;

    private PaymentFactory()
    {
        paymentList = new EnumMap<>(PaymentType.class);
        paymentList.put(PaymentType.CREDIT_CARD, CreditCard::new);
        paymentList.put(PaymentType.DEBIT_CARD, DebitCard::new);
    }

    static PaymentFactory getInstance()
    {
        if (instance == null) {
            instance = new PaymentFactory();
        }

        return instance;
    }

    Supplier<Card> get(PaymentType type)
    {
        return paymentList.get(type);
    }

    enum PaymentType {
        CREDIT_CARD,
        DEBIT_CARD
    }
}
public class DebitCard implements Card {
    @Override
    public void charge(double amount) {
        System.out.println("Debit card charged " + amount + "$");
    }
}
public class CreditCard implements Card {
    @Override
    public void charge(double amount) {
        System.out.println("Credit card charged " + amount + "$");
    }
}
public interface Card {
    void charge(double amount);
}


from enum import Enum


class PaymentType(Enum):
    credit_card = 1
    debit_card = 2


class Card:
    def charge(self, amount):
        raise NotImplementedError


class CreditCard(Card):
    def charge(self, amount):
        print("Credit card charged {}$".format(amount))


class DebitCard(Card):
    def charge(self, amount):
        print("Debit card charged {}$".format(amount))


class PaymentFactory(object):
    payment_list = {
        PaymentType.credit_card: CreditCard,
        PaymentType.debit_card: DebitCard
    }

    @staticmethod
    def get(payment_type):
        assert payment_type in PaymentFactory.payment_list, "Payment type not found in factory."
        return PaymentFactory.payment_list[payment_type]


def main():
    credit_card = PaymentFactory.get(PaymentType.credit_card)()
    debit_card = PaymentFactory.get(PaymentType.debit_card)()

    credit_card.charge(10.0)
    debit_card.charge(10.0)
    credit_card.charge(14.0)
    debit_card.charge(12.0)
    credit_card.charge(11.0)
    debit_card.charge(9.0)


if __name__ == '__main__':
    main()

 

In addition to everything I described above; the main reason I wanted to write about this subject is that when you are new to design patterns and in this specific case, factory pattern, you will probably google it and see tons of examples. In most examples, the factory will use conditionals as I described at the beginning. Then one day, when you are brave enough, you will run a code quality analyzer on your code and it will raise alarms telling you to remove those conditionals. Then maybe(!) you will come to the same conclusion by yourself but you will lose time because you didn’t do it this way in the first place. So I hope someone someday reads this and saves a little bit of time. 

Technology Stack

  • OpenJDK Runtime Environment (build 10.0.2+13-Ubuntu-1ubuntu0.18.04.4)
  • Python 3.6.7

Acknowledgements

Berk Kibarer for writing the java version of the code and for helping to write the article.