My Brain on Code - Part 1

My Brain on Code - Part 1

It's been some time since my last rambling, as I've been head-deep in multiple demanding projects. Ironically, as some of you might relate, I often find clarity amidst chaos. Being continuously engaged, and juggling between tasks, I keep coming to the conclusion that I think better when distracted. It might sound counterintuitive, but it has been a boon in disguise. This bustling period has offered me precious moments of introspection, allowing me to reflect on my approaches and methods in the coding world.

One significant insight has been my deep-seated desire to preempt surprises, especially those dreaded out-of-hours calls that all developers dread. As I’ve ventured through various projects, from the compact to the sprawling, I’ve fine-tuned my strategies for foreseeing challenges, managing scale, and building efficient solutions.

So, I've decided to put together a two-parter! In these, we'll delve into the dynamics of assessing solution scale and how our mental processes evolve between smaller and larger technical undertakings. This series is my attempt to crystallize and share these strategies with you, in hopes that you'll find them as beneficial as I have.

Let's dive in, starting with understanding the potential magnitude of a project, the art of breaking colossal problems into manageable pieces, and the nuanced dance of when and how to abstract. Here’s to learning, sharing, and avoiding those unexpected midnight calls!

Grasping the Scale of the Solution

The start of any project I undertake is often the most challenging. That initial step, akin to the first brush stroke on an empty canvas, is teeming with unknowns. While the limitless possibilities excite me, they also come rife with challenges. Many times, I've found that the most daunting task isn't the act of coding but staring at that blank screen, grappling with where and how to begin.

