2
\$\begingroup\$

I have a set of if/else statements that trigger certain actions. As a first step, I was thinking about moving the logic in each if statement to be its own function, but are there any recommendations on what design patterns can help clean this code up?

I guess I am trying to see if there is a cleaner way to determine what the next action/command should be based on a set of criteria. I see people recommend the Command pattern, but how does one abstract the logic in the if-statement to begin with?

function WaterRetriever(largeBucketSize, smallBucketSize, waterSize) {
    var buckets = {};

    if (waterSize > largeBucketSize) {
        throw new Error("The buckets are not large enough.");
    }

    buckets.firstContainer = new Bucket(largeBucketSize);
    buckets.secondContainer = new Bucket(smallBucketSize);

    this.getWaterFromLake = function () {
        var maxSteps = 25,
            step = 1;

        buckets.firstContainer.fill();

        while (step <= maxSteps && buckets.firstContainer.getCurrentAmount() !== waterSize) {
            if (buckets.firstContainer.isFull() && buckets.secondContainer.isEmpty()) {
                buckets.firstContainer.transferTo(buckets.secondContainer);

            } else if (buckets.secondContainer.isFull() && buckets.firstContainer.hasCapacityAvailable()) {
                buckets.secondContainer.empty();
                buckets.firstContainer.transferTo(buckets.secondContainer);
            } else if (buckets.firstContainer.isEmpty() && buckets.secondContainer.hasCapacityAvailable()) {
                buckets.firstContainer.fill();
            } else if (buckets.firstContainer.isFull() && buckets.secondContainer.hasCapacityAvailable()) {
                buckets.firstContainer.transferTo(buckets.secondContainer);
            }
            console.log(buckets.firstContainer.getCurrentAmount() + ", " + buckets.secondContainer.getCurrentAmount());
            step = step + 1;
        }

        if (step > maxSteps) {
            console.log("could not find a solution within the steps limit...");
        }
    };
}
\$\endgroup\$
  • \$\begingroup\$ Have a look at The Clean Code Talks \$\endgroup\$ – Jomoos Jul 27 '12 at 3:41
  • \$\begingroup\$ I'm not quite sure what the end goal is, but how about keeping an Array of buckets and just looping through until you find two you can transfer between? \$\endgroup\$ – rjz Jul 27 '12 at 3:42
  • \$\begingroup\$ Does this one answer your question - stackoverflow.com/questions/4168285/… \$\endgroup\$ – g13n Jul 27 '12 at 3:42
3
\$\begingroup\$

I found your conditions to be a little confusing. It was hard for me to understand the rules of what is going on by just a quick glance. In this simple case, you don't necessarily need to create more abstraction, just be more clear about presenting the rules.

One way to make your code more readable is to try and avoid an if/else maze if a series of ifs could suffice instead. There's a certain amount of personal preference to that, however, when there are many if/else's, your brain has to stack up and keep track of all the previous conditions. It's better to check a condition, then handle it immediately and return to a higher scope instead of nesting further. By not nesting further, you can "forget" about it and simply move on to the next condition.

A trivial example example:

function describeHeight(person) {
    var height = person.height;
    if (height < 4) {
        console.log('short');
    }
    else if (height >= 4 && height < 6) {
        console.log('average');
    }
    else if (height >= 6 && height < 8) {
        console.log('tall');
    }
    else {
        console.log('giant!!!');
    }
}

Versus:

function describeHeight(person) {
    var height = person.height;
    if (height >= 8) {
        console.log('giant!!!');
        return;
    }
    if (height >= 6) {
        console.log('tall');
        return;
    }
    if (height >= 4) {
        console.log('average');
        return;
    }
    console.log('short');
}

Not the best example, perhaps. But I feel like the second version is easier to read. See how the first example has more nesting, and the second has less?

In your example, I would try to simplify the rules. The logic may not be correct here, because I don't exactly understand your rules, but something like this may be easier to read:

while (step++ <= maxSteps) {
    // Check if container has the appropriate amount of water.
    if (buckets.firstContainer.getCurrentAmount() === waterSize) {
        break;
    }

    // If first container is empty, then fill it.
    if (buckets.firstContainer.isEmpty()) {
        buckets.firstContainer.fill();
        continue;
    }

    // If first container is full, fill up the second container if there's room.
    if (buckets.firstContainer.isFull() && buckets.secondContainer.hasCapacityAvailable()) {
        buckets.firstContainer.transferTo(buckets.secondContainer);
        continue;
    }

    // If second container is full, dump it out, and try to fill up the first container if there's room.
    if (buckets.secondContainer.isFull() && buckets.firstContainer.hasCapacityAvailable()) {
        buckets.secondContainer.empty();
        buckets.firstContainer.transferTo(buckets.secondContainer);
        continue;
    }
}
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ +1 good sample - clean === if (buckets.firstContainer.getCurrentAmount() === waterSize) { \$\endgroup\$ – cl-r Jul 27 '12 at 14:12
  • \$\begingroup\$ +1 as well... I updated my code to use a series of IF statements instead of if/else. Like you mentioned, it's a small thing and can be subjective, but I agree that it makes a difference. \$\endgroup\$ – Abe Jul 27 '12 at 19:53
0
\$\begingroup\$

In this case, in every step you could do:

buckets.secondContainer.empty();
buckets.firstContainer.transferTo(buckets.secondContainer);
buckets.firstContainer.fill();

Then inside each function you could test if there is anything to do and if not, return.

void Empty()
{
    if(this.isEmpty()) return;
    ...
}
void TransferTo(Bucket dest)
{
    if(this.isEmpty() || dest.isFull()) return;
    ...
}
void Fill()
{
    if(this.isFull()) return;
}

That should give you the same behaviour without all the if/elses. The checks could also be performed in the main loop but I assume you are already checking for them inside the functions.

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

I created an action handler class that just executes the next step.. It may be overkill, but this project is for learning purposes anyway...

Would something like this be more readable than my original code?

function calculateSteps() {
    var maxSteps = 100,
        step = 1;

    while (step <= maxSteps && !isCalculationDone()) {
        bucketActionHandler.executeNextStep();
        createSnapshot();
        step = step + 1;

    }

    if (step > maxSteps) {
        console.log("could not find a solution within the steps limit...");
    }
    amplify.publish(events.calculationCompleted, snapshots);

}

function BucketActionHandler(containers, size) {
    var buckets = containers,
        waterSize = size,
        waterSizeSmallContainerOffset = waterSize - containers.secondContainer.getCurrentAmount();

    function shouldFinalRetrievalByDumpingExtra() {
        return buckets.firstContainer.getCurrentAmount() === waterSize && buckets.secondContainer.isFull();
    }

    function shouldTransferToSmallBucket() {
        return buckets.firstContainer.isFull() && !buckets.secondContainer.isFull();
    }

    function shouldFillLargeContainer() {
        return buckets.firstContainer.isEmpty() && buckets.secondContainer.hasCapacityAvailable();
    }

    function firstContainerTargetOffsetReached() {
        return buckets.firstContainer.getCurrentAmount() === waterSizeSmallContainerOffset;
    }

    function shouldTransferFromLargeToSmallBucket() {
        return buckets.secondContainer.isFull() && buckets.firstContainer.hasCapacityAvailable();
    }

    this.executeNextStep = function () {
        if (shouldTransferToSmallBucket()) {
            buckets.firstContainer.transferTo(buckets.secondContainer);
            return;
        }

        if (shouldFinalRetrievalByDumpingExtra()) {
            buckets.secondContainer.empty();
            return;
        }

        if (shouldFillLargeContainer()) {
            buckets.firstContainer.fill();
            return;
        }


        if (firstContainerTargetOffsetReached && buckets.secondContainer.isEmpty()) {
            buckets.secondContainer.fill();
            return;
        }

        if (firstContainerTargetOffsetReached() && buckets.secondContainer.isFull()) {
            buckets.secondContainer.transferTo(buckets.firstContainer);
            return;
        }

        if (shouldTransferFromLargeToSmallBucket()) {
            buckets.secondContainer.empty();
            buckets.firstContainer.transferTo(buckets.secondContainer);
            return;
        }
    };
    buckets.firstContainer.fill();
}
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.