loading...
Cover image for Practical Coding Patterns For Boss Developers #1: Special Case

Practical Coding Patterns For Boss Developers #1: Special Case

jamesmh profile image James Hickey Originally published at blog.jamesmichaelhickey.com Updated on ・6 min read

Originally published on my blog

Design patterns are necessary (in my opinion) for you to start gaining an advanced understanding and ability to design and refactor software.

These patterns also give developers a common language to speak about certain code structures.

E.g. If you are facing a certain problem, and I say "Try the strategy pattern..." then I don't have to spend an hour explaining what you should do.

You can go look it up or, if you already know the pattern, go implement it!

Not Your Typical Design Patterns

Everyone is talking about the design patterns found in the famous gang of four book Design Patterns:

  • Strategy
  • Builder
  • Factory
  • Adapter
  • etc.

But, there are way more software design patterns not found in this book which I've found super helpful.

Some of them I've learned from Martin Fowler, others from Domain Driven Design, and other sources.

I figured I'd start to catalogue some of these by sharing them!

As a general rule, I'm going to use TypeScript for my code samples.

A Close Relative...

The pattern I want to start with is a close relative to the null object pattern that I've tweeted about before:

The null object pattern is a way of avoiding issues around null states (like all the extra null checking you need to do 😒).

You create a specialized version of a class to represent it in a "null" state, which exposes the same API or interface as the base object.

In other words, it's kinda like a stub.

Here's an example off the top of my head, in TypeScript:

// Concrete Order
class Order {
    private _items: any[];

    constructor(items: any[]) {
        this._items = items;
    }

    public placeOrder() {
        // API call or whatever...
    }
}

// Null "version" of the Order
class NullOrder extends Order {
    constructor() {
        super([]);
    }

    public placeOrder() {
        // We just do nothing!
        // No errors are thrown!
    }
}

// Usage:
const orders: Order[] = [
    new Order(['fancy pants', 't-shirt']), new NullOrder()
];

for (const order of orders) {
    // This won't throw on nulls since we've
    // used the null object pattern.
    order.placeOrder();
}

An Example Scenario

Imagine we had a scheduled background process that fetches multiple orders and tries to place them.

Like Amazon, we might have a complex process around placing orders that isn't as linear as we might think it would be.

We might want a buffer period between the time when you click "place order" and when the order is really placed. This will make cancelling an order an easy process (it avoids having to remove credit card charges, etc.)

Note: I might write about this pattern later. 😉

In this scenario, we might be trying to process orders that have already been changed:

  • Cancelled order
  • Payment declined
  • The payment gateway is not responding so we need to wait...
  • etc.

The null object pattern can help with this kind of scenario.

But even better, when you have multiple versions of these kinds of "special" cases, the special case pattern is here to save the day!

Special Case Pattern

The special case pattern is essentially the same in implementation, but instead of modelling specific "null" states, we can use the same technique to model any special or non-typical cases.

Using the code example above, instead of having "null" versions of our Order, by using the special case pattern we can implement more semantic and targeted variants of our Order class:

class IncompleteOrder extends Order {
    constructor() {
        super([]);
    }

    public placeOrder() {
        // Do nothing...
    }
}

class CorruptedOrder extends Order {
    constructor() {
        super([]);
    }

    public placeOrder() {
        // Try to fix the corruption?
    }
}

class OrderOnFraudulentAccount extends Order {
    constructor() {
        super([]);
    }

    public placeOrder() {
        // Notify the fraud dept.?
    }
}

As you can see, this pattern helps us to be very specific in how we model special cases in our code.

Benefits

Some benefits are:

  • Avoiding null exception issues
  • Having classes with more targeted single responsibilities
  • Ability to handle special cases without our Order class blowing up with tons of logic
  • The semantics of the class names makes our code much more understandable to read
  • Introducing new cases involves creating new classes vs. changing existing classes (see open-closed principle)

Refactoring To The Special Case Pattern

So, when should you use the pattern?

Constructor

You might consider this pattern whenever you see this type of logic in a class' constructor:

constructor() {
    if(this.fraudWasDetected()) {
        this._fraudDetected = true;
    } else {
        this.fraudWasDetected = false;
    }
}

Note: The refactoring for this will begin in the Oops, Too Many Responsibilities section below.

Outside "Asking"

When you see something like the following, then you may want to consider the special case pattern:

const order = getOrderFromDB();

if(order.fraudWasDetected()) {
    order.doFraudDetectedStuff();
} else if(!order.hasItems()) {
    order.placeOrder();
} 
// ... and more ...

Focusing on this example, why is this potentially a "code smell"?

At face value, this type of logic should be "baked into" the Order class(es).

Whoever is using the order shouldn't have to know about this logic. This is all order specific logic. It shouldn't be "asking" the Order for details and then deciding how to use the Order.

For more, see the tell don't ask principle - which, most times, does indicate that your logic might be better suited inside the object you are using.

The first fix then is to move this logic to the inside of the Order class:

