AHA Programming

Are you the kind of programmer who prefers to never see the same code in two places, or do you make liberal use of copy/paste? Many developers swear the Don't Repeat Yourself (DRY) philosophy while others prefer to Write Everything Twice (WET). But which of these produces more maintainable codebases? I've seen both of these approaches lay waste to codebases and I have a new ideology I would like to propose to you: Avoid Hasty Abstractions (AHA). In this keynote, we'll talk about abstraction and how you can improve a codebase applying and creating abstractions more thoughtfully as well as how to get yourself out of a mess of over or under-abstraction.


Transcript


Intro


Hi, everyone. My name is Kent C. Dodds and I am super excited to be talking to you virtually. I hope that you're all healthy and happy and doing well. And I'm excited to be talking with you about AHA Programming. So go ahead and take your DRY and your WET programming principles, set them to the side for a little bit and let's talk about Avoid Hasty Abstractions, AHA Programming.

So I've got some links in here that might be interesting to you, like my slides. And I'm coming to you from Utah. I've got a wife and four kids and a dog and they're awesome. I've got a website on the World Wide Web and, in particular, testing javascript.com. If you haven't tried it already or looked at it, definitely give that a look. It'll teach you everything that I know about testing, which is not a small amount. And EpicReact.dev is going to be even bigger, such a huge amount of content that will be available to you at EpicReact.dev in the very near future. So look forward to that. Take a look at the rest of this stuff later. Let's go ahead and get into this.


Live coded contrived example of the lifecycle of an abstraction


[01:28] So this is what we're going to be covering today. This is a live coded contrived example of the lifecycle of an abstraction. So, hopefully, you can relate to this, even though it's a little bit contrived, but I think that you'll enjoy it nonetheless. And we're going to be considering what's important and why it's important to be thoughtful about an abstraction. And we're, basically, going to be taking this story ... Oh, it's an actual example of a story told Sandi Metz in this blog post around Wrong Abstraction that I strongly advise you take a look at.

We're not going to be going through slides. Most of this is in my text editor here and it's not passively consumable. So you need to decide right now whether you're going to be focusing on Twitter or on my talk, because you won't be able to do both very effectively. So just choose. I'm not offended.

[02:17] So here we are in 02.js and I'm going to be using a tool called Quokka.js. It's an extension for VS code and, among other things, it allows me to console.log in here and it will show in blue what the value of that log statement is. And so that, we're going to be using quite a bit.

So we have an application and we have a user object here. This is Phil and Phil has a name and a username, and in three different places of our application, we've got code for pulling that user's username for the first and the last name. So this is their "Display Name" that we're going to be displaying throughout the UI of our application.

[02:54] Now, you may notice we actually have a bug in here and this is our first part of the abstraction. Here, we're saying "Philip undefined," but Philip's name is Rodriguez, that's his last name. And he is frustrated that it says "Philip undefined" when he logs into our app. And so we've been given the task to fix this problem. And when I was working at a company before, I remember they would say, "Hey, we've got this bug on this page." And so I'd go fix it. And then the QA manual tester over there would say, "Hey, Kent, I thought you said you fixed it." And I said, "Yeah, I did." He said, "Well, it's broken over here." "Oh, shoot. Yeah. I guess that was copy pasted. So let's go fix it over there too." And it was a real pain.

So we build abstractions so that we don't have to fix the same bug in lots of places. And we have three places where we're doing the exact same thing. So rather than fixing the bug right here, how about we just make an abstraction and we fix it in that one place and then all of the other places will get that fixed automatically.

[03:51] So let's go ahead and do that. I'm going to make a function here called "Get Display Name." We'll take a user and then we'll return this and we'll generisize it so we'll take that user, get the name and, instead of the "First and last," we'll do "First and last." So let's go ahead and we'll replace all these with "Get Display Name" for Phil and then we'll fix it in this one place last. Ta-daa, and all of the places were fixed.

We're really happy about this because it means that we don't have to fix it in all of the other places. We just fix it in this one place. And if we ever want to make changes, we can just make changes to this one function, which is awesome.

