ISSUE 2Hey Reader, Can you believe it's almost August? That's correct; month 8 out of 12. Where is time even going in such a hurry? In this week's issue, we'll talk about abstractions, design patterns, and the WET codebase (you read that right, wet.) Let's jump in. SOFTWARE DESIGNGood Abstraction, Bad AbstractionEarly on in my career, I thought creating abstractions was all about keeping things DRY (Don’t Repeat Yourself.) Whenever I saw two blocks of code that looked alike, I'd do my best to remove the duplication by creating a single function that could handle all use cases. But there’s a problem with this approach—it doesn’t scale very well. As requirements change and the codebase evolves, this model begins to fall apart. If we start to change our functions to accommodate completely different use cases, our well-intentioned attempt at code reuse quickly turns into the wrong abstraction. So what’s the secret to creating a good abstraction? In a nutshell, it’s about focusing less on reusability and more on what abstractions are truly for—managing and encapsulating complexity. And a great way to do that is to look out for these three fundamental qualities of any good abstraction:
Let’s talk about them. Good Abstractions Are Made of Deep ModulesA deep module is one that provides powerful functionality via a simple interface. We can think of an interface as the cost of an abstraction and the functionality as its benefit. Deep modules make for better abstractions because they provide more functionality at a lower cost. By contrast, a shallow module is one with a relatively complex interface compared to its functionality. If a function does too little (e.g., a “pass-through” method), this could mean that we’re dealing with a shallow abstraction. Preferring deep modules doesn’t mean that we should aim for large functions. If a function only has a few lines of code but encapsulates more complexity than it exposes via its interface, we still consider it deep. Good Abstractions Hide Unimportant DetailsAn abstraction should hide unimportant implementation details, but it should reveal the important ones. Here’s an example: You’ll notice that this function takes an axios config object as a parameter. There’s nothing inherently wrong about that, but in this particular case, the fact that the function uses axios to make a network request is an implementation detail that users of the abstraction shouldn’t concern themselves with. Another thing you might have noticed is that this function behaves asynchronously when the user is logged in but synchronously when they’re not. This is also a detail of the implementation, but it’s an important one that should be more obvious to users of the abstraction. Why is that detail important? Imagine adding an item to the cart and then immediately calculating the cart's total. You'll get two completely different results depending on whether the sync or async behavior was called. An alternative version of that function could look something like this: We're constructing the axios config object within the same function here for simplicity. You'd probably want to handle this separately in a real use case, but that's beside the point. The important thing to note is that we’ve fixed the leak in our abstraction by specifying the exact configuration options available. Now we could change the implementation of this function to use fetch instead of axios, and it wouldn’t change its interface at all. Additionally, now that the function always works asynchronously, we’re making its behavior more consistent and evident to its users. Good Abstractions Operate at a Single LevelMixing levels of abstraction makes your modules more unpredictable and harder to work with. A function that updates your database shouldn’t also update the DOM. These are two different responsibilities that should be split accordingly. Even if your framework of choice allows for these two behaviors to coexist in the same module, it’s always a good idea to separate the concerns of each abstraction by splitting the logic into smaller, more focused functions. The Single Responsibility Principle is worth calling out as well. A great way to ensure we're operating at the same level of abstraction is to give each module only one reason to change. Does this mean that all of your functions need to tick every box in the “good abstraction checklist”? Not necessarily. These should be treated as guidelines, not rules. And my hope is that you’ll use them to make your abstractions better, not perfect. After all, we’re not in the business of creating perfect abstractions (there isn’t such a thing, anyway.) We’re in the business of solving problems—and if your existing abstractions let you do that with a manageable level of complexity, then you’re already doing the important things right. WATCH LISTThe WET CodebaseI know we just came from an entire essay about abstractions, and you might be ready for a change of topic. But hang on for a few more; I promise this one is worth it. Dan Abramov gave this talk at Deconstruct 2019 (which I know feels like decades ago), but the ideas he shares in it are genuinely timeless. Dan talks about the dangers of focusing too much on keeping things DRY without considering whether the abstractions we create still make sense, and he shares some key aspects to keep in mind when dealing with any abstraction:
Dan ends the talk with his recommendations for what to watch next for a deeper dive into this topic. As a big fan of all of these talks, I +1 Dan's picks and encourage you to add them to your watch list:
ARCHITECTURE SNACKSLinks Worth Checking OutBOOKSHELFLearning JavaScript Design PatternsFinding the right abstraction for the problem you're trying to solve often comes down to choosing the right design pattern. But with so many patterns to choose from, and implementations that vary widely depending on the programming language, this is easier said than done. That's why I was so excited to learn that Addy Osmani's classic book, Learning JavaScript Design Patterns, was getting a second edition. The new edition has been updated to use the latest features of the language and includes some of the new patterns that have emerged over the past decade. In about 260 pages, you'll explore:
|
113 Cherry St #92768, Seattle, WA 98104-2205 |
Get the latest articles, talks, case studies, and insights from the world of software design and architecture—tailored specifically to frontend engineers. Delivered right to your inbox every two weeks.
Read it on the website ISSUE 10 Hey Reader, I have some exciting news to share before we start—Frontend at Scale has officially reached 1,000 subscribers 🎉 This is absolutely incredible to me, and I cannot thank you enough for being part of this journey so far. Whether you’ve been reading my ramblings from the very beginning or you just joined us last week, I want you to know how much I appreciate you hanging out with me every two weeks. If you enjoy the newsletter, the absolutely best way to...
Read it on the website ISSUE 9 Hey Reader, I hope you had a fantastic week. Mine was great—I took some time off work to enjoy the beautiful California woods with my wife and kids, which was a much needed rest but also the reason there was no Frontend at Scale last weekend. I hope you didn't miss the newsletter too much 😊 Today I'm flying out to NYC for this year's React Summit US. I'm super excited to hang out with the nice folks from the React community and to give my talk, The Messy Middle,...
Read it on the website ISSUE 8 Hey Reader, Do you have plans for this upcoming Friday, October 27th, at 9:30am Pacific? Work, you said? I'm not entirely sure what that word means, but I hope you can make some time come chat about application design in Astro with Ben Holmes and me on my Twitch channel. This is the first of what I'm hoping will become a series of talks on frontend architecture and software design with different framework authors and experts, and I couldn't be more excited to...