class Order {

    public placeOrder() {
        if(this._fraudWasDetected()) {
            this._doFraudDetectedStuff();
        } else if(!this._hasItems()) {
            this._placeOrder();
        } 
    }
}

Oops, Too Many Responsibilities!

But, now we run into some issues: we are dealing with different responsibilities (placing orders, fraud detection, item corruption, etc.) in one class! 😓

Note: What follows can be applied to the constructor refactor too.

Special case pattern to the rescue!

// Note: I'm just highlighting the main parts, this won't compile 😋
class CorruptedOrder extends Order {

    public placeOrder() {
        this._fixCorruptedItems();
        super.placeOrder();
    }
}

class OrderOnFraudulentAccount extends Order {

    public placeOrder() {
        this._notifyFraudDepartment();
    }
}

class IncompleteOrder extends Order {

    public placeOrder() {
        // Do nothing...
    }
}

Combining Patterns

Great! But how can we instantiate these different classes?

The beauty of design patterns is that they usually end up working together. In this case, we could use the factory pattern:

class OrderFactory {

    public static makeOrder(accountId: number, items: any[]): Order {
        if (this._fraudWasDetected(accountId)) {
            return new OrderOnFraudulentAccount();
        } else if (items === null || items.length === 0) {
            return new EmptyOrder();
        }
        // and so on....
    }
}

Note: This is a pretty clear example of when you would really want to use the factory pattern - which I do find can be easily over-explained. Hopefully, this helps you to see why you would want to use a factory in the first place.

We have split our Order class into a few more special classes that can each handle one special case.

Even if the logic for, let's say, processing a suspected fraudulent account/order is very complex, we have isolated that complexity into a targeted and specialized class.

As far as the consumers of the Order class(es) go - they have no idea what's going on under the covers! They can simply call order.placeOrder() and let each class handle its own special case.

Resources

Thoughts?

Have you ever encountered the special case pattern? Or perhaps any of the others I've mentioned?

Keep In Touch

Don't forget to connect with me on:

You can also find me at my web site www.jamesmichaelhickey.com.

Navigating Your Software Development Career Newsletter

An e-mail newsletter that will help you level-up in your career as a software developer! Ever wonder:

✔ What are the general stages of a software developer?
✔ How do I know which stage I'm at? How do I get to the next stage?
✔ What is a tech leader and how do I become one?
✔ Is there someone willing to walk with me and answer my questions?

Sound interesting? Join the community!

Discussion

pic
Editor guide
Collapse
qcgm1978 profile image
Youth

I want to ask whether it's complicated. And the next one is it conflicts with early failure principle. In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure.

Collapse
jamesmh profile image
James Hickey Author

I'm not quite sure what your first question is?

For the second question, if you are talking about trying to fail fast on the "fraud detection" (I think that's perhaps what you're trying to say), then it still applies here.

Within the long running process,fraud will be detected early on (before any payments are made etc.). However, it's not a good idea to do that type of check "right away" - like during the HTTP request from the user since making that check would involve making some external API checks (most likely) and take a while.

We wouldn't want our user sitting there, watching a loading spinner on his screen, for a few minutes 😂

There's always trade-offs. By making a longer-running process like this then the trade-off is that the user doesn't have to be directly involved while waiting, but it might take a little bit longer for the entire process to complete.

Collapse
qcgm1978 profile image
Youth

I think trade-off is a great word to describe the conditions. We should delay the detection on the beginning of development phases and expect it crash as soon as possible. And complete the program to satisfy the code logic and business logic step by step. At last we should consider to set up the closed cycle in fixing bugs phase.

So I think we should change our thinking paradigms in different development phases.

Collapse
shoupn profile image
Nick Shoup

Thank for sharing this James. I've seen this and used this pattern before, but didn't really have a name for it. Thanks for the clarification. I've always thought about it as a part of implementing the factory pattern.

Collapse
jamesmh profile image
James Hickey Author

You're welcome 🤜🤛

I agree, there just seems to be a natural connection between this pattern and the factory pattern since you need something to decide how to instantiate the classes.

Usually, the pattern is for avoiding null issues, but I really like the idea of being intentional about splitting classes up more semantically instead of having internal variables that hint that the object is in a very specific or special state.

I've used the pattern when building viewModels too, for example. Each variant can each format themselves totally differently 👍

Collapse
dbanty profile image
Dylan Anthony

This feels a lot like the state pattern to me. How is it different?

Collapse
jamesmh profile image
James Hickey Author

Seems very similar, except the special case is usually used for replacing null values as a starting point.

When used to expose polymophic behaviors, the state pattern uses a "container" class to hold the state using composition, but the special case classes use sub-typing (thus the need for a factory of some kind).

Collapse
diomalta profile image
Diego Malta ®

Thanks for the clarification, i'm still young in design patterns, but I hope to get better with articles and tips like this.

Collapse
jamesmh profile image
James Hickey Author

You're welcome! 💯