[04:32] Well, as it happens, we do actually need to make some changes. And that is if the profile page we decide, we want to include the "Honorifics." So let's say Dr. Philip Rodriguez is, "Hey, it would be really cool if my profile page says I'm a doctor. I went to a lot of schools. So I want people to know I'm a doctor."

And so the product manager comes to you and says, "Hey, we want to add the 'Honorific' to the profile page, the display name there." And you say, "Okay, cool." So you come to code and you're, "Oh, yeah, I wrote this abstraction for this." So because the abstraction exists, our natural inclination is to go into the abstraction to enhance the abstraction to support the new use case and also just to see if it already supports the new use case. So we're just naturally inclined to go to the abstraction first and we see this and we see, "Oh, there's no support for 'Honorific' so I could either remove the abstraction here or add that use case to this existing abstraction. And it's just really natural for us to prefer to use the existing abstraction for various reasons. Maybe we think that other people might benefit from this extra use case, or maybe we just feel, since we're already using the abstraction, I don't want to walk away from all of the benefits the abstraction gives me. And in a real world scenario, that could actually be quite a bit. And so you wouldn't necessarily want to just remove the abstraction just for adding one feature.

[06:00] So it seems a lot easier to just enhance the abstraction to support the use case. So that's what we're going to do. And what we'll do is we'll take "Options," but not everybody's going to pass "Options," right? So we're going to default that to an object. And I don't want to just call this "Options." We'll de-structure this and we'll just take and include "Honorofic" and we'll default that to "False." We don't want to break existing users of this abstraction. So we'll say "Include honorific here." We'll grab a "Display Name" and return the "Display Name." So that's just a regular re-factor. We haven't changed anything yet. Now, we'll take that "Display Name," and if there's "Include Honorific," then we'll say the "Display Name" is what it is. We'll put that in a template literal there, but we'll include the "Honorific." So we'll say "User.name.honorific." Great. And then down here in the profile page, we can say "Include Honorific?" "Yes." And boom, we've got it. We're super happy about this. We commit that, we get it reviewed and people are, "Wow, cool." We re-used the abstraction.

Now look at all the power that we've got with this abstraction. Let's go ahead and write a couple of unit tests to make sure that we don't break this feature that we've added. And we move right along and, in the future, somebody comes around and says, "Hey, we need to support a username for the user card. So we want it to say their name and then in parentheses their username. And so you say, "Okay, that should be pretty straightforward." We go to the abstraction again, we see that's not supported. And rather than removing the abstraction from our code, we're going to add this feature to the abstraction, but we don't want to break the existing stuff and so we're going to touch as little as possible. And we'll just add another option here for "Include Username." And we'll default that to "False," because we don't want to break existing users of this abstraction. And we'll say "If include username," ... Except you've got to spell it correctly otherwise it won't work. And we'll say the "Display Name" is the "Display Name" as it is currently and then parentheses and then whatever the username is. So user.username. Great. And then down here we can say "Include username." "True."

[08:14] Perfect. This is exactly what they wanted. We were able to encode that into the abstraction and we add a couple of tests to make sure that this is supported. And actually in the process of adding tests, we realize that there's some combinations of these options that we don't actually support or we don't actually need in our code base, but our abstraction supports. And maybe we realize that maybe we don't, but we know that those use cases are supported where you could provide both of these as "True." And so we add tests for that just to make sure nobody breaks that existing feature so in case anybody might want to use that feature in the future.

And so it's not too complicated. It's a pure function. It's really easy to test. And so we go ahead and add a test for that. And then later on we get another feature request for this code and they say, "Hey, our navigation, we want that first name to not be a first name, but to be a first initial." And so you go back to the abstraction, you say, "Okay, that's not supported today." I don't want to lose the benefits of having the abstraction. It's well tested. And so I'm just going to add my stuff here, but I don't want to break anything that already is there. I don't want to break existing codes. So I'm going to take a first initial. We'll default that to "False," so existing users of this abstraction aren't broken. And we'll take that first initial as an "Option." And we'll say, "If the first initial ..." Then we want the first name to be just the first initial.

[09:40] So what I'm going to do is we'll take this out. We'll just call this "First" and we'll actually assign that to a "Let" here for "First equals username first." And then if the first initial is "True," then we'll say, "First it's actually going to be equal to the First," and we'll slice off the first character and add a dot. Okay, cool. And then we can come down here and say "Include ..." Or actually it's a, "First initial is True," and boom, we've got all of these use cases supported our abstraction. We add a couple more tests in here and we're really happy with this.

