๐Ÿง‘๐Ÿพโ€๐Ÿ’ป prep

Navigating and working with code written by others

๐Ÿ›๏ธ Understanding Legacy Code

Learning Objectives

Legacy code is any code you inherit

What Makes Code “Legacy”?

Legacy code isn’t necessarily bad or even that old. It’s code that:

  1. You didn’t write - It lacks your mental model and intentions
  2. Powers important systems - It can’t simply be replaced
  3. Contains institutional knowledge - Sometime undocumented! Decisions were often made for good reasons which we may not remember any more, and changing those decisions may be risky.

The Purple Forest application is now your legacy code. Someone else designed and built it, and you need to maintain and extend it.

Working with Purple Forest

Purple Forest has:

  • Established architecture - It follows defined patterns
  • Functional system - It mostly works, even if you don’t understand how
  • Multiple components - Changes might have unexpected side effects
  • Documented design - But the documentation might be incomplete or out of date

So how do you approach understanding and working with this legacy codebase?

๐Ÿ—ฟ Fear and logic

Learning Objectives

Do you remember your first day at CYF ? You couldn’t find the building, maybe, and you had no idea how the day would go. What on earth is a day plan, or a backlog, you thought to yourself. Perhaps you got frustrated: why are all my changes from last week in my new PR? How?! It was incomprehensible. But you learned! You asked questions, you read the guides, and you built a mental map of the system.

You might have found your first code reviews challenging too. You worked on a project for days, just got it all working, and now someone is telling you to change it. Changing code you don’t understand very well feels risky.

Feeling a bit hesitant is fine. In fact, some caution is healthy. If code is working and it’s doing something important for the business, we don’t want to break it. But we also don’t want to be so fearful that we can’t fix it or write new features. We must balance caution with curiosity. We will approach legacy code with a structured, logical plan.

--- title: How we're feeling config: look: handDrawn --- graph TD Fear -->|What if I break it?| Paralysis Paralysis -->|I'll just work around it| Avoidance Avoidance -->|Let's rewrite from scratch| Rewrite Rewrite -->|What should it do?| Fear
--- title: How we act to address those feelings config: look: handDrawn --- graph TD Logic -->|What should it do?| Hypothesise Hypothesise -->|What does it actually do?|Test Test -->|Small, careful changes| Modify --->|Cycle of progress|Logic

A good rule here is Chesterton’s Fence. This says that before we change something, we must explain why it’s like that in the first place.

In code, Chesterton’s Fence comes up a lot when we read code that looks complicated. It’s easy to think “This code could be simpler”. And maybe it could! There are a lot of reasons code is more complicated than it could be. Maybe it is complicated because:

  • โœ… the person who wrote it didn’t know a better way. If so, we can simplify it.
  • โœ… the simpler way was only introduced to the language after the code was written. If so, we can simplify it.
  • ๐Ÿšซ we want to support old versions of the language when the simpler way didn’t exist. If so, we can’t simplify it: we would break something important!
  • ๐Ÿšซ some important edge-case we hadn’t considered. If so, we need to understand that edge-case before we can change it, or we’ll break it.

Understanding why is crucial here. Tests can help us to understand. If we simplify the code and a test for a particular edge-case breaks, we found out why the code was more complicated! Comments can help too. A comment saying “We don’t do the simpler thing because it doesn’t handle undefined properly” tells us why the code is more complicated. But sometimes legacy code doesn’t have useful tests or comments.

๐Ÿงญ Finding things

Learning Objectives

By tracing request flows, drawing maps, and using code search tools, you can efficiently find your way through legacy code.

1. ๐Ÿ” Find

We’re going to use the features of our IDE to help us. VSCode has a million features for this, but we’re just going to start with four: Open file, Find references, Peek definition, and Find in files.

๐Ÿ“‚ 1. Open file : Cmd+P or Ctrl+P

Code Along

Open the Purple Forest codebase. Let’s start at the router. Press Cmd+P and type router. Open the file router.mjs and look at where it shows up in the Explorer. What else is in that directory? Why have these files been grouped together? Write down your ideas.

What does this module do? What can it tell you about the system?

๐Ÿท 2. Find references: fn+Shift+F12

