Not Another Opinion

Sometimes we have some data and we need to draw some conclusions about it.

E.g.

const customer = {
   tier: GOLD,
   points: 12345,
};

... // meanwhile in the business logic

// shall we give the customer a discount?
if (customer.tier === GOLD && points > 10000) {
   giveDiscount();
}

In the above, the function deciding to give a discount is calculating whether the customer is eligible for a discount. It’s commented, but we should probably create an isEligibleForDiscount function.

function isEligibleForDiscount(customer) {
    return customer.tier === GOLD && points > 10000;
}

So, where does this function live?

We could imagine that the function would be a member of the object itself. That would be an object-oriented solution.

if (customer.isEligibleForDiscount()) {
   giveDiscount();
}

Or we could imagine that the function belongs in a business logic module (as essentially shown in the first example).

So Where Does It Go?

The answer is, of course, well – it depends.

We could make a strong argument for any implementation. But here’s a rule of thumb:

An object should have a strong opinion on its internal state, and weak opinions on how to project that state into other forms.

For example:

const shape = new Square();
if (shape.isRegular()) {
   // a shape fundamentally IS or IS NOT
   // of equal proportions
}

Conversely

if (shape.isVisibleToUser()) {
   // wait - each shape is concerned
   // about how it's being rendered?
}

// or

if (shape.isCustomerFavourite()) {
   // mixing concerns between customer logic
   // and shape logic
}

Using the open-closed principle, it would be problematic for a data type to change every time some new business rules come along. Sure, sometimes we need a broader model, but is the interpretation of the model ALWAYS the concern of the class representing that model?

How To Apply

If a function for an object is fundamentally about mapping that object’s hidden internal state into some meaningful interface that’s about the object itself, then of course such functions should be attached to the object.

Conversely, if the primary “concern” is some external opinion, then the logic probably should consume the outward representation of the object, and produce the results related to that opinion.

In our example, we could apply seven different discount schemes to our customer and still not need the customer class to change.

Extension Functions to the Rescue

I’m in two minds about whether extension functions are a good thing. I’m going to say they are.

If we imagine our isEligibleForDiscount function is actually an external opinion on Customer, then we might write it as a static function somewhere. This makes calling it look something like this:

boolean isEligible = isEligibleForDiscount(customer);

This reads, in natural language, as are they eligible for discount, this customer. This is a strange word order. It’s better to have our code sentences in a meaningful order.

Extension functions allow us to bind, for the purposes of calling, a function to the type it needs. We essentially say to the compiler that we’re happy for the code to call the function in the same way as a member of the object, even though it’s external.

boolean isEligible = customer.isEligibleForDiscount();

This clearly reads better.

Where I have doubts here is around the expectations of a reader looking at the second form. In the first form, it’s clear that the function is external to the Customer class. In the second, we’ll end up discovering that later.

However, since IDEs are really rather good at helping us navigate our code, I’m happy to admit that the improvement in word order is probably worth the confusion.

That said, if we’re exploring the code via GitHub during a code review, or source-code exploration of an external module, then there could be some confusion with this version.

Published on Java Code Geeks with permission by Ashley Frieze, partner at our JCG program. See the original article here: Not Another Opinion

Opinions expressed by Java Code Geeks contributors are their own.

Exit mobile version