And there are two things I want to call out about this: First off, we now have three uses of this abstraction and maybe there are more throughout the code base, but these three don't look anything alike. They have nothing in common with one another other than the fact that some of them show the first and the last name, but each one of them has very distinct differences from the others. And so this is actually pretty common to happen to abstractions is that eventually the abstraction evolves beyond the initial use case and that's not necessarily a bad thing, but all of the use cases have diverged from each other pretty significantly. And so even though our abstraction supports so many things, they actually are supporting use cases that aren't entirely related to one another.

The other problem with this is, as we're writing tests for this, we're going to be writing tests to support use cases that we don't actually have. And while that's maybe not a terrible thing, here's a problem with that. As we write all of these tests to test use cases, when we come in to re-factor this, if we want to make improvements to it, then we have to make sure that our re-factorings support everything that our tests say our abstraction supports.

[11:30] But the only thing that cares about that use case is the test. So it exists for itself, and that is super useless. The test is intended to make sure that the use cases that you need to support are continuously supported. And if the only one who cares about it is not the users, but the tests, then just delete the test and now nobody cares about it.

And so thoughtlessly adding features to this abstraction, we wind up in a really hairy situation and it doesn't stop here. No, we've got some more to do. What if the profile page no longer wants the "Honorific?" So we're, "Okay, that's fine. We can just remove the 'Honorific.'" Boom, it's gone. We're happy, we save this, commit it, push it, it's merged. We're happy about this because all it took was removing that option. And this is what normally happens. And we don't actually think about pulling out the "Honorific" option and getting rid of the code that's specific for the "Honorific." Or maybe we do think about that. And there are a couple other reasons why we might not want to remove it. For one thing, the cost of keeping it in place is pretty low, or it feels low. And the risk of removing it and accidentally breaking something, that actually feels high. And so with that cost versus risk analysis there, we just decide, "Oh, let's just keep it in there. I don't want to break anything." We do this a lot with CSS in particular, like global CSS.

[12:54] I'd much rather add something new than modify something existing or, heaven forbid, delete something existing, because it's so hard to identify if really that's being used. And then there's always the lingering thing in the back of your mind, like maybe someday we'll want to include the "Honorific" in the future. So we'll just leave that feature and then nobody will have to make any changes to support that use case in the future. And that also is problematic because we have Git and we could go back and look at what the code was at that time.

So it is not a costless thing to leave this code in place because we have to maintain it as we make refactorings to it if we leave it in place. And the only one who cares that this code exists is the code and the test itself. So if we remove them, then nobody cares and that's fine.

[13:45] So we're not quite done though. What if our user card here, instead of the first and last name they decide, "Hey, I actually just want to show the last name." Now, doing that with what we have here is actually really easy. We just remove the abstraction and then we just say. "Phil.username." But because the abstraction exists, it just draws us in and we think, "You know what? Instead of "Include username," I'll have an "Only username." And then up here I'll accept that "Only username," We'll default that to "False" so we don't break other people. And I don't want to touch any of this stuff. So I'm just going to add this here to the bottom and say "Only username. Display name equals user.username."

And there we go. We've got support for this new feature and we have now two options that nobody cares about except for the code itself and the tests that were written to make sure we don't break those use cases.

[14:40] So we've wound up with an abstraction that's actually quite a bit more complicated than it needs to be and we can re-factor it, but we've got all these tests now that are making sure that we don't break these use cases that we actually don't care about. And so it's just become a tangled mess. And this is a pretty simple function. It's just concatenating the strings together. Think about your complicated React components or your Angular components or whatever it is that you're writing and all of these different abstractions that you've built around this stuff, and it's pretty easy to build yourself up into an abstraction that's just scary to work with.


How to avoid building scary abstractions