Code Along

In router.mjs there is a function called handleRouteChange. What is using this function? Where is it called?

Press fn+Shift+F12 . This will show you a references panel with links to every place that references this function. Double click on a reference to navigate to that file.

๐Ÿซฃ 3. Peek definition: fn+F12

Code Along

Now you’re in index.mjs you can see where the function is called, but you can’t see the details of the function. Double click on the function name to select it and now press Fn+Option+F12. This opens the peek panel, which shows you the function definition without leaving the file you’re in.

๐Ÿ—ƒ 4. Find in files: Cmd+Shift+F or Ctrl+Shift+F

Code Along

Is that everything to do with the router? Press Cmd+Shift+F and search for route. What else do you find?

Repeat this process with navigateTo. Deliberately practice using keyboard shortcuts to navigate your codebase. As the code you work with gets more complicated, scrolling through files becomes enormously time-consuming.

๐Ÿ’กTip

You will learn these keyboard shortcuts over time.

Use the commands now, but you won’t be able to remember all the keyboard shortcuts at once. Try to learn one or two more each week. Use the Command Palette to look up the keyboard shortcuts.


2. ๐Ÿ“ž Trace a Request Flow

Open Purple Forest on your local machine. Read the README to get it running, and launch the frontend with Live Server. Again, Devtools has a million features but we’re going to use four: Event Listener panel, Network panel, Sources panel, and Local Storage in the Application panel.

Code Along

  1. Inspect the login form and find the event listener in the listener panel. Make a prediction. What will happen, step by step, when we submit this form? Write down your answer in a numbered list. It doesn’t have to be perfect, just jot down a quick prediction.

  2. Open the Network panel. Log in๐Ÿงถ๐Ÿงถ Log inYou can find the seed login details in the codebase you are reading! as user sample. What do you see? Write down any observations you can make in bullet points.

  3. In your notebook, sketch the flow of the request from the user click to the server response. Complete this flowchart:

--- config: look: handDrawn --- graph LR A[Login form submit] -->|Event| B[handleLogin] -->C[?]

Note: Your completed flowchart should show a sequence from the user click to the UI update. Label the answers to the following questions:

  • What function makes a request to the server?
  • What is the endpoint?
  • What comes back from the server?
  • Where does that response go next?
  • What drives the UI update?
Some help if you are completely stuckLogin form submit --> handleLogin --> Sends form data to apiService.login --> Fetches token & success from /login --> calls updateState --> State updates, persists to localStorage, dispatches state-change event --> Router listens for event and --> calls Home View --> clears page with Destroy, then calls Render --> renders Profile, Timeline, and Logout components with current State

Add a console.trace(); to the home view to help you trace the flow.


3. ๐Ÿ– Sketch the System from different perspectives

The practice of sketching clarifies the mental model of the system in your mind. It doesn’t have to be a complicated drawing. The router module calls different “views”. Where are the views defined? What do they do? Here’s a quick sketch showing the relationship between the router and views:

--- config: look: handDrawn --- graph LR A[Router] -->|matches url| B{Views} B --> C[Home View] B --> D[Login View]

