DEV Community

Cover image for Your Code Isn't Reba: Decoupling State from Storage
Tara Timmerman
Tara Timmerman

Posted on

Your Code Isn't Reba: Decoupling State from Storage

Robust, maintainable code starts with one rule: don’t tie your core logic to how its data is stored.

A common mistake is locking a central class to a specific storage method. What starts as a quick localStorage.getItem() quickly turns into a mess when you want to test, refactor, or adopt a new storage mechanism.


The Problem: A Model Doing Too Much

At first, my Model was doing it all. Managing state and worrying about persistence.

It was like Reba, a single mom, juggling too many roles at once. Too much for one class!

Example of tight coupling:

export class Model {
  private state: GameState = { /* ... default state ... */ };
  constructor() {
    // Directly reading from localStorage
    this.state.scores.player = parseInt(localStorage.getItem('playerScore') || '0', 10);
    // ... many more localStorage.getItem calls
  }
Enter fullscreen mode Exit fullscreen mode

Why This Was a Problem

  • 👎 Tests weren’t isolated (every test depends on localStorage).
  • ⚡️ Low flexibility (switching to IndexedDB or an API means rewriting the Model).
  • Single Responsibility violated (business logic + storage concerns combined).

Reba Meme, a single mom who works too hard


The Solution: Helping Model Get a Break

Break the coupling by introducing an interface:

export interface IGameStorage {
  getScore(participant: Participant): number;
  setScore(participant: Participant, value: number): void;
  removeScore(participant: Participant): void;

  getRoundNumber(): number;
  setRoundNumber(value: number): void;
  removeRoundNumber(): void;

  // …other getters/setters as needed
}
Enter fullscreen mode Exit fullscreen mode

Then implement it:

export class LocalStorageGameStorage implements IGameStorage {
  getScore(participant: Participant): number {
    return parseInt(localStorage.getItem(`${participant}Score`) || '0', 10);
  }
  // …
}
Enter fullscreen mode Exit fullscreen mode

The Refactor: Small, Test‑Driven Changes

With a test suite in place, the refactor became a series of manageable steps:

✅ Confirm all existing tests were green.
✅ Replace one localStorage.getItem() call with gameStorage.getScore().
✅ Run the test suite. Failures point to the next area that needs adjustment.
✅ Refine test setup by mocking IGameStorage where needed.
✅ Repeat until every piece of state was abstracted.

Each test acted like a roadmap, making the refactor predictable and manageable.


The Refactored Model: Storage Abstraction in Action

export class Model {
  constructor(gameStorage: IGameStorage = new LocalStorageGameStorage()) {
    this.gameStorage = gameStorage;

    this.state.scores.player = this.gameStorage.getScore(PARTICIPANTS.PLAYER);
    this.state.scores.computer = this.gameStorage.getScore(PARTICIPANTS.COMPUTER);
  // ... no more direct persistence calls; Model is now truly persistence-agnostic
}
Enter fullscreen mode Exit fullscreen mode

Result: The Model no longer knows (or cares) where its data comes from. All storage details are abstracted, making it easy to evolve and test.

Reba Meme, Im a survivor


What This Shows

✅ Abstraction makes code more flexible and testable.
✅ Tests turn refactoring into a safe, incremental process.
✅ Small changes reduce risk and save time in the long run.

The takeaway: Decoupling doesn’t have to be hard. Sometimes it’s as simple as extracting a few direct localStorage calls and introducing an interface. The payoff (clarity, testability, maintainability) is worth it.

Remember: Much like Reba, who eventually finds help and balance, your code can too! 🫡

Top comments (0)