And so how do we avoid this problem? Or how do we back out of this problem when we're in it? And this is something that Sandi Metz talks about in her blog post The Wrong Abstraction. I really advise that you take a look at this because it's really great. And she has a talk here that you can give a watch to. But, in here, what she says is that, "The fastest way forward is back." So the idea is first you reintroduce the duplication inlining and then with each color you use the parameters being passed to determine the subset of the inline code that's specific for the colors, what the color executes, and then delete the bits that aren't needed for that particular color. And so, in this case, that, basically, means we'll make three copies of "Get display name," and inline it into each one of these and then remove the pieces from each one of those that are no longer needed.

[16:07] So let's just walk through that really quick. What I'm going to do is console.log right here. And I just need to have a console.log right here that's the same as this one. And ours is pretty simple. So I don't really need to make a separate function. I'll just look at the function and grab the pieces that I need. So here we just need the first initial and then the last name. So that's, pretty much, all this code that I need for this. So what I'll do is I'll grab this right here. We'll put it right there and we're going to get the first from "Phil.name.first." Okay. So that's getting our p-dot and then the rest of this is right here. So let's grab that. And instead of "User" that's going to be "Phil."

Okay, those two things are the same. So we can grab this now and put it in place of the abstraction. Now, this situation, or this navigation file, is no longer using the abstraction. And we could come in here and delete the stuff that's not being used anymore, but let's keep going and inline the abstraction into all the places where it's used and then we can remove it.

[17:11] So the next one is just the first and the last name. And so that is right here ... Or, no, it is this part right here. So let's just grab that. We'll add a console.log right here. And the first is going to come from a "Phil.name.first," and this is going to be "Phil.name.last." And we're going to need to put that in a template literal there. There we go. Cool. And that's exactly what we need. So we'll just replace those. Get rid of that. And now we've removed the abstraction from that one. And then for our last one, it's just the username, and all that we're using from the abstraction for this is just user.dot.username. So we'll come down here and that's so easy. I'm just going to say, "Phil.username." There it is. We have the exact same thing that we had before. And, as it turns out, we can remove this whole abstraction. It's gone from our code base. That really big, hairy, scary thing that you were worried about is gone. And maybe you have it just all inlined like this. Or maybe that was a big component and you have four different copies of the same component that are just slightly different for each use case.

Now that you have those four different copies, you can see the similarities between some of them and maybe there are two different categories of that same abstract and there's the one that shows the dropdown and then there's the category that doesn't. And so we'll just keep those as separate things, whatever the case may be. But, because you've done this, you're able to identify the commonalities between the different abstractions and the differences. And in your current state in building this today, you're a lot more able to create an abstraction that works for what we have today. And because you've experienced ... You have some battle scars on bad abstractions, you're more thoughtful about the abstraction that you're making.


Conclusion


[19:02] So with all of this, I just want to wrap up with a couple of takeaways: First off, Do not Repeat Yourself, or DRY, that's not necessarily a bad thing. Do not Repeat Yourself in theory is a good idea because it does allow us to get rid of some business logic bugs in one place, or just even some typos if you're not using TypeScript, but it can really help you avoid some duplication. Duplication is not inherently bad, but it can be a problem and it can just propagate a bunch of bugs all over the place. So Do not Repeat Yourself itself is not necessarily bad.

But the key here is that you can't tell the future. So the only thing you should really be optimizing for is change. So one thing that Sandi Metz talks about is, "Duplication is far cheaper than the wrong abstraction." So preferred duplication over the wrong abstraction. I agree with that wholeheartedly and I think that, as you duplicate things and just wait for commonalities in that duplicate code to scream at you for abstraction, then those abstractions become so much more obvious for you and you can build out the right abstraction for the use cases that you have present for you at the time.

[20:15] So as you're building out stuff and you have all these ideas like, "Oh, cool abstraction here. Cool abstraction there," just keep duplicating stuff, copy, paste, move it around, and then once you're all done, you can see what you have and you see the commonalities and be, "Oh, actually these two things are not as common as I thought they were when I first wanted to abstract them. So I'm glad I didn't. We'll just leave them as separate things." Or, "These two things are really common. There's just three things that I could parameterize," and then you can make an abstraction for it.

If you have shared code with lots of branches, then I recommend to you to resist the urge to add more conditionals to it and instead re-factor it first. And go and learn everything that you can about the users of that abstraction. And maybe there's something that you can learn about the abstraction as a whole so that you can maybe split the abstraction up into multiple pieces that will be easier to manage themselves.

