Chapter 8. Strategies for Execution

Opened in 1904, the New York City subway is among the world’s oldest and most-used public transit systems, serving just under six million riders on an average weekday. Those of us who are intimately familiar with the sprawling network have developed dozens of tiny optimizations that make riding the subway second-nature. We listen for announcements for announcements to changes in service when hoping on late at night on a Tuesday. We know the precise force and angle with which to scan our MetroCards through the turnstiles. For newcomers to the city, we can share some of these small but mighty tips to make their first few trips a bit less hectic.

Think of this chapter like the friendly New Yorker giving you advice as you set out to navigate the city’s subway system. It contains a medley of tips for promoting smooth execution throughout a refactor. We’ll first touch on good team-building practices. There are a handful of ways we can go beyond establishing regular communication habits to keep our teammates productive and happy. Next, we’ll cover a few items you should be keeping track of during the refactor to make sure that you’re staying on course and know precisely what to attend to when you’ve reached the final stages of the refactor. Finally, we’ll discuss a few coding strategies to keep sturdy reigns on the refactor as you’re implementing it.

Team Building

In Chapter 6, we examined a few reasons why having a strong team is important within the context of large software projects, including ambitious refactors. We mostly focused on the benefits of having reliable teammates during difficult times (e.g. when the project reaches a mundane stage, or hits a new roadblock). What we didn’t mention is that teams that work well together are more creative, learn more from one another, and ultimately solve problems better and faster. To that goal, it’s vital that you and your teammates prioritize regularly participating in team-building activities. The options outlined here are not exhaustive, but I believe that they are some of the most useful habits to develop in order to strengthen your relationship with your teammates. Once you’ve built up the muscle-memory around them, they’ll become second nature, and will surely make the refactor fly by smoothly.

Pair Programming

Pair programming is a great team-building tool. By working on a problem together, it gives the participants a great opportunity to learn each others’ strengths (and weaknesses) in a collaborative, low-stakes environment. If your team hasn’t had much experience working together yet, consider encouraging that they pair on a handful of tasks at the onset of the project. Starting early is important; not only does a new project give you the unique opportunity to set good habits from the very start, understanding your teammates’ abilities early can help the project kick off on the right foot and continue to make forward progress efficiently.

More practically, pair programming can also be a great way to transfer knowledge from one teammate to another. Engineers who are alone in understanding one or more pieces of a given system are a liability to your project, and not infrequently, your company as whole. In many cases, these engineers may feel that they are unable to take time off or completely disconnect from work for a few days out of fear that they’ll be needed in the event of an emergency with the part of the system only they know. To ensure that no single developer on your team is a “knowledge island”, you can set up pairing sessions as a means of transfering their expertise to others on the team. Evenly distributing knowledge across each of your team members lightens the load on any single developer should problems arise with any aspect of the refactor.

Pairing can also be a great way to debug or solve a difficult, or abstract problem. We say two heads are better than one for a reason: by having two engineers thinking through the same problem, you’re more likely to come up with a greater variety of solutions, landing on one that works well sooner. The active back and forth helps you address disagreements head-on, refining your solution more effectively. As you navigate through the problem together, you’ll end up making fewer mistakes; in fact, research out of the University of Utah shows that code written in pairs results in about 15% fewer bugs. Finally, you’re less likely to get distracted; because you’re both committing the time and energy to solving a problem together, reasoning through the problem out loud, the temptation to check your email or shoot someone a message decreases.

Refactoring in pairs can be particularly effective because with one person typing, the other is freer to think about the bigger picture. When refactoring, it’s easy to get stuck in the weeds trying to detangle what often tends to be confusing, legacy code. Your pair can help you regain focus on the greater goal, and, by thinking through the problem a few steps further, point out any pitfalls you may run into earlier in the development process.

Pair programming isn’t without its downsides, however. When it comes to scoping out a problem or learning something new (e.g. using a framework, adopting a tool, learning a programming language), some engineers, myself included, to do so on their own. I find that I’m able to retain important concepts better if I stumbled through learning them the first time around. For problems that are well-defined and relatively straightforward to solve, pairing isn’t a particularly productive approach; while there is a slight chance you might solve the task more quickly and produce a less buggy outcome, tying up two engineers’ time on a simple task is not always the best use of resources on your team.

Pairing can also be a draining task for the duo. Needing to articulate your thinking process over a sustained period of time takes up quite a bit more energy than quietly working on your own, reasoning through the problem internally. By the end of a pairing session, you might need to take a break and switch gears to recharge. For developers who aren’t great verbal communicators, pairing can be especially challenging, making any pair programming exercise feel like a chore. This is why it’s important to be mindful of everyone of the team’s abilities and preferences when advocating for pairing.