What about the relationship between views and components? Find the components that are called in the home view. In your notebook, draw your own diagram showing the relationship between views/home and components/*.

You could also sketch:

  • Data Flow Map: Illustrate how data moves through the system
  • Component Tree: Show the hierarchy of components
  • Dependency Map: Identify which modules depend on each other

๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ๐Ÿ‡ฌ๐Ÿ‡ง Identifying patterns

Patterns are reusable solutions to common problems.

In the PurpleForest application, there are identifiable patterns. These are rules that the original developers followed when they built the application. By understanding these patterns, you can understand how the application works and how to extend it.

Architectural Patterns

These are known solutions to common tasks in software. In Purple Forest, we could identify an MVC pattern, a SPA pattern, and a RESTful API pattern, among others.

You don’t need to memorise all these names, just be alive to the idea that there are patterns in the codebase that you should look for and reuse. You have implemented patterns many times before in previous modules without knowing their names.

Design Patterns

Formal design patterns are more commonly used in object-oriented programming (OOP).

In Purple Forest, which is not written in an OOP style, you can still find patterns like the Factory Method or the Singleton.

Code Conventions

We can also see regularities in the codebase called conventions. You can derive useful information from these conventions. Write down your answers to the following questions:

Investigate and document

  1. How are functions named? If you wanted to edit a function that handles user input, what could you search for?
  2. How are files organised? To find out how the application puts together the signup page, where will you look?
  3. Is there a pattern to the classes and ID names in the HTML? If you were to add a new template, how would you name it?
  4. Compare any two components. Is there a similarity in their structure? How would you write a new one?

Component Creation Pattern

Let’s identify a convention in the Purple Forest application. Read any “createComponent” function in the Purple Forest codebase. In your notebook, write down the general steps this function takes to create a component.

Play computer and think about it
// function name starts with create
// then name of file
// function expects a template (id) and data as arguments {
// first, return if there's no data
// next, clone template to create a fragment
// then, populate the fragment with data
// return fragment
//}

Check your specification against another “createComponent” function. Does the pattern hold?

๐Ÿ’กTip

Once you identify a pattern, you can predict how other parts of the system will work.

Why Patterns Matter

Patterns help you:

  • Predict how other parts of the system work
  • Guide your implementation of new features
  • Spot potential problems where patterns are broken

Understanding patterns allows you to chunk๐Ÿงถ๐Ÿงถ chunkChunking is a way to group information together so it’s easier to remember and understand. information, making complex codebases easier to comprehend.

๐Ÿ› Debugging: Proposing and Discarding Hypotheses

Learning Objectives

Bug Report ๐Ÿ”—

Bug report

๐Ÿ‘ค When I click on hashtags, the page flashes blank on and off

User provided details

  • User agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
  • URL: #/hashtag/do
  • :gear: bug
  • ๐Ÿ• Priority Mandatory
  • ๐Ÿ‚ Size Medium
(You could take a moment to refine the issue report. )

๐Ÿง  Recall that debugging is about forming and testing hypotheses. Each test brings you closer to understanding the system’s intended and actual behaviour.

Remind yourself of your debugging skills
--- config: look: handDrawn --- graph LR A[Predict] B[Explain] C[Try] D[Compare] E[Update] A --> B B --> C C --> D D --> E E --> A

You have used this strategy many times before at CYF , and loads of debugging too.

With the application running, reproduce the issue by creating a bloom with the hashtag #do and navigating to /#/hashtag/do. Open Devtools and trace the request flow, just as we did in Navigation.

Yikes! As soon as we open the Network panel we can see the app is making many many many requests!

Prediction: there’s some kind of loop in the system that’s causing the page to refresh over and over. Explanation: the network panel is showing a lot of requests to the same endpoint.

We can see precisely which files are involved in this request in the call stack. This stack trace allows us to reduce our problem domain to these 5 files.

_apiRequest @ //front-end/lib/api.mjs:33
getBloomsByHashtag @ //front-end/lib/api.mjs:163
hashtagView @ //front-end/views/hashtag.mjs:20
handleRouteChange @ //front-end/lib/router.mjs:24
(anonymous) @ //front-end/index.mjs:44
updateState @ //front-end/lib/state.mjs:26

Remembering what we just learned, Cmd+P and open api.mjs, Cmd+F to jump to _apiRequest. This is a wrapper function that all these endpoints call, so it’s not likely to be the problem if only this one view is refreshing.

  1. Read getBloomsByHashtag. Is there a clue in here?
  2. What is calling this function? Use fn+Shift+F12 to navigate to hashtagView. Prediction: if we comment out the apiService.getBloomsByHashtag call, the page will stop refreshing. Try it.

This should be a clue.

๐Ÿ’กTip

Legacy code is like a crime scene. Use your detective skills to understand how it happened.

Our expected request flow is:

sequenceDiagram title Expected Flow hashtagView->>apiService: Get blooms apiService->>Server: Request Server-->>updateState: Update State-->>Router: Event Router-->>hashtagView: Render once

hashtagView calls apiService.getBloomsByHashtag which calls _apiRequest which makes a request to the server. Success updates the state which dispatches a state-change event that the router listens for and calls hashtagView again to render the page with the blooms.

But our actual flow is:

sequenceDiagram title Actual Flow (Loop) hashtagView->>apiService: Get blooms apiService->>Server: Request Server-->>State: Update State-->>Router: Event Router-->>hashtagView: Render Note right of hashtagView: Loop starts hashtagView->>apiService: Get blooms again apiService->>Server: Request again Note right of Server: Endless cycle...

hashtagView calls apiService.getBloomsByHashtag which calls _apiRequest which makes a request to the server. Success updates the state which dispatches a state-change event that the router listens for and calls hashtagView that calls apiService.getBloomsByHashtag which calls _apiRequest which makes a request to the server…

This is where debugging legacy code can be faster than a greenfield application. This application is working, and other views don’t have this problem. So we can look at other views, and spot the difference. In views/profile.mjs for example, we call the apiService inside a conditional:

// Only fetch profile if we don't have it or if it's incomplete
if (!existingProfile || !existingProfile.recent_blooms) {
  apiService.getProfile(username);
}
  1. Hypothesis: The hashtagView is calling apiService.getBloomsByHashtag multiple times.
  2. Test: Comment out the apiService.getBloomsByHashtag call in hashtagView.
  3. Result: The page stops refreshing… and is also blank.
  4. Conclusion: The loop is caused by hashtagView calling apiService.getBloomsByHashtag multiple times.

Uncomment the line and don’t make any further changes to the codebase. Move on to the next step.

๐Ÿงช Capturing behaviour in tests

Learning Objectives

Tests are the best documentation for how a system should behave.

Describe the Behaviour, Capture your Understanding

Now we have an understanding of the system, the bug, and the intended behaviour, we need to write a test to capture our understanding. There are some Playwright tests in the codebase already, but no project has complete test coverage for absolutely every eventuality.

Launch the test runner (look in package.json to find the command). Find the file to add your test and describe the behaviour you expect.

๐Ÿ“ Activity: Write a test for the hashtag endpoint

// Given I am logged in
// When I navigate to /front-end/#/hashtag/playwright
// Then the number of requests should be fewer than 4
You might find this a bit tricky if you're new to end to end testing, so here's a test to copy if you get stuck.
test("should not make infinite hashtag endpoint requests", async ({ page }) => {
  // ===== ARRANGE
  const requests = [];
  page.on("request", (request) => {
    if (
      request.url().includes(":3000/hashtag/playwright") &&
      request.resourceType() === "fetch"
    ) {
      requests.push(request);
    }
  });
  // ====== ACT
  // When I navigate to the hashtag
  await page.goto("/front-end/#/hashtag/playwright");
  // And I wait a reasonable time for any additional requests
  await page.waitForTimeout(200);

  // ====== ASSERT
  // Then the number of requests should be 1
  expect(requests.length).toEqual(1);
});

Your test should be failing because your system is making too many requests. This is a good thing! It means you have a clear goal for your next step.

๐Ÿ”ง Fixing: Targeted Changes with Test Support

Learning Objectives

The fixing cycle protects against regressions

--- config: look: handDrawn --- graph LR A{Identify Issue} --> B[Write Test] B --> C[Write Fix] C -->|Tests Pass| E(Document Fix) C -->|Tests Fail| F[Revise Fix] F --> C

We have identified our issue and written our test/s. We’ve really done all the hard work already. Fixing the actual code is now simple.

Fix your code!

If you are really stumped, here's the fix

On line 20 of views/hashtag.mjs, only fetch if the hashtag has changed.

if (hashtag !== state.currentHashtag) {
  apiService.getBloomsByHashtag(hashtag);
}

If you were tempted to write something like if (state.hashtagBlooms?.length), consider: what would happen if you navigated to a hashtag view with no matching blooms in the database?

Try it and look in the network tab. Yikes! Another infinite loop. A conditional written presuming there is always content available is risky with user generated content. You can’t rely on users!

What extra test could you write to cover this case? Write it.

โœ๐Ÿพ Document your fix in your PR message.

Once you have got the entire fix working end to end with tests, open a PR with your changes. In your PR message, write everything that you needed to know to solve this problem. Be good to the reviewer, they’re a good friend of yours.

๐Ÿ“… Schedule a revision

Schedule a revision in your calendar for one week from today to come back and review your own PR.

๐ŸŒฑ Extending: Adding Features the Right Way

Learning Objectives

๐Ÿ’กWhen in Rome

When adding features to legacy code, write code that looks like it belongs. This isn’t the time to introduce radically different approaches or programming paradigms. That would make our code harder to navigate and understand.

For Purple Forest, this means:

  1. Following the component-based architecture
  2. Using the single source of truth pattern for state
  3. Extending the API service for data fetching and updates if necessary

In your backlog, you have some more features to add. Let’s do one simple feature extension together. Branch from main to feature/unfollow.

Add an “Unfollow” button to the Profile component. This button should remove the current user from the list of followers. The button should only appear if the current user is following the user.

Given a profile component sample
And I am logged in as sample2
And sample2 is following sample
When I view the profile component for sample
Then I should see a button labeled “Unfollow”
When I click the “Unfollow” button
Then I should no longer be following sample
And the unfollow button is not visible And a “Follow” button should be visible

The tabs contain sample code for each step of this process, but you should write your own implementation, based on your understanding of the Purple Forest codebase.

Before you start coding, open each file you think you will need in your editor. You will need to touch 5-7 files only. Use what you understand about the system to predict which files these will be.

test("allows unfollowing a user from their profile", async ({ page }) => {
  // Given a profile component sample
  // And I am logged in as sample2
  await loginAsSample2(page);
  // And sample2 is following sample
  await page.goto("/front-end/#/profile/sample");
  await page.click('[data-action="follow"]');

  // When I view the profile component for sample
  // Then I should see a button labeled "Unfollow"
  const unfollowButton = page.locator('[data-action="unfollow"]');
  await expect(unfollowButton).toBeVisible();

  // When I click the "Unfollow" button
  await unfollowButton.click();

  // Then I should no longer be following sample
  const followerCount = page.locator("[data-follower-count]");
  await expect(followerCount).toHaveText("0");
  // And the unfollow button is not visible
  await expect(unfollowButton).toBe("hidden");
});

Commit your changes to your branch.

๐Ÿ  index.html

Find the follow button and, and following its patterns, add an unfollow button next to it.

๐Ÿชช components/profile.mjs

handleUnfollow: Add a new function to handle the unfollow action. Find the follow handler and use it as a template.

createProfile : For each line that creates the follow button, add a line that creates the unfollow button. For example, after:

const followButtonEl = profileElement.querySelector("[data-action='follow']");

add

const unfollowButtonEl = profileElement.querySelector("[data-action='unfollow']");

Don’t feel tempted to optimise this yet. You can refactor later.

๐Ÿฑ views/profile.mjs

You should have exported the handleUnfollow function from the profile component. Now you need to import it into the profile view and call it when the unfollow button is clicked.

Find the follow button event listener and add a similar listener for the unfollow button.

Commit your changes to your branch.

Your test is still failing. Make a prediction about what will happen when you click the unfollow button. What will you need to change to make the test pass?

Use your debugging skills to find out.

(You could have done this from the back to the front, we just happened to start at the front end.)

You have written an interface, but you haven’t connected it to anything in the back end! There’s an API endpoint in the apiService, but is there an matching endpoint in main.py? Go look.

Find the @route for follow in main.py and use it to make a new route, directly underneath, called unfollow. Play computer with each line of code so you are sure you understand what is happening.

@app.route("/unfollow/<unfollow_username>", methods=["POST"])
@jwt_required()
def unfollow(unfollow_username):
    username = get_current_user().username

    if unfollow_username not in users:
        return make_response(
            (f"Cannot unfollow {unfollow_username} - user does not exist", 404)
        )

    follows.remove(username, unfollow_username)
    return jsonify(
        {
            "success": True,
        }
    )

Will this make the test pass? Make a prediction and then go look.

โœ๐Ÿพ Document your feature in your PR message.

Once you have got the entire feature working end to end with tests, open a PR with your changes. In your PR message, write everything that you needed to know to build this feature. Be good to the reviewer, they’re a good friend of yours.

๐Ÿ“… Schedule a revision

Schedule a revision in your calendar for two weeks from today to come back and review your own PR. You might choose to refactor your feature in your review.