1. Introduction to AHA Programming
Hi, everyone. My name is Kent C. Dodds and I am super excited to be talking to you virtually. I'm excited to be talking with you about AHA programming. So go ahead and let's talk about avoid hasty abstractions, AHA programming. I've got some links in here that might be interesting to you. I've got a website on the World Wide Web. And, in particular, testingjavascript.com. If you haven't tried it already or looked at it, definitely give that a look. 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.
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. Unlike 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, testingjavascript.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.
2. Introduction to Abstraction Life-cycle
This is a live-coded, contrived example of the life-cycle of an abstraction. We're going to be considering what's important and why it's important to be thoughtful about an abstraction. It's like an actual example of a story told by Sandy Metz in this blog post, The Wrong Abstraction.
Let's go ahead and get into this. This is what we're going to be covering today. This is a live-coded, contrived example of the life-cycle of an abstraction. 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... It's like an actual example of a story told by Sandy Metz in this blog post, The 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.
3. Fixing the Display Name Bug
So here we are in 02.js. We have an application with a user object named Phil. There's a bug where it says Philip undefined instead of Rodrigues. We've been given the task to fix this problem. Instead of fixing the bug in multiple places, let's create an abstraction called GetDisplayName to fix it automatically.
So here we are in 02.js. And I'm going to be using a tool called CuocaJS. It's an extension for VS Code. 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 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 last name. So this is their display name that we're going to be displaying throughout the UI of our application. 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 Rodrigues. That's his last name. And he is kind of 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 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. You 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 a abstraction and we place and then all of the other places will get that fixed automatically. So let's go ahead and do that. I'm going to make a function here called GetDisplayName. We'll take a user and then we'll return this and we'll genericize it. So we'll take that user, get the name and instead of the first and last, we'll do first and last.
4. Enhancing the Abstraction for Honorific
We replace all occurrences of getDisplayName with the new function for Phil, fixing the bug in one place. However, we now need to add support for the honorific on the profile page. Since we already have an abstraction in place, it's natural to enhance it to support the new use case. We'll add an 'includeHonorific' option to the function, defaulting it to false to avoid breaking existing users. This is just a regular refactor without any changes yet.
So let's go ahead and we'll replace all of these with getDisplayName for Phil and then we'll fix it in this one place, last. Ta-da, 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.
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 honorific. So let's say Dr. Philip Rodriguez is like, hey, I would be really cool if my profile page says I'm a doctor. Like I went to a lot of school, so I want people to know I'm a doctor. 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 the code and you're like, 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. 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 like, since we're already using the abstraction, I don't want to, you know, 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, yeah, you wouldn't necessarily want to just remove the abstraction just for adding one feature. So it seems like 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 destructure this. And we'll just take and include honorific. 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 refactor. We haven't changed anything yet.
5. Enhancing the Abstraction with User Name
Now we'll take that display name and, if there's include honorific, we'll include the honorific. We've got it. We reused the abstraction. Let's go ahead and write unit tests to make sure we don't break this feature. In the future, we need to support a user name for the user card. We'll add this feature to the abstraction by including the user name.
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 like, wow, cool. We reused the abstraction. So let's take it all the power that we've got with this abstraction.
Let's go ahead and write a couple of unit tests to make sure 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 user name for the user card. So we want it to say their name, and then in parentheses, their user name. 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, so we're going to touch as little as possible. And we'll just add another option here for include user name. 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 user name, 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 user name is, so user dot user name. Great, and then down here we can say include user name true.
6. Evolving Abstraction and Test Complexity
We add tests to support different combinations of options. Later, we receive a feature request to display the first initial instead of the first name. We modify the abstraction to support this new use case without breaking existing code. However, the use cases supported by the abstraction have diverged significantly. Writing tests for non-existent use cases can complicate future refactoring.
Perfect, this is exactly what they wanted. We were able to encode that into the abstraction and we add a couple 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 code. So I'm going to take a first initial, will 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. 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 user name first. And then if the first initial is true, then we'll say, first is 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 by our abstraction, we had a couple more tests in here, and we're really happy with this.
Now 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 like 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.
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 the problem with that. As we write all these tests to test use cases, when we come in to refactor this if we want to make improvements to it, then we have to make sure that our refactorings support everything that our tests say our abstraction supports. But the only thing that cares about that use case is the test, so it exists for itself.
7. Avoiding and Fixing Abstraction Complexity
The test is intended to make sure that the use cases that you need to support are continuously supported. By thoughtlessly adding features, we wind up in a hairy situation. We often keep unnecessary code and features due to low cost and high risk of removing them. Leaving unused code in place requires maintenance. We end up with a complicated abstraction that's difficult to work with. Sandy Metz suggests reintroducing duplication and selectively removing unnecessary code as a way to avoid or fix this problem.
But the only thing that cares about that use case is the test, so it exists for itself. And that is like 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 by 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 like, okay, yeah, 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, right, 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 decided, 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. 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 can 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.
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 fill.username() but because the abstraction exists it just kind of 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. We're 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. So we've wound up with a abstraction that's actually quite a bit more complicated than it needs to be and we can refactor it but we've got all these tests now that are making sure 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 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. 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 Sandy 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 go and take a watch and 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 by 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 getDisplayName and inline it into each one of these and then remove the pieces from each one of those that are no longer needed. 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.
8. Inlining the Abstraction
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 is all that I need for this. So what I'll do is I'll grab this right here. And we're going to get the first from fill.name.first. And then the rest of this is right here. So let's grab that. And instead of user that's going to be fill. Now this situation or this navigation file is no longer using the abstraction. We'll inline the abstraction into all the places where it's used and then we can remove it. For the next one, we'll add a console.log and get the first and last name from fill.name.first and fill.name.last. For the last one, we just need the username from fill.username. Now that you have identified the commonalities between different abstractions and the differences, you're more thoughtful about the abstraction you're making. Remember, duplication is far cheaper than the wrong abstraction.
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 is all 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 fill.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 fill. 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.
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 fill.name.first and this is going to be a fill.name.last and we're gonna need to put that in a tag template or 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.username. So we'll come down here and that's so easy I'm just gonna say fill.username. Tada! There it is. We have the exact same thing that we had before and as it turns out we can remove this whole It's gone from our codebase that really big hairy scary thing that you were worried about is gone and maybe you have it just all in line like this or maybe that was a big component and you have like 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 they're two different categories of that same abstraction. 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. you've experienced you have some battle scars on bad abstractions you're more thoughtful about the abstraction that you're making.
So with all this I just want to wrap up with a couple takeaways first off dry do not repeat yourself or dry that's not necessarily a bad thing it 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. Like duplication is not inherently bad but it can be a problem and it can just like propagate a bunch of bugs all over the place. So do not repeat yourself by 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 Sandy Mintz talks about is duplication is far cheaper than the wrong abstraction. So prefer duplication over the wrong abstraction.
9. Building the Right Abstractions
As you duplicate code and wait for commonalities to emerge, the right abstractions become more obvious. Keep duplicating code and then identify the commonalities. If there are shared code branches, refactor instead of adding more conditionals. Learn about the users and split the abstraction into manageable pieces.
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. 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 like, 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 like 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 refactor 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 by themselves.
Resources and Q&A on Aha Programming
So I've got a couple of resources for you. Sandy Metz gave a talk called All the all the little things. I have a blog post about Aha Programming and a testing variant. Follow me on twitter. Thank you. Kent's talk on Aha Programming is always wonderful. Let's do some Q&A. Kent shares the origin of Aha and the frustrations with dry programming and creating abstractions.
So I've got a couple of resources for you. Sandy Metz gave a talk that is really good kind of around this same idea called All the 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 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 yeah subscribe. Thank you.
All right. That was excellent. Kent, as always, that was 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 got some great questions in the Slack. Kent, welcome back. Thank you. Thank you. I'm so excited to be here. I always enjoy spending time with you, Jason and our 4,000 friends or however many people are watching right now. Yeah, I think we're up above 4,000 right now on the live stream. So holy crap. I had a question because I've actually 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 of how you got to now? Yeah, so Aha was kind of just like my frustrations with dry programming as like a practitioner early on. 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.
Introduction to AHA Programming Continued
And then I heard about wet programming, which is write everything twice. And I was frustrated by that as well because then you have to fix bugs in multiple places. And so I just thought it was like 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. That's great. You briefly mentioned this, but I want to talk, well, actually, you know what you answered. I'm not going to tell you, 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 like project wise? Like if you were gonna roll this out into a given project, how do you work that into your process? Now that's such a great question. I think that it's so tempting when we're like, if you're starting a brand new project or you're coming into a project and you're like, 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 code so far, and you are architecting it for a three million line code base.
And then I heard about wet programming, which is write everything twice. And I was frustrated by that as well because then you have to fix bugs in multiple places. And so I just thought it was like 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 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 cringey. But I was like, it's okay. And I couldn't really think of what that could stand for. So I didn't even make a what that acronym is. And I tweeted about it and I got so many people like that is so gross. They're like just laughing hysterically about it. So I was like, okay, okay, I need to come up with a different name. And Cher Scarlet gave me a perfect name. And AHA, Avoid Hasty Abstractions, it was perfect. And it's like lightbulb moment. Like I love it for every, you know, it's just the perfect acronym. So that's where that came from.
That's great. You briefly mentioned this, but I want to talk, well, actually, you know what you answered. I'm not going to tell you, 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 like project wise? Like if you were gonna roll this out into a given project, how do you work that into your process? Now that's such a great question. I think that it's so tempting when we're like, if you're starting a brand new project or you're coming into a project and you're like, 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 code so far, and you are architecting it for a three million line code base.
Iterative Approach and Avoiding Bad Abstractions
Take an iterative approach and don't worry about duplication. If you encounter bad abstractions in a code base, inline them and pretend they've always been that way. Be thoughtful and avoid trying to architect the entire application for what it's not today.
You're going to wind up in bad abstractions and you'll never make it to that three million line code base. At least not one that you want to work in. So I recommend just taking a really iterative approach. Don't worry about duplication and you know, if you come into a code base and it's got bad abstractions, then what I talk about in the talk and referencing Sandy Metz there, inline those abstractions and then the, like you can just pretend that it's always been this way and you're like, 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, that for something that it's not today.
Considering Abstraction and Code Duplication
In the context of abstraction, it's important to consider whether to extend it or create multiple functions. Copying code initially helps determine if the abstraction is necessary or if the code is not as similar as thought. An example is having similar login and register buttons with different attributes. Creating a component with many props may not be simpler than the duplication. The implementation may differ even if the code visually appears the same. Adding more props for slight differences creates more work. The cost of bad abstraction can be worse than duplicating code and bugs. However, it's a nuanced topic and shouldn't be interpreted as a dislike for abstraction.
So a question here that is, I think, in the context of an abstraction, like do you feel like 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? So maybe I'm not totally understanding the question but my thought process normally, 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, don't do that yet. Just copy it, even if it's like five lines of code, like it seems 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 like taking the time to make the abstraction and create the variable names and the 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 yeah, I don't know if that answers the question because maybe I misunderstood it but that's just, yeah.
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 like an example? Because I feel like that's one of those things that it's easy to say in the abstract and hard to kind of put into concrete terms. So if somebody is thinking about this, like when's the case when like the same code, code that you've copy-pasted isn't as similar as we think it is.
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 and different ARIA labels for the modal, they pop up different colors and stuff. And I thought, you know what? 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 like, this is what the ARIA 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. It's like 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 like variables or anything, not the CSS, but the JSX for it. So I just created a JSX element and then I in-lined that element as a variable. And so like I'm able to take that little piece of commonality without making it a whole function component that has like 12 props on it. And you know, that's a really interesting distinction too because in both cases, you're talking about code that would visually look the same. Like 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.
Yeah, exactly. And then anytime you wanna make anything that like 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 like two copies of almost the same thing or a function component that you're calling that is almost the same amount of lines of props. So like, what are you buying yourself there? Not much, just chores. Totally, totally. Okay, so another question just kind of following that. Is the cost of bad abstraction usually much worse than that of duplicating the code in tests and possibly duplicating bugs? What's your experience with that? That's a great question, and it's pretty nuanced because I don't want anybody coming away from my talk saying, oh, Kent likes duplication, he hates abstraction.
The Value of Abstraction and Duplication
As a library author, I see the value of abstraction, but there comes a point where duplication becomes a problem. If you find yourself duplicating over many files, there may be a good case for an abstraction. There is no strict rule for this, as it is nuanced.
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 that, there comes a point where the duplication becomes a real problem. For this login register example that we just shared, like 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 kind of nuanced, and yeah.
Comments