🧐
“Just slap anything on when you see a blank canvas staring you in the face like some imbecile. You don't know how paralyzing that is, that stare of a blank canvas is, which says to the painter, ‘You can't do a thing’. The canvas has an idiotic stare and mesmerizes some painters so much that they turn into idiots themselves. Many painters are afraid in front of the blank canvas, but the blank canvas is afraid of the real, passionate painter who dares and who has broken the spell of `you can't' once and for all.”- Vincent van Gogh

Famous artists, writers, and thinkers have eloquently spoken about the intimidation of the blank slate. Leonardo da Vinci's words often echo in my mind: “Art is never finished, only abandoned.” To me, it signifies that every start is merely a phase in the journey. Similarly, Edward Hopper's sentiment, “If I could say it in words, there would be no reason to paint,” hits home. In the world of coding, sometimes a solution's grace or intricacy defies words—it must be meticulously crafted.

This is why I find it crucial to hit the ground running. Even a basic prototype or a few initial code snippets offer something tangible to critique. It's less about getting off to a flawless start and more about simply taking that first step. By producing something, however preliminary, I start shaping my vision of the project's scale, direction, and potential hurdles. This proactive approach helps me pierce through initial uncertainties, drawing me closer to a clearer path.

As I delve further into any project, grasping its potential scale becomes a top priority. I often ask myself: Is this a sprint or a marathon? Will it stand alone or morph into a complex framework? Understanding this early on gives me the foresight to channel my efforts productively. Even if my early assessments aren't entirely accurate, they offer a reference point, allowing me to adjust my strategy as I gain deeper insights.

So, what am I really looking for when trying to comprehend the scale of a project?

  • Risk Management: By accurately gauging a project's scale, I can pinpoint risks sooner. This insight aids in everything from resource allocation to deadline setting, and even spotting technical pitfalls.
  • Efficient Resource Allocation: With a clear picture of a project's size and complexity, I can more wisely allocate resources, from hardware and software to manpower, time, and funds.
  • Stakeholder Expectation Management: I've learned that misaligned expectations can quickly cause friction. Thus, by fathoming and then conveying the project's scale, I ensure everyone involved is on the same page.
  • Future-proofing the Solution: A distinct understanding of scale guarantees my solutions are designed with an eye to the future, making them adaptable to forthcoming growth or alterations without massive revamps.

And how do I set and meet these expectations?

  • Initial Requirements Gathering: I start by immersively exploring the project's requirements. This involves connecting with stakeholders via interviews, surveys, and workshops. My goal is to unearth not just the overt needs but also the unspoken, forward-looking ones.
  • Prototyping: I always aim to produce a rudimentary prototype. This physical representation offers preliminary insights into the project's depth and scope.
  • Feedback Loops: I believe in consistent engagement with stakeholders and team members. Iterative feedback not only keeps everyone in sync but also ensures early correction of any misunderstandings.
  • Historical Data and Comparative Analysis: I often look back at similar projects I've undertaken. Their challenges, triumphs, and pitfalls often shed invaluable light on the current venture.
  • Engage Experts: For areas outside my expertise, I don't hesitate to consult with domain authorities. Their specialized knowledge often highlights aspects I might have missed.
  • Modularity in Design: I always prioritize modularity in my designs, ensuring they're flexible and can scale in alignment with evolving requirements.

In the unpredictable landscape of coding and solution design, while the exact scale might be a moving target initially, these strategies serve as my guiding light. They empower me to navigate the terrains of any project with confidence and precision.

The Smaller the Better

Every seasoned coder or architect will attest: facing a colossal technical problem can be overwhelming. It's like staring up at a mountain peak, wondering how on earth you'll make the ascent. But the more projects I've undertaken, the more I've recognized a vital truth — those mountains are scalable. The key? Breaking them down into manageable hills.

Why Bother with Decomposition?

Firstly, let's address the "why". When I look at a task in its entirety, it's easy to become lost in its complexity. However, by dissecting it, I can:

  • Reduce Cognitive Load: Each smaller task requires less mental juggling, helping me maintain focus and clarity.
  • Achieve Quicker Wins: Smaller tasks often lead to quicker solutions, giving me a steady stream of accomplishments, which is a great morale booster.
  • Facilitate Collaboration: When a problem is divided, it's easier to delegate tasks or collaborate with peers, tapping into diverse expertise.

Strategies I Employ to Break Problems Down

Over the years, I've honed several strategies to make problem decomposition more effective:

  • Understand the Core Objective: Before anything, I clarify the primary goal. It acts as my North Star, guiding me as I segment the problem.
  • Sketch It Out: Whether it's on paper, a whiteboard, or using a digital tool, visually mapping out a problem can be revealing. I can often see patterns, dependencies, and logical breaks. Often this happens as I daydream during initial discussions...apologies if it seems like I am not paying attention.
  • Start Broad, then Narrow Down: I begin by identifying major components or modules. From there, I delve deeper into each, further breaking them into sub-tasks or functions.
  • Prioritize: Not all sub-tasks are created equal. I evaluate which elements are pivotal and tackle those first. This not only streamlines my workflow but often illuminates solutions to secondary issues.
  • Iterative Refinement: After my initial breakdown, I revisit and refine. Often, upon a second or third look, I'll see new ways to segment or consolidate tasks.

Recognizing and Navigating Challenges

Decomposition isn't without its hurdles. Sometimes, even after breaking down a problem, I find that a segment is still too vast or unclear. In these instances:

  • Seek External Perspectives: I find discussing the problem with colleagues or mentors invaluable. Fresh eyes can offer new angles or insights.
  • Research: There's a wealth of knowledge out there, from online forums to scholarly articles. If I'm stuck on a segment, I often delve into research to see how others have approached similar challenges. ChatGPT and other AI tools have also become additional tools for this purpose.
  • Prototype: When theory becomes muddled, practical experimentation can clarify. I'll often build a rudimentary prototype of a solution segment to test its viability.

While massive technical challenges can be daunting, they're far from insurmountable. By adopting a systematic approach to decomposition, I've found that I can navigate even the most intricate problems with precision and efficiency. Remember, every mountain is made up of smaller stones and pebbles; it's just about figuring out how to arrange them.

To Abstract or Not To Abstract

Imagine you're creating a system to manage books in a library. At first, you're just concerned with physical books.

class PhysicalBook {
    constructor(public title: string, public author: string, public ISBN: string) {}

    checkout() {
        console.log(`${this.title} has been checked out.`);
    }

    returnBook() {
        console.log(`${this.title} has been returned.`);
    }
}

Later, the library decides to add eBooks. Anticipating more types of books in the future (like audio books or magazines), you prematurely abstract a "Book" class.

abstract class Book {
    constructor(public title: string, public author: string) {}

    abstract checkout(): void;
    abstract returnBook(): void;
}

class PhysicalBook extends Book {
    constructor(title: string, author: string, public ISBN: string) {
        super(title, author);
    }

    checkout() {
        console.log(`${this.title} has been checked out.`);
    }

    returnBook() {
        console.log(`${this.title} has been returned.`);
    }
}

class EBook extends Book {
    constructor(title: string, author: string, public downloadLink: string) {
        super(title, author);
    }

    checkout() {
        console.log(`${this.title} is ready for download at ${this.downloadLink}.`);
    }

    returnBook() {
        console.log(`${this.title} has been marked as returned.`);
    }
}

The abstraction might seem okay at first. However, what if eBooks have a DRM expiration, or they require a specific format download based on the reader's device? Additionally, what if audiobooks enter the mix, and they involve streaming links or specific file formats?

As the complexity of different book types grows, our Book abstraction might end up being too general or might need frequent refactoring to account for the unique properties and behaviours of different book formats. It could have been more practical to wait and see the commonalities and differences between various types before committing to an abstract class.

What lessons can we learn from this contrived example?

Don't Predict Too Far Ahead: While it's beneficial to design with future needs in mind, trying to anticipate every potential requirement can lead to over-complicated code structures.

Look for Genuine Commonalities: Instead of forcing shared behaviours on classes (like all books having a checkout or return method), it’s more practical to wait until genuine shared behaviours or properties emerge across multiple classes.

Refactoring Isn’t Failure: If you initially design without abstraction and later realize an abstraction would be beneficial, refactoring to introduce it is entirely acceptable. Sometimes, the real-world use of the system provides the clarity we need.

Remember, it’s all about creating a system that’s maintainable and adaptable with the least amount of friction. Always prioritize clarity and simplicity over an anticipated, complex future need.

Wrapping Up Part 1

Stepping back and reflecting on our journey, building technical solutions is as much an art as it is science. Whether it's understanding the scale of our project, breaking down mammoth tasks, or knowing when (and when not) to abstract, our choices often dictate the longevity, maintainability, and adaptability of our solutions.

But it's not merely about best practices or optimal patterns. It's about our relationship with the code and the projects we nurture. We must approach them with a blend of passion, patience, and pragmatism. Remember the intimidation of that blank canvas? It's a testament to the challenges we embrace head-on and the opportunities that await.

I've shared some strategies and insights that I've collected over the years, but it's crucial to remember that every project, every team, and indeed every developer, brings a unique perspective to the table. What works for one might not work for another. That's the beauty of our craft: the endless opportunity to learn, adapt, and grow.

I hope these reflections offer you some guidance, a fresh perspective, or at the very least, a moment of resonance. As always, I'm eager to hear about your own experiences, insights, and the strategies that have helped you thrive in the intricate dance of software development.

If you found this interesting, consider subscribing and you'll be notified when Part 2 becomes available! Thanks a bunch ;)