Being mindful of drawbacks, here are a few recommendations for how to institute pairing on your team:

  1. Encourage pairing, but do not make it mandatory. There’s a strong chance that some members of your team are great proponents of pair programming and others are not. By highlighting its benefits and underscoring your support for the practice, you’ll hopefully convince those who are on the fence (or have never tried it before) to give it a go. (And hopefully, after having tried it, they’ll be eager to repeat the exercise.) On the other hand, forcing those are uncomfortable to pair can be a recipe for disaster; they may grow to resent the team and the project, leading them to seek a way out.

  2. Pair engineers with similar levels of experience. Unless you’re using pair programming as a tool to teach something specific to someone more junior, you’re better off pairing like-skilled engineers together. When working through a difficult problem or debugging an issue, developers who are at a similar level are less likely to be frustrated by the other’s lack of experience. You’ll more effectively bounce ideas off of one another if you’re at comparable levels in your technical ability.

  3. Timebox the session. Because pair programming can be taxing, it’s important to give the session a well-defined cut-off (with breaks as needed). Start with an hour, and if you come to the end of the time and you have the energy (and time) to keep going, extend your session by another hour. Give each other an opportunity to call it a day; you don’t want the pairing to stretch beyond either of your capacities and risk needlessly decreasing the efficiency of the session.

Keeping Everyone Motivated

In Chapter 4, we discussed building out a focused, properly-balanced execution plan that gives the team enough flexibility to prevent exhaustion. We can further ensure that our teams stay motivated throughout a long at-scale refactor by taking the time to recognize our teammates and celebrate our achievements along the way. Your team doesn’t need a massive budget for branded mugs or access to covetted off-site activities to build meaningful connections across the team or highlight the group’s contributions. There are a number of simple but impactful ways to keep everyone’s morale up.

Motivating individuals

First, we’ll consider how we can keep individuals motivated. One of the more compelling ways we can boost a teammate’s drive is by giving them the opportunity to contribute to the refactor in a way that best leverages their unique skills and abilities. Your teammates will be much happier (and likely more productive) if they are working on pieces of the refactor that they find to be the most rewarding. If your teammates are looking for opportunities to grow, whether by developing a new technical skill or by overseeing a more significant portion of the project, do your best to make these opportunities available to them. Remember how you may have pitched this teammate on joining your effort in Chapter 6, especially if you drew them in with the prospect of an opportunity at greater visibility or responsibility (and perhaps even a promotion.)

If possible, give your teammates the flexibility to choose when, where, and how they work. Not everyone is cut-out to work from 9am to 5pm with a half hour for lunch at noon every day. Some might prefer to come into the office at the crack of dawn and head out in the early afternoon. Others might only log on mid-morning, pick up their children mid-afternoon, and wrap up after dinner. If you are able to accommodate for your teammates’ assorted schedules while continuing to maintain good communication practices (see Chapter 7), they will be not only be thankful, but likely even more productive overall!

Recognizing individual teammates for their distinct contributions is a great way to keep them motivated. By showing them that you and the rest of your team appreciate their hard work, you’re reaffirming that they are doing the right thing, encouraging them to keep going, and fostering a sense of belonging on the team. Recognition can take just about any shape: it can be through a formalized department- or company-wide program, or can be as simple as crafting a handwritten note. Be mindful of your teammates’ preferred way of being recognized. Though some enjoy hearing their name called out at an all-hands, others shy away from public praise. Recognition in the wrong form is at best not very effective, and at worst a total fiasco. Sometimes, a thoughtful email or glowing peer review is more than enough.

Tip

Your manager can be a great asset for helping you set up ways to recognize your team as a whole. (You’ll probably need their support if you’re hoping to get a budget for whatever you’re planning to put together.) That said, there is unique value in having the team recognize its peers.

You could, for example, put together a lightweight “Win of the Week” tradition. To kick it off, the team acquires a small trophy (or any item clearly visible from a teammate’s desk) and chooses someone to recognize for excellent work done over the previous week. This could be anything from stepping in to help resolve a tricky bug, or a crafting great description for a given patch. The following week, the winner chooses the next winner, passing on the trophy. The tradition continues until the project wraps up or until the team chooses to retire it.

Motivating teams

