< Articles


The Zombie Feature Survival Guide

Not long after being hired at Xactware, I reached a significant milestone in every software engineer’s career: my first major feature. In reading through the feature description, I sensed the importance of what I was about to build and the impact it would have on our customers. From a technical perspective, I envisioned potential challenges managing state, passing around form data, and navigating the seemingly endless weave of conditional logic built around customer rights, administrator settings, and existing form fields. Above all, I felt an overwhelming sense of pride in what I was about to do and was intent on creating the best written feature my team had ever seen, fully furnished with an intuitive api, a simple to understand architecture, and a battery of ‘I dare you to try and break me’ unit tests.

A few weeks later, my masterpiece was complete and was merged into the codebase. I felt both excited and relieved: excited that the code was in a good state and relieved that I could move on to bigger and better things. That feeling of accomplishment didn’t last long though as someone pointed out a small bug just a couple of weeks later. A short time after the initial bug, it happened again. And again. And again. A feature that was dead in my mind kept creeping into my life, distracting me from the tasks I had since picked up. It had grown into a zombie feature, ready to come back to life at a moment’s notice.

Realizing the inefficiency and pain of my poor code, I set out to create a process that would eliminate zombie features once and for all. After considering a number of potential solutions, I settled on one that should have been obvious from the start: the key to great code is refinement. Let’s face it, do we ever implement the best solution on the first try? Over the last six months, this process has become my standard for writing code and for writing content. I refine (or refactor, whichever you prefer) until I can’t find anything left to refine. Let’s take a look at how this works from a writing perspective, and then we’ll jump into some code examples. Here’s the process I went through for the first paragraph of this post:

First Paragraph: Round One

After a month or two at Xactware, I was tasked with my first big feature. In a lot of ways, I’d felt like I had officially broken into the tech industry. I had the confidence of my team and I was finally going to be doing something outside of code refinement and bug fixes. I definitely wasn’t going to screw this one up.

Alright, not bad. It gets the basic point across but it sounds a little bit out of place and the jump from sentence to sentence is too quick. The whole paragraph is made worse by the “I definitely wasn’t going to screw this one up” line which sounds like something out of a Disney Kids tv show. We can definitely do better.

First Paragraph: Round Two

Not long after I was hired at Xactware, I was tasked with my first big feature. It was a complicated undertaking that required consistent communication with our product team in combination with a well thought out architecture. While complicated, simply being tasked with this feature gave me the feeling that I had finally made it. I could feel the confidence my team had in me and I wanted to prove that it wasn’t misplaced. This would be the most well thought out, best tested feature they had ever seen.

Wow, that’s a lot better. We’ve added in a lot of those smooth transitions which helps the paragraph flow and in general it feels more professional. We’re certainly on the right track but it still doesn’t feel like the paragraph has any personality. Let’s see if we can refine it a little more.

First Paragraph: Final Revision

Not long after being hired at Xactware, I reached a significant milestone in every software engineer’s career: my first major feature. In reading through the feature description, I sensed the importance of what I was about to build and the impact it would have on our customers. From a technical perspective, I envisioned potential challenges managing state, passing around form data, and navigating the seemingly endless weave of conditional logic built around customer rights, administrator settings, and existing form fields. Above all, I felt an overwhelming sense of pride in what I was about to do and was intent on creating the most amazing feature my team had ever seen, fully furnished with an intuitive api, a simple to understand architecture, and a battery of ‘I dare you to try and break me’ unit tests.

We’ve done it! The paragraph matches my voice, the transitions through the paragraph are smooth, and it doesn’t feel so dry. It’s a great kick off to the article and something I’m okay putting my stamp of approval on.

Now you’re probably thinking, “Why should I care?”. Well, the same principles that apply to great writing apply to great code. After all, the pinnacle of great code is creating something that the computer can parse and something that a developer (probably future you, surprise!) can read and understand. Let’s experiment with this technique from a code quality perspective. Here’s the task:

You’re working for a blog that simulates Super Smash Bros fights. To be clear, we’re talking about the greatest Super Smash Bros ever created which is of course for the N64. In preparation for the big product launch, you’ve been tasked with creating the initial logic that simulates the fight. You’ve been told that your function should take in two super smash characters and apply some sort of logic such that any character can win but stronger characters will win more often than their weaker counterparts. The code right now looks something like this:

const characters = [
    { id: "1", name: "Pikachu", attackDamage: 10, hitChance: 0.8 },
    { id: "2", name: "Kirby", attackDamage: 10, hitChance: 0.7 },
    { id: "3", name: "Donkey Kong", attackDamage: 6, hitChance: 0.5 },
    { id: "4", name: "Ness", attackDamage: 7, hitChance: 0.3 },
    { id: "5", name: "Jigglypuff", attackDamage: 4, hitChance: 0.9 },
];

const winnerText = [
    "You were an honorable foe.",
    "Bro, do you even play super smash?",
    "Why would anyone choose your character? I'm legit OP",
];

const fight = (characterId, characterTwoId) => {
    // do some fighting
    // have the winner gloat
};

For the simplicity of the code, we’ve left off a few notable characters and we’re assuming that both characters can die on the same turn. If that ends up happening, we’ll say that the character with less health is the loser. Let’s get started on our first implementation:

Super Smash: Round One

