DEV Community

Cover image for Turn Your Spaghetti Code into Functions - Part 3
Gunnar Gissel
Gunnar Gissel

Posted on • Updated on • Originally published at gunnargissel.com

Turn Your Spaghetti Code into Functions - Part 3

Orignally posted at www.gunnargissel.com

Picking up where we left off in Part II, let's set the stage for using a validator. We left our example elephant in a cool convertible, but let's upgrade his ride to a rocket ship. We'll start like all good rocket scientists - with a round of cleaning!

After mise en place, we will be ready to implement a Validator and Result, which will create a single, standard, expandable format for business rules and an api for consuming results from business rule queries. No more guessing what a rule-compliant call looks like! No more stringing together crystal towers of nested boolean logic!

Cleanup

There are a couple places where we used .and() to combine predicates in an if block. Replace those with an independent predicate, so all if blocks refer to only one predicate

static final Predicate<WidgetTransfer> suffientAmount = trans -> trans.getTransferer().getAccount(trans.getFromAccount()).getBalance().compareTo(trans.getAmount()) < 0;
    static final Predicate<WidgetTransfer> isPartner = trans -> trans.getTransferTypeCode().equals("200");
    static final Predicate<WidgetTransfer> isFriendsAndFamily = trans -> trans.getTransferTypeCode().equals("710");
    static final Predicate<WidgetTransfer> isFriendAndFamilyDiscountLegal = trans -> trans.getAreaCode().matches("574|213|363|510");
    static final Predicate<WidgetTransfer> isPartneringArea = trans -> trans.getAreaCode().matches("907|412|213");
    static final Predicate<WidgetTransfer> isDirigibleForbiddenArea = trans -> trans.getAreaCode().matches("213");
    static final Predicate<WidgetTransfer> isDirigibleCategory = trans -> trans.getTransferer().getCategory().equals("D");
    static final Predicate<WidgetTransfer> isInternal = trans -> trans.getTypeCode().equals("I");
    static final Predicate<WidgetTransfer> isBlockSize = trans -> isBlockSize(trans);
    static final Predicate<WidgetTransfer> isTotalOverCap = trans -> isTotalOverCap(trans);

    static final Predicate<WidgetTransfer> parterTransferReqs = trans -> isPartner.and(isPartneringArea.negate()).test(trans);
    static final Predicate<WidgetTransfer> dirigibleTransferReqs = trans -> isPartner.and(isDirigibleForbiddenArea.negate()).and(isDirigibleCategory).test(trans);
    static final Predicate<WidgetTransfer> friendsAndFamilyReqs = trans -> isFriendsAndFamily.and(isFriendAndFamilyDiscountLegal.negate()).test(trans);
    static final Predicate<WidgetTransfer> internalBlockReqs = trans -> isInternal.and(isBlockSize).test(trans);
    static final Predicate<WidgetTransfer> internalTotalCapReqs = trans -> isInternal.and(isTotalOverCap).test(trans);

    public static final String checkWidgetTransfer(WidgetTransfer transfer) {
        String businessRuleErrors = "";

        if (suffientAmount.test(transfer)) {
            businessRuleErrors += "Insufficient balance to transfer ; ";
        }

        if (parterTransferReqs.test(transfer)) {
            businessRuleErrors += "This area is not a transfer eligible area. ; ";
        }

        if (dirigibleTransferReqs.test(transfer)) {
            businessRuleErrors += "D Category Transferer can only be transferred in transfer area 213. ; ";
        }

        if (friendsAndFamilyReqs.test(transfer)) {
            businessRuleErrors += "This area is not a transfer eligible area. ; ";
        }

        if (internalBlockReqs.test(transfer)) {
            businessRuleErrors += "Amount is too small for I type transfer. ; ";
        }

        if (internalTotalCapReqs.test(transfer)) {
            businessRuleErrors += "This transfer is too large. ; ";
        }

        return businessRuleErrors;
    }
Enter fullscreen mode Exit fullscreen mode

The above code has a lot of boiler plate - but it's boiler plate that is invisible to the average Java dev. All those if blocks are boiler plate. You have to type them again, and again. What if I told you, you could use a validator and clean up all your business rules? Think of the miles of conditionals in your business code, and imagine each reduced to a single composable function call.

Boilerplate Removal

literal boiler plat

Another problem is that checkWidgetTransfer returns a string. This pushes the responsibility for determining if an error has occured onto the calling method. All checkWidgetTransfer callers need a section that looks like this:

String result = checkWidgetTransfer(transfer);
if(null == result || 0 == result.size()) {
    //continue
}else{
    handleError(result);
}
Enter fullscreen mode Exit fullscreen mode

Multiply this by every bizarre process that Bob in accounting, Carol in sales uses and Duane in HQ uses. It can get.... big.

We can save on typing, share conditionals and share business logic by using the Validator and Result technique. Here's what it looks like from the caller's perspective:

checkWidgetTransfer(transfer).onError(err -> handleError(err));
//continue
Enter fullscreen mode Exit fullscreen mode

This provides an api for the caller that indicates what an error condition is. Callers no longer have to guess that an empty string is a pass, and a non-empty string is a fail. The api provides a place to put error handling, which can take an existing function, or have an on-the-fly lambda in place. Sweet!

You still have to write up Bob, Carol and Duane's favorite workflow. but things are compact and you don't have to go down many twisty branches, each more a like than the last.

Now with a Validator

rocket blasting off

What if you didn't have to write any if/then/else statements? What if you you only had to write the logic, and something else would handle stringing that logic together. A validator can make that possible.

Here's what the implementation of checkWidgetTransfer looks like, using a validator:

public static final Result<WidgetTransfer> checkWidgetTransfer(WidgetTransfer transfer) {
    Validator<WidgetTransfer> widgetTransferValidator = new Validator();
    widgetTransferValidator.addRule(suffientAmount, "Insufficient balance to transfer");
    widgetTransferValidator.addRule(parterTransferReqs, "This area is not a transfer eligible area");
    widgetTransferValidator.addRule(dirigibleTransferReqs, "D Category Transferer can only be transferred in transfer area 213");
    widgetTransferValidator.addRule(friendsAndFamilyReqs, "This area is not an eligible area");
    widgetTransferValidator.addRule(internalBlockReqs, "Amount is too small for I type transfer");
    widgetTransferValidator.addRule(internalTotalCapReqs, "This transfer is too large");
    return widgetTransferValidator.validate(transfer); 
}
Enter fullscreen mode Exit fullscreen mode

The validator can take as many rules as needed, and each rule gets a name and a matching message. The validator ensures every rule is applied to the transfer, and a Result is returned, containing either the error messages or the transfer.

Implementation Details

I implemented a Validator as a HashMap of functions to error strings. The validate method tests each function, and if the test is true, collects the matching message in the Result.

I implemented Result as an Either. Callers have the option of getting a boolean hasErrors or passing in a Consumer to onError. The important thing about a Result is that it is never null and it guides the developer to the correct method of handling an error.

Wrap up

With just a tiny push, we helped our elephant reach orbit. We've gone from bog standard spaghetti business logic to pure functions embodying composable business logic. Thank you for coming along for ride. I welcome comments or feedback at gunnar@gunnargissel.com, or @monknomo on Twitter

elephant in space, happy they have cleaned up code

Sign up for my email list to get notifcations about updates in continuing series, and a monthly digest of interesting programming and tech leadership articles

Credits

Thank you Oliver Dodd for the elephant

Thank you NASA, ESA, N. Smith (U. California, Berkeley) et al., and The Hubble Heritage Team (STScI/AURA) for the Carina Nebula

Photo of the shuttle Endeavour by Mr. Littlehand

Boiler Plate from Les Chatfield

Blastoff by Matthew Lancaster

Top comments (2)

Collapse
 
mtbsickrider profile image
Enrique Jose Padilla

I really liked your 3 part series. Thank you for sharing. I'm not a java developer myself, but I like how java is getting onto this functional hype and the code seems a lot cleaner.

Also, from a design patterns perspective you could of used a decorator patterns with all the validations?

To encapsulate change, would you move the creation of checkWidgetTransfer to its own file so if you have to add a predicate your not changing it in the main code?

I reiterate, this is such a great example of Clean Code.

Collapse
 
monknomo profile image
Gunnar Gissel

You certainly could use the decorator pattern with the validation. I'm not sure how it would play with the functional approach, but it would be interesting to take for a test drive and see.

Another pattern I thought of trying out is the chained filter pattern in the Validator class. I think that could give some richer possibilities for linking business rules together.

If I were doing this for real, I would have the related functions all in one empty class, and the methods like checkWidgetTransfer in a different class, yes. I've found functional Java tends to collect a couple classes that are empty except for functions, which felt weird at first, but where else are you going to put them?