Next, we’ll take a look at helpful methods for keeping your team motivated as a whole. A near fool-proof way to get everyone excited about doing great work is to turn it into a game. By gamifying the more mundane portions of the refactor, you may find your teammates eager to complete tasks and progressing towards milestones more quickly. Take, for example, a simple game of Bingo. Identify small but important contributions your team can make during the refactor’s current milestone and plop them into a Bingo game sheet generation tool. These can be as simply as pairing with some on a difficult problem or completing ten code reviews. You can print out the boards and distribute them to your team and offer a small prize for winners.

When gamifying any number of tasks, be mindful not to incite too much competition. While it can be a great motivator, if it gets out of hand you’ll risk sparking conflict and seeing morale and teamwork deteriorate. Incorporate aspects of teamwork into the game deliberately; this will encourage everyone to pull up those around them and further solidify your team. With a large-scale refactor, there is very little room (if any) for sloppy execution, so you’ll also want to be careful to chiefly incentivize the quality of the work rather than its completion. If you put emphasis on reaching the finish line, your teammates might cut some corners in an attempt to get there faster.

Tip

When planning estimates for smaller subtasks within larger project milestones, consider gamifying part of the process. Have each member of the team submit their best guess as to when you’ll hit a target metric, following The Price is Right rules (i.e. closest without going over). When you reach the metric, recognize the winner with a drumroll reveal at your next team meeting. Everyone will get a kick out of trying to hit the nail on the head and your estimates might get better over time!

Finally, remember to celebrate your team’s achievements with a gathering or two speckled throughout the project, particularly after concluding significant milestones. Moments of celebration help create sustained engagement and maintain good morale. If the team never has the opportunity to hit pause and commemorate each others’ efforts, your refactor will begin to feel like an endless rat race. Carve out some time to bring everyone together whichever way works best – whether that’s a team potluck lunch or a mid-afternoon coffee toast. You’ll all be thankful to have taken a moment to reflect on your accomplishments.

Keeping a Tally

As you’re executing on your refactor, it’s important to frequently check-in on your progress and maintain a running tally of important findings. By measuring and reflecting often, you’ll be more confident that the project is headed in the right direction, and decrease the likelihood that your team forgets something important in the final stages of the refactor. Be certain to continue to update the living version of your execution plan discussed in “Execution plan” with your mid-project updates.

Intermediate Metric Measurements

In Chapter 3, we examined a number of distinct ways to characterize the problems we aim to fix with our refactor. We later used those metrics to inform our execution plan, and further broke down the project into individual milestones, each with their own set of metrics. We shouldn’t lose sight of these goals while actively executing on the refactor at the risk of veering off-course. With every ambitious software project, there is a significant and dangerous opportunity for scope creep at every turn.

By measuring the team’s progress towards each intermediate metric on a weekly (or biweekly) basis, you are holding yourselves responsible for moving the needle forward on the goals you’ve identified as the most important. With frequent check-ins, the team is less likely to give into the temptation to embark on any tangential side quests, allowing for the project scope to increase. Periodic check-ins also give you the ability to assess your velocity. If everyone is focused on the right tasks, but there is little positive change in the metrics for several weeks in a row, something is clearly amiss. Perhaps the team is struggling to make substantial progress because they continue to encounter a number of difficult bugs, or the metrics are not ideal candidates for conveying your team’s contributions. Whatever the underlying dilemma, you’ll know you successfully solved it when you begin to notice a good change in your metrics once more.

Unearthed Bugs

Regardless of whether your refactor is motivated by the desire to surface and fix systemic bugs, you are bound to encounter a handful of defects throughout the endeavor. For each bug, no matter what you decide to do about the bug (fix it or not), you should document when in the project it was uncovered, the conditions under which it arises (for easy reproduction), and what actions were taken as a result. There are typically two options when confronting a bug within the context of a refactor; the first is to fix the bug, and the other is to reimplement it.

Consider the case where your team fixes the bug. If the fix is easy and clean as a result of the refactor, having an example you can quickly reference to demonstrate its efficacy is convenient for showing off to stakeholders or sharing with peers. Sometimes, just one or two thorny, well-documented bugs can convince anyone who was initially on the fence about the refactor that it is well worthwhile. On the other hand, if your team ports the bug into the refactor, you’ll need to know precisely where to find it and how to reproduce it in order to either fix it or to hand off to the appropriate team to patch.

Clean-Up Items