[21:11] So I've got a couple resources for you: Sandi Metz gave a talk that is really good around this same idea called All the Little Things. Definitely give that a look. And then the blog post, of course, is also great. And then I have a blog post about this concept, AHA Programming, you can and check out on my blog and I have a testing variant of that, as well. And yeah, follow me on Twitter because I tweet things. Thank you very much. I hope you have a wonderful time at the conference. Stay happy, stay healthy and subscribe. Thank you.


Questions


[21:45] Jason Lengstorf: All right. That was excellent. Kent, as always, that is a wonderful talk. If you are new to the AHA Programming concept, I think it's such a cool thing. I always love seeing what Kent does. And so now let's bring Kent back to the stage and we will do some Q&A. We've got some great questions in the Slack. Kent, welcome back.

[22:09] Kent C. Dodds: Thank you. Thank you. I'm so excited to be here and I always enjoy spending time with you, Jason, and our 4,000 friends or however many people are watching right now.

[22:20] Jason Lengstorf: Yeah, I think we're up above 4,000 right now on the live stream. So, holy crap. So I had a question because actually I've been following you for a long time and I remember the origin of AHA and so I'm going to put you on the spot a little bit. But AHA wasn't always called AHA. So can you talk a little bit about the origins and how you got to now?

[22:44] Kent C. Dodds: Yeah. So AHA was my frustrations with DRY programming as a practitioner early on and I learned about Do not Repeat Yourself and how important it is that you create abstractions for things. And then I found out what ... I think most people find out with that is eventually the abstractions you create are really bad. And then I heard about WET programming, which is Write Everything Twice. And I was frustrated that, as well, because then you have to fix bugs in multiple places. And so I just thought they were both way too dogmatic. And so I decided let's just be more mindful of the abstractions we make and then everything will be better, hopefully. And so in the process of writing a blog post about this, because I have to write a blog post about everything that I think of, I decided, "Okay. Well, it's not DRY. It's not WET. What's not those things? I guess, it's moist." And so that's what I called it at first, was Moist. And I don't really like the sound of that word and I know a lot of people see it really cringy, but I was, "Hey, it's okay." And I couldn't really think of what that could stand for. So I didn't even make what that acronym is. And I tweeted about it and I got so many people, "That is so gross," or just laughing hysterically about it. So I was, "Okay, okay. I need to come up with a different name." And Cher Scarlett gave me a perfect name, and AHA, Avoid Hasty Abstractions. It was perfect. And it's light bulb moment. I love it for every ... It's just the perfect acronym. So that's where that came from.

[24:30] Jason Lengstorf: Ah, that's great. You briefly mentioned this, but I want to talk ... Well, actually, you know what? You answered it. I'm not going to talk. You talked about the difference between DRY and AHA. So let's go to the Slack and someone asks ... There's a great question here: "How do you implement this project-wise? If you were going to roll this out into a given project, how do you work that into your process?"

[22:54] Kent C. Dodds: Oh, that's such a great question. I think that it's so tempting when we're ... If you're starting a brand new project or you're coming into a project and you're, "Wow, look at the mess of abstractions that we've got here," or whatever, it's just so tempting for you to instantly want to architect the entire thing from the very beginning. And that is an enormous mistake to make. You automatically ... You have a code base of 3000 lines of codes so far and you are architecting it for a three million line code base. You're going to wind up in bad abstractions and you'll never make it to that three million line code base. So it's not one that you want to work in.So I recommend just taking a really iterative approach. Don't worry about duplication. And if you come into a code base and it's got bad abstractions, then what I talk about in the talk and referencing Sandi Metz there, inline those abstractions, and then you can just pretend that it's always been this way, and you're, "Oh, wow, there's an abstraction just sitting here for me," but it'll be totally different from the bad one that you inlined before. So, at least, hopefully, it will be. But that's what I'd recommend, is just be really thoughtful about the whole process. Don't try to architect your whole application for something that it's not today.

[26:14] Jason Lengstorf: So a question here that is, I think, ... "In the context of an abstraction, do you feel abstraction should not be extended? Or would it be better to just extend it and see what happens until you end up with three to four functions and then change?"