const fight = (characterId, characterTwoId) => {
    let characterOneTemp;
    characters.forEach((character) => {
        if (character.id === characterId) {
            characterOneTemp = character;
        }
    });

    let characterTwoTemp;
    characters.forEach((character) => {
        if (character.id === characterTwoId) {
            characterTwoTemp = character;
        }
    });

    const characterOne = { ...characterOneTemp, health: 100 };
    const characterTwo = { ...characterTwoTemp, health: 100 };

    while (characterOne.health > 0 && characterTwo.health > 0) {
        const characterOneCanAttack = Math.random() <= characterOne.hitChance;
        if (characterOneCanAttack) {
            characterTwo.health -= characterOne.attackDamage;
        }

        const characterTwoCanAttack = Math.random() <= characterTwo.hitChance;
        if (characterTwoCanAttack) {
            characterOne.health -= characterTwo.attackDamage;
        }
    }

    if (characterOne.health > characterTwo.health) {
        const winnerTextIndex = Math.ceil(33 / (100 / 3)) - 1;
        const gloatingPhrase =
            characterOne.health <= 0
                ? winnerText[0]
                : winnerText[winnerTextIndex];
        console.log(
            `${characterOne.name} yells to ${characterTwo.name}, "${gloatingPhrase}"`
        );
    } else {
        const winnerTextIndex = Math.ceil(33 / (100 / 3)) - 1;
        const gloatingPhrase =
            characterTwo.health <= 0
                ? winnerText[0]
                : winnerText[winnerTextIndex];
        console.log(
            `${characterTwo.name} yells to ${characterOne.name}, "${gloatingPhrase}"`
        );
    }
};

Well, we’ve got it working but this code is ugly. We’re duplicating all kinds of logic and the function is way too long. Can you imagine coming back to this code in the future? Let’s just refine it right now so we don’t have to.

Super Smash: Round Two

const fight = (characterId, characterTwoId) => {
    const characterOneTemp = findCharacter(characterId);
    const characterTwoTemp = findCharacter(characterTwoId);

    const characterOne = { ...characterOneTemp, health: 100 };
    const characterTwo = { ...characterTwoTemp, health: 100 };

    while (characterOne.health > 0 && characterTwo.health > 0) {
        characterAttacks(characterOne, characterTwo);
        characterAttacks(characterTwo, characterOne);
    }

    if (characterOne.health > characterTwo.health) {
        proclaimVictory(characterOne, characterTwo);
    } else {
        proclaimVictory(characterTwo, characterOne);
    }
};

const findCharacter = (characterId) => {
    let characterTemp;
    characters.forEach((character) => {
        if (character.id === characterId) {
            characterTemp = character;
        }
    });
    return characterTemp;
};

const characterAttacks = (attackingCharacter, attackedCharacter) => {
    const characterCanAttack = Math.random() <= attackingCharacter.hitChance;
    if (characterCanAttack) {
        attackedCharacter.health -= attackingCharacter.attackDamage;
    }
};

const proclaimVictory = (victoriousCharacter, losingCharacter) => {
    const winnerTextIndex = Math.ceil(33 / (100 / 3)) - 1;
    const gloatingPhrase =
        victoriousCharacter.health <= 0
            ? winnerText[0]
            : winnerText[winnerTextIndex];
    console.log(
        `${victoriousCharacter.name} yells to ${losingCharacter.name}, "${gloatingPhrase}"`
    );
};

Round two is certainly an improvement. We’ve gotten rid of a lot of the duplicate logic while making the fight function significantly more readable. Still, we’ve got some room for improvement. It would be really nice if our fight function was less than 10 lines of code. As well, let’s make sure all of our variable names are really descriptive. And lastly, let’s get rid of those abbreviations. In my opinion, that’s typically a code smell.

Super Smash: Final Revision

const fight = (characterId, characterTwoId) => {
    const characterOne = createCharacterInstance(characterId);
    const characterTwo = createCharacterInstance(characterTwoId);

    while (characterOne.health > 0 && characterTwo.health > 0) {
        characterOne.triesToAttack(characterTwo);
        characterTwo.triesToAttack(characterOne);
    }

    proclaimVictory(characterOne, characterTwo);
};

const createCharacterInstance = (characterId) => {
    for (let i = 0; i < characters.length; i++) {
        const currentCharacter = characters[i];
        if (currentCharacter.id === characterId) {
            return augmentedCharacter(currentCharacter);
        }
    }
};

const augmentedCharacter = (character) => {
    return {
        ...character,
        health: 100,
        triesToAttack: function (opposingCharacter) {
            const characterCanAttack = Math.random() <= this.hitChance;
            if (characterCanAttack) {
                opposingCharacter.health -= this.attackDamage;
            }
        },
    };
};

const proclaimVictory = (characterOne, characterTwo) => {
    const victor =
        characterOne.health > characterTwo.health ? characterOne : characterTwo;
    const loser = victor === characterOne ? characterTwo : characterOne;

    const winnerTextIndex = Math.ceil(33 / (100 / 3)) - 1;
    const gloatingPhrase =
        victor.health <= 0 ? winnerText[0] : winnerText[winnerTextIndex];
    console.log(`${victor.name} yells to ${loser.name}, "${gloatingPhrase}"`);
};

This code is so much better. Each function takes no more than 10 lines of code and does just one thing, we’ve made the code easier to read, and we’ve eliminated the need for any abbreviations. This code would be easy to come back to and even easier to test.

So when you’re writing your code, remember to give adequate time to come back and refine. Hold off on submitting the pull request until you feel the code is bulletproof and readable. If you don’t, there’s a good chance you’ll be seeing this feature again in a few short weeks.