In “Cleaning Up Artifacts”, we looked at the importance of including a distinct phase in our execution plan for cleaning up artifacts produced during the refactor. Every refactor should prioritize leaving the codebase in an orderly state for other developers; after all, usually a substantial motivation for a large refactor is to improve the ergonomics of your codebase. While we might have a modest intuition about the kinds of artifacts we’ll be generating throughout the project well before we write our first line of code, there will be undoubtedly be an assortment of them we create on the fly.

Keep track of everything that’ll need tidying, whether you plan to tackle the clutter at the end of your current milestone or only in the final stages of the project. Updating your list immediately as you render a section of code obsolete is critical; this way, you’ll be certain to remove each relevant artifact once you’ve reached the clean-up phase. The engineers who interface with the newly-refactored code will be grateful for an orderly experience.

Tip

Just as a cook would recommend cleaning pots and pans as you use them when preparing a meal, I recommend continually cleaning up as refactor progresses. It is far easier (and safer) to remove pieces of code soon after rendering them unnecessary. At this stage, the myriad of interactions between the newly-obsolete code and the remainder of the refactor is fresh in your mind and you risk making fewer mistakes extricating it.

Out of Scope Items

Nearly every engineer on your team will encounter a few opportunities to add scope to the refactor during its lifetime. Obviously your project will have a better chance at hitting its important deadlines if everyone resists the temptation, but these opportune extensions should not be outright ignored. Consider keeping a list of the opportunities you encounter to expand on the project. Having a succinct set of spin-off projects can demonstrate the versatility of your refactor; if there are a broad number of distinct ways to capitalize on the project’s momentum to continue to improve the codebase, your stakeholders (and peers alike) will be more likely to believe the refactor was a valuable endeavor. If your own team (or any other team at the company) wants to build upon the foundation established by the refactor and continue making incremental improvements to the codebase following its completion, they could scope out a few projects from this list and kick them off immediately.

Programming Productively

There are handful of useful strategies you can adopt to make a lengthy refactor much more pleasant for both yourself and your team members. Large software projects are not always tricky to develop; in fact, when writing something entirely new, there might only be a handful of difficult maneuvers, most of which only necessary when embedding the feature into the existing codebase. On the other hand, when there is a significant amount of code needing to be written for a refactor, the majority of it a copy of existing behavior, it needs to be carefully designed and delicately integrated with its original implementation. There are considerably more opportunities for the painstaking process to fail. Hopefully you can learn to successfully navigate the refactoring development process by following the techniques described in this section.

Prototyping

When we set out to draft a plan for our refactor in Chapter 4, we aimed to strike the right level of detail. We wanted the plan to to be approachable to important stakeholders who might not be intimately familiar with the technical details, but sufficiently specific so that we could properly inform a team around the project and begin execution without ambiguity. Where the plan remained deliberately vague is a perfect opportunity for prototyping.

Prototyping early and often helps your team ultimately move faster if you abide by two important principles:

  1. Know that your solution will not be perfect. Focus on crafting a solution that works well overall, being mindful about not spending too much time perfecting the details. Remember that even if we spent hours attempting to devise the ideal solution, a future change in requirements might render it obsolete. (We saw a few concrete examples of this in Chapter 2.) A great solution is one that solves the most important problems well, and allows for a fair amount of flexibility down the line.

  2. Be willing to throw code away. If we spend a week or two writing a solution that simply doesn’t deliver, take the pieces that work, throw the rest away, and start again. Prototyping is all about trying something, learning from that experience, and starting again.

Let’s consider a refactor where your team wants to split up a bloated class into a few distinct components. Your team came up with a preliminary design that divides up its primary responsibilities into three new classes, but there are a number of minor, albeit important responsibilities that have yet to be assigned to any one of them. Instead of committing wholeheartedly to a solution early in the process, you decide to prototype a few different options, trying out the ergonomics of the new classes in a just a few illustrative sections of the codebase. Given the prototypes, your team is able to decide what works, what doesn’t, and iron out a solution that should integrate well with the remainder of the codebase.

Keep Things Small

When making sweeping changes across a large surface area, it’s easy to get carried away. Say, for instance, we need to migrate all callsites of one function, pre_refactor_impl, to a new one, post_refactor_impl. There are about three hundred instances of pre_refactor_impl throughout the codebase, spanning just over eighty files. You could do a simple find and replace, lump the changes into a single commit, and put the patch up for review by a teammate. If the migration is fairly straightforward, although creating just a single set of changes might appear to be more convenient, there are a few severe disadvantages.