[26:32] Kent C. Dodds: So maybe I'm not totally understanding the question, but my thought process normally, and this is something that I really had to fight for, but as I'm working on some code, before I've committed anything, I'm just toying around with stuff, I am constantly thinking, "Oh, this looks a lot like this and so let's just put that in a function." And I really had to fight myself and say, "No, no, no, no, no. Don't do that yet. Just copy it." Even if it's five lines of code, it seemed so duplicate, just copy, paste that because eventually you'll find out one of two things: You'll either find out that you didn't need it at all in the first place. And so taking the time to make the abstraction, create the variable names and genericizing that function was a waste of time anyway. Or you find out that they weren't as similar as you thought they were. So I don't know if that answers the question because maybe I misunderstood it, but that's just-

[27:27] Jason Lengstorf: Well, I'd like to follow up on that actually. So when you say that you realize that they're not as similar as you thought they were, do you have an example? Because I feel that's one of those things that it's easy to say in the abstract and hard to put into concrete terms. So if somebody's thinking about this, when's the case when the same code that you've copy pasted isn't as similar as we think it is?

[27:52] Kent C. Dodds: Yeah. That's a great question. So an example of this that I just experienced recently, and especially it's a React conference, so a React example, I had a "Login" and a "Register" button, very similar, just different words or different Reo labels for the modal, they pop up, different colors and stuff. And I thought, "These are really similar. I could make a React component, just take a couple of props. But I've held off, and I found that if I were to make a component for this, there would be so many little props that are this is what the Reo label should be. This is what the title of the modal should be. This is what the type of button it should be. And I just don't see that type of an abstraction being any simpler than the duplication that I have instead. Each one is six or seven lines of code. Now, there is a little piece in there that the modal that it pops up has a little "Close" button that is styled specifically for the "Login" and "Register" modals. And so all I did for that was I just extracted the CSS for it because there's no variables or anything ... Or not the CSS, but the JSX for it. So I just created a JSX element and then I inlined that element as a variable. And so I'm able to take that little piece of commonality without making it a whole function component that has 12 props on it.

[29:17] Jason Lengstorf: That's a really interesting distinction too, because in both cases you're talking about code that would visually look the same, what shows up in the browser, it visually looks the same. It's a "Close" button or it's a registration form, but the implementation is where the difference starts to come in. If you have to change every part of those attributes, you're not really writing an abstraction, you're just giving yourself chores.

Kent C. Dodds: Yes, yeah. Exactly.

Jason Lengstorf: Yeah, that's-

[29:42] Kent C. Dodds: Anytime you want to make anything that makes them slightly different you have to add another argument, another prop and it's just more homework for you. And, at the end of the day, what you end up with is either copy pasted JSX that's just two copies of almost the same thing. Or-

Jason Lengstorf: Right.

Kent C. Dodds: ... a function component that you're calling ... That is almost the same amount of lines of props. So what are you buying yourself there? Not much. Just chores.

[30:10] Jason Lengstorf: Totally. Totally. Okay. So another question, just following that, "Is the cost of a bad abstraction usually much worse than that of duplicating the code in tests and possibly duplicating bugs?" What's your experience with that then?

[30:27] Kent C. Dodds: That's a great question. And it's pretty nuanced because like, I don't want anybody coming away from my talks saying, "Oh, Kent likes duplication. He hates abstraction." That's absolutely not the case. As a library author, clearly, I make many libraries and I see the value of abstraction. I just see there comes a point where the duplication becomes a real problem. For this "Login" "Register" example that we just shared, that duplication, it's right there, and if there's a bug in one, it's really easy to just fix the bug in the other. Not too much of a problem. But if you find yourself duplicating over many, many files, then maybe there's a good case for an abstraction there. So I can't give you a rule because there is really no rule about this. It's all just nuanced and ... Sorry. That's not very helpful, but hopefully, the talk was.

[31:23] Jason Lengstorf: No, I think that's great. And with that, we are going to start moving on. That's all the time we've got. Kent, thank you so much. It's always a pleasure and we'll see you-

Kent C. Dodds: Thank you.

Jason Lengstorf: ... in the Slack.

Kent C. Dodds: All right. See you.

Kent C. Dodds
32 min

Check out more articles and videos

Workshops on related topic