First, committing small, incremental changes makes it much easier to author great code. By pushing bite-sized commits, you can get relevant feedback early and often from your tooling (e.g. integration tests running on a server through continuous integration). If you push a wide breadth of changes infrequently, you risk needing to wade through and fixing a heap of test failures. More modifications per commit leads to a greater likelihood of cascading test failures; fixing one error only reveals another. Keeping tight commits ultimately enables you to better understand their impact and fix any failing tests faster. The same applies when manually verifying changes.

Second, reverting a small commit is much easier than a big one. If something goes wrong, whether during development or well after the code has been deployed, reverting a small commit allows you to carefully extract only the offending change.

Third, Because concise commits tend to be sufficiently focused, you’ll also be able to write better, more precise commit messages. With better commit messages, not only will you be able to quickly locate a specific set of changes faster, your teammates will better understand them when scanning through version history at a later date. (Tiny commits typically get reviewed and approved much, much faster, too!)

Finally, it is nearly impossible for a teammate to adequately review the entirety of the modified code. Although organizations should not rely on code review to catch bugs (relying instead on thorough and earnest testing), if there is insufficient test coverage, the burden of catching potential mistakes falls to the reviewer. Superficially, the changes may seem easy to verify, but after auditing just a few of them, unless we retain a steadfast focus, our ability to spot discrepancies wanes. Large change sets are far easier to review if split up into logically organized, pithy commits.

Tip

When refactoring, you want to maintain the original version history as much as possible. Consider using operations like git mv to move files around rather than deleting them and adding them back. Make it clear in your commit descriptions that the change is part of a larger refactor, so that engineers know to dig deeper into the commit history when looking for a potential code owner. Be a thoughtful teammate when writing descriptions for your teammates reviewing your code. Write a thorough description, outlining what the review should expect to find in the change set, along with any necessary context.

Test, Test, Test

Because refactors involve gradually reimplementing existing behavior, we need to ascertain that the changes are not modifying the intended behavior. In practice, it is typically much more difficult to verify that nothing has changed rather than the opposite, making it particularly important that we test incrementally and repeatedly when refactoring. By frequently rerunning unit tests, integration tests, or walking through manual tests, we can either confirm that everything has remained unaffected or pinpoint the precise moment at which the behavior has diverged.

Tip

Before you begin modifying any section of code, verify that there are neat, distinct unit tests for it. There might already be a handful of tests to assert the behavior, but you should take the time to determine whether any additional cases are missing. If the tests are too coarse (e.g. only testing the flow for a top-level function, without any tests for any of the individual helper functions), split them up. Granular tests, just like granular commits, will help you narrow down issues early.

Asking the “Stupid” Question

We’ve all been in that meeting: the meeting where we sit with a bunch of senior engineers, talking about a technology or a product feature we don’t understand very well. At first, it seems as though everyone’s following along, nodding as a select few lead the discussion. We’re confused, but we’re too worried that we’ll look unprepared to ask any clarifying questions. There are two directions this meeting usually ends up taking. The first is the one where we continue to sit quietly and spend the rest of the meeting trying to piece everything together, unable to meaningfully contribute to the conversation. The second is the one where someone else interjects, politely asking the very same question we were too embarrased to ask. We’re thankful for our teammate’s curiosity (thankful we weren’t alone), and we’re able to get back on track with everyone else pretty quickly.

We can’t always count on our inquisitive teammates to have the same questions, nor should we be content to waste time sitting in a meeting or reading an email thread continuing to wonder what is being discussed. So, I propose a third direction, the one where you stand up and simply ask the “stupid” question. By prioritizing getting clarity over maintaing an illusion of omniscience, you are modeling important behavior for your team. You’re affirming that no question is, in fact, a “stupid” question, and that above all else, it’s important to make sure that everyone is on the same page. You’ll have more productive discussions, fewer misunderstandings, and get to work solving the right problems more quickly.

When refactoring something at scale, because the surface area of the changes can be quite vast, there is a distinct chance that you will come in contact with portions of the codebase you’re unfamiliar with. Being unafraid to seek out the experts in these areas and ask for guidance is crucial. Whether you need a short explanation, or a more in-depth walkthrough, it’s imperative to build a strong understanding of the code you’re seeking to modify. Not only will you save on development time, and introduce fewer bugs as you refactor it, you’ll also have the insight necessary to refactor it in a way that best suits the code.

Conclusion

Once you’ve pushed the final few commits, and tidied everything up, you’re ready to take on one last, vital task. You need to find ways to make all of your efforts persist long-term. Our next chapter will take a look at a few important steps your team can take to ensure that your codebase does not slowly regress back to its previous state.

(TODO: Come back to this?)

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.211.66