Software Design

Bursting at the Seams

Bursting at the Seams

by David McDonald

In the talk titled "Bursting at the Seams" presented by David McDonald at RubyConf 2019, the concept of seams in software design is explored as a vital tool for improving code maintainability and testability. McDonald begins with a humorous reference to a memorable scene from the movie "Tommy Boy," setting the stage for discussing how seams can metaphorically represent crucial points in code that allow for easier modifications and testing. He emphasizes that many existing codebases are often rigid and difficult to change, which can hinder software development. The presentation outlines the following key points:

  • Definition of Seams: Seams are defined as points in the codebase that allow developers to change how the code behaves without altering the actual code, offering flexibility in testing and refactoring.
  • Importance of Testability: Drawing from Michael Feathers' work on legacy code, McDonald stresses that seams facilitate easier testing and promote healthier code evolution by breaking down dependencies.
  • Examples of Seams in Code: McDonald presents a practical example with a "make requests" method, identifying it as a complex method that requires changes when new features are introduced. By refactoring to utilize seams, the code becomes easier to work with.
  • Refactoring Strategies: The importance of identifying where seams can be created to isolate dependencies is highlighted. He discusses using dependency injection as a means to achieve this.
  • Incremental Improvements: As more requirements emerge (such as adding new user types), instead of modifying the original code, McDonald demonstrates how to leverage the seams created to smoothly integrate enhancements.
  • Conclusion: Ultimately, McDonald argues that recognizing and creating seams in code allows developers to manage change with less friction and lays the groundwork for future refactoring. He encourages developers to think of seams in their own work to enhance their software designs and coding practices.

The main takeaway from the presentation is the power of seams as a conceptual tool in software engineering that promotes adaptability, testability, and cleaner, more maintainable code, which is essential for developers facing continual shifts in project requirements.

00:00:12.920 About that time, welcome everyone! It is good to see a couple of friends out here. Thanks for attending this talk.
00:00:20.510 This talk is called "Bursting at the Seams." I'm sure you've all seen Tommy Boy. This is the one scene I think about whenever I hear that expression: "the fat guy in a little coat." As a kid, I would attempt to reproduce that scene, but it’s actually really hard to do!
00:00:27.289 So they must have pulled a couple of threads out or something. Anyway, this talk is about seams: what they are, why we care about them, and how to work with them.
00:00:35.159 That’s really the purpose of the talk. Also, that's a Friesian horse by the way. I recently had a shaman tell me I needed more magic in my life.
00:00:40.469 They said that I needed to have my spirit guide be a part of my daily life, and they gave me some spirit animals. I don’t know, we'll see what happens.
00:00:51.440 We’re going to look at some code, talk about seams, what they are, why we care about them, and then we’ll take a practical application of what we've learned and apply it to the code we just looked at.
00:00:56.549 My name is David McDonald. That’s another spirit animal jumping over me, like the cow jumped over the moon. But that’s Norka. I like orcas because they’re matriarchal.
00:01:03.030 I’m a software engineer at Weedmaps, and I’d like to thank Weedmaps for sponsoring me and allowing me to come out here and speak to you today.
00:01:09.870 With that, let’s dive into things. A quick note on the format: I generally like live coding talks.
00:01:15.990 I know that they can be treacherous and very difficult. Every time I’ve tried to do one, I bumble it.
00:01:23.580 So, I’m not going to be doing live coding today. Instead, this will be a talk where I have some screen grabs.
00:01:29.080 I’ll talk to you about the screen grabs, and I will compare them to previous screen grabs that we just looked at.
00:01:36.330 Sorry in advance if this is at a pace that is too slow for you or if it’s too quick; I’m trying to find a middle ground so that everyone feels like they can follow along.
00:01:46.269 But remember, patience is a virtue and a requirement for senior developers. With that, let’s jump in.
00:01:55.380 Here we have a long-looking method called "make requests." It takes four keyword arguments, two of which are required. It looks like a lot, but it’s really just instantiating a couple of objects and then executing those, building up some arguments for them.
00:02:06.400 When we enter the method, we create an instance of a signer, and we will learn what that is shortly. The signer instance takes a method and an endpoint argument that we just passed into "make requests."
00:02:12.359 As you can see, we call that instance, and we get back another object: something called a signed header. We end up using that to build up the arguments for yet another object that we’ll retrieve from the build requests.
00:02:24.010 Next, we create an argument hash composed of the same method and endpoint args, and we also give them a name and a body if one was passed in.
00:02:36.120 This is code you might have written yourself or seen before. It can be a bit hard to look at. Ultimately, we just issue a request. As you can see, we’re rescuing a REST client exception.
00:02:46.269 This is a REST client object here that we’re building at the end. So we finally issued that request, parsed out the response, and did something with that response.
00:02:55.380 In this case, we are storing data locally in the database. This gives a bit more context to what we’re working with.
00:03:03.030 You can see that the large method we just went over is pretty much the bulk of this class. We’re going to call it the request class.
00:03:10.900 There’s another spirit animal, a legato, that was given to me very recently. I chose this example because it’s not very complex; it's fairly readable.
00:03:16.680 However, it's really easy to nitpick stylistically. Any one of us in the room could probably pick out little things: 'I don’t like that you named it this way.'
00:03:23.580 Or, 'I don’t like that all the arguments are structured like this.' Some might say, 'I don’t like that you have instance variables that you’re just directly calling aside from this method.'
00:03:30.920 However, we could do that, or we could talk about what’s really wrong with this method, and that’s kind of the heart of this talk. Beyond talking about seams today, that’s what I really want everyone in the room to take away.
00:03:44.300 So, let’s just think to ourselves here, or shout things out, or whatever: what is wrong with this method? Some of you might be saying a lot; others might say, 'I don’t know, not really a whole lot.' Who even wrote this terrible thing?
00:03:55.640 Ah yes, I did write this method. I’ve changed some things up here to obscure the source, but I wrote this a few years ago.
00:04:02.270 I was looking back over some source code, and that was the inspiration for this discussion. You could have listed a few things like maybe some SOLID principles.
00:04:10.900 Or what Sandy would call them: true principles. But the number one problem I see with this code is that it’s just hard to change.
00:04:20.920 There are a lot of places in the code base that seem to be tied to this one method. It feels pretty crucial to the health of the application.
00:04:27.570 This speaks to what is probably the real point here: software is meant to be changed. Otherwise, we’d probably be implementing this in hardware.
00:04:36.120 If you’re a software engineer, it’s literally your job to change software. So the need to accommodate changes in our code is one of the main reasons we write object-oriented code.
00:04:44.070 Code that is hard to test is often harder to change; it obviously depends on what the changes are.
00:04:51.130 But if your code is really rigid or brittle, that can just cause a lot of messes and a lot of time wasted.
00:04:58.650 But I don’t want to digress. What I wanted to focus on is how this method, which seems really crucial to our application, will respond to a change in requirements.
00:05:09.380 So, let’s imagine a new requirement comes in. It’s going to feel a little contrived here, but our users have recently been categorized into specific types of users.
00:05:16.700 Each user has been given one of several levels, let’s say gold, silver, or bronze. Depending on which group or tier they are in, we have to sign each of these requests with a different algorithm.
00:05:23.890 This is a real scenario that exists out there in the world, but for our cases, we can pretend there’s a time-locked service that we can only access during certain times of the day.
00:05:31.480 You have to have certain user permissions to do so, and you have to sign those requests each time; otherwise they won’t be accepted.
00:05:39.990 So we’re really worried about security here. For now, we’re only going to have two types of users: gold and silver. But we know more will be forthcoming.
00:05:48.200 So this is obviously going to require some changes to our code. We need to take a look back at our method.
00:05:55.330 We know that we need to add a new signer, or maybe that signer needs to apply a different algorithm depending on the user.
00:06:05.690 So, we need some way to toggle which signing algorithm we are going to use, and we also need to apply some logic to do that.
00:06:12.660 But what does our signer even look like? Do we need to know? Do we need to worry about that? Let’s take a quick look at it.
00:06:23.030 Not really, it’s not doing much special. There’s some magic where we’re applying an algorithm, a hashing algorithm.
00:06:31.180 Do we really care? In fact, another developer on our team implemented that, and they are going to implement the future ones and the ones after that.
00:06:39.510 So, we just know it’s there and what way it’s hand to say. Another contributor in the balloon implemented this entity called a channel, which delegates these calls off to a user.
00:06:47.690 It asks, "Are you a gold user?" or "Are you a silver user?" So, we have all of this stuff ready. It’s now up to us to implement these new signer classes.
00:06:56.570 Let’s also note that we have these two instance methods here on the channel for a consumer ID and a private key.
00:07:04.220 I might have noticed that we use those here in our make request method. That’s where those are coming from.
00:07:12.890 Let’s quickly implement a naive approach to the problem. We need to create a new signer class or extend our current one.
00:07:20.630 So for now, let’s keep it really simple: we’re just going to wrap our algorithm and copy-paste for two different signers—one for gold and one for silver.
00:07:29.960 Okay, so we basically cloned that whole class. We wrote some unit tests, those unit tests are passing, and now we’re back to our make request method.
00:07:37.920 We can conditionally call one or the other. We already have a reference to the channel, so in our request class, we can go through and ask the channel.
00:07:44.630 Like, hey, is your user a gold user? Is your user a silver user? We can also just look up the consumer keys since those are instance methods on the channel.
00:07:55.270 This avoids the need to pass around raw instance variables, which was bothering me, so we can do that.
00:08:01.620 We can also pass the channel down into the request method and look up those consumer keys directly.
00:08:08.690 Our signer classes both have the same signatures: they take the same arguments. So, we just build that up, check the channel level, and we’ve implemented our new requirement.
00:08:15.930 Here we simply ask the channel if you are gold; if you are, call the gold signer; otherwise, call the other one. Pretty simple.
00:08:22.860 Is that really all we need to do? Have we covered our bases here? We run our tests, and those are passing.
00:08:30.450 We’ve been diligent and thorough with our QA. We’re ready to go, so we just ship this thing, right?
00:08:37.210 What more could we do? You might be thinking, "Okay, didn’t you say there’s going to be more signers in the future?" Yes, I did say that.
00:08:44.020 But those aren’t here yet. We’ve only got two, so should we worry about that now?
00:08:49.990 As soon as we need to add another version of our signing algorithm, then we’ll be forced to make a change here.
00:08:58.320 But is that really that big of a deal? We could turn this ternary operator here into a longer if-else statement or a case statement.
00:09:04.900 That means we will have to come in here and change this every time. We don’t know how many different signers are going to come through.
00:09:12.480 Wouldn’t it be nice if we could just affect change in this method without having to change the code ever?
00:09:21.310 So, let’s pause there on that question. I’ll freely admit that I’ve never had an original thought in my life.
00:09:27.810 I’m very dubious of anyone that claims that they have. I owe a lot to those who have come before me.
00:09:35.770 One of those people is Michael Feathers, specifically in his great book "Working Effectively with Legacy Code."
00:09:41.680 The whole premise for this talk comes from this book, chapter four specifically. Please read it if you haven’t; if you have, read it again.
00:09:50.220 Every one of us in this room has probably worked within a legacy codebase. He defines that as a codebase that’s not under test.
00:09:58.200 That means everything under test; we’re probably all going to write code that is not under test at some point in the future.
00:10:05.700 I know that I work on a couple of apps at Weedmaps that do have 100% code coverage and a couple of others that don’t.
00:10:13.260 But it’s very relevant for today, so I highly recommend it. It’s from this book that we were first introduced to this concept called seams.
00:10:19.530 So, what are seams? We go back to that question we just discussed: wouldn’t it be nice if we could affect change in our request method from the outside?
00:10:29.040 Without having to change the code itself? This is really where seams come into play.
00:10:35.720 A seam is a place that allows you to modify the behavior of whatever you’re dealing with without modifying the code.
00:10:44.280 That sounds pretty abstract. Without looking at something, especially if you've never been introduced to this, you might think about mocks or something like that.
00:10:52.530 If you’re a Rubyist, you might be thinking is this like dependency injection or what is this?
00:10:59.360 Let’s get to a concrete example here with our code. The bigger question now is why do we care about this?
00:11:06.750 Michael Feathers, in the book "Working Effectively with Legacy Code," focuses on the idea of testability and getting something under test.
00:11:15.130 Having done that for years, he discusses the emergence of seams. There are many benefits to these seams that we can take advantage of on a daily basis.
00:11:23.120 Once you start seeing your code, you begin noticing these seams, specifically what he calls object seams.
00:11:30.310 Refactorings and decoupling kind of appear to you, and it comes naturally.
00:11:36.760 Thinking about seams is all about thinking about dependencies. It’s about finding and creating an incision point where we can loosen our dependencies.
00:11:43.830 This makes code easier to write tests for, which is a huge benefit. It also makes it easier to change later.
00:11:50.310 As I said, our software is always changing. It’s our job to change software, so let’s make it easier for ourselves and for those after us.
00:11:57.020 For our future selves, those who are going to touch our code later, read our code later, and write tests for our code later.
00:12:05.350 To write effective tests, you need to break these dependencies. Thinking in terms of these seams will help you first of all identify dependencies.
00:12:13.410 If working with dependencies is a new concept to you, this is a great place to start.
00:12:22.500 The analogy I like is that of seams in clothing. Here we have the seam on a baseball. If anyone’s ever pitched or anything like that, you know how important seam placement is with your fingers.
00:12:29.760 For me, the analogy of clothing works well: a seam is a place where two parts are stitched together.
00:12:37.020 The pieces on each side touch each other right at the seam. In software, if you identify a seam, you’ve identified a place where there’s a well-defined interface.
00:12:45.300 This allows you to leverage something like dependency injection, meaning you can take one of these dependencies and stick it in there.
00:12:52.060 You don’t have to declare it directly in your code. I like that seams help us talk about a lot of the SOLID principles, many object-oriented principles.
00:12:58.270 This gives us a nice way to discuss these concepts without having to talk about them directly. Maybe you don’t have a common ground with someone to discuss a topic like open/closed.
00:13:05.140 This is an easy way to get the ball rolling. Code complexity, extensibility, and testability have been hot topics in the industry for years.
00:13:12.300 Seams can help a novice get up to speed with object-oriented programming and help them think in helpful and beneficial terms for the whole team.
00:13:19.890 Hopefully, these insights can make your codebases more flexible and resilient to change.
00:13:26.230 Let’s take a quick look back at our implementation and ask this question: can I create a seam?
00:13:34.560 This is something that modifies the behavior without changing the code. Can that help us here? We don’t have a seam here right now.
00:13:42.680 Thinking emojis come into play: could we have a seam here? It looks like we could have done something right there.
00:13:51.920 That seems to be the hot spot in this method. But you know what? We’ve made our change; we deployed it. So that’s game over, talk over, thanks for coming!
00:13:58.650 Now the time is now. This is the talk. A new requirement comes in, and now we have a bronze level.
00:14:05.500 This is a perfect time. We know what to do; we know that seams one spot where we put that ternary operator or if statement.
00:14:12.270 We need to go in there, create a seam, and that will facilitate the addition of the next signer that’s surely coming down the line.
00:14:19.350 So let’s try to make use of that. We kind of have to get our bearings here so let’s take a glimpse at the overall class.
00:14:27.040 We have a new signer we want to slip in, but we just aren’t set up for that yet. To do this right, we need to know the bigger picture of the overall application.
00:14:35.150 How these signers are used, how this request class is used, right? What are we dealing with? We have these signers that take the same argument list.
00:14:42.950 Looks like they do. We basically want to figure out how to pull those things out and let something else decide which one we need to give to our method.
00:14:51.080 At that point, our method isn’t the one changing the behavior; it’s whatever something else decided.
00:14:58.040 We can create a signer method that wraps up this logic and gives us back the thing that we want. So we go ahead and do that.
00:15:07.040 Near the bottom, we have our signer which is going to forward along our method and endpoint arguments that we received from before.
00:15:15.460 If you look at the signer implementation above, it’s really just copy-paste from before. We’ve added signer C, our bronze signer.
00:15:23.020 Bing-bang-boom, we’re done. Our tests are passing, but what does this really give us? What have we gained here?
00:15:30.730 Is our code more extensible? Is it easier to test? Is it easier to read? Is it open-closed?
00:15:36.650 What happens next time we have to make a change and we have to add a fourth signer? Well, we just basically pushed the change off from our make request method into our new signer method.
00:15:43.220 I’d argue that it’s not really that great, but things start to emerge once you’ve made a change like this.
00:15:50.740 Okay, so this doesn’t fix everything, but this is the start of something. What’s the big deal with seams?
00:15:59.740 If we just created a seam, we don’t have to change the make request method to change its behavior now.
00:16:07.410 Believe it or not, this class is a lot more adaptable and easier to refactor. Once we create a seam like this, other improvements to the design tend to reveal themselves.
00:16:14.200 Other places that know about this code show up as points that can now be refactored, making them a little bit more resilient.
00:16:21.540 Seams, in addition to Michael Feathers’ insights about making our codebases easier to test, are like refactoring gateways.
00:16:30.220 You’ve opened up a whole new world to gain more breathing room with your dependencies and move them around.
00:16:39.300 Does this class really want to be responsible for deciding which signer is to be applied to every request?
00:16:46.100 Or is that the responsibility of someone else? How far should we take this?
00:16:52.640 The seam we created has provided us with the flexibility to explore this, and paired with our tests, we can make changes in this class without fear.
00:17:01.470 So let’s walk down the road I took to illustrate the point a bit and some of the benefits that you can gain.
00:17:09.140 This is ultimately up to you; it’s about how far you need to change it.
00:17:14.970 Do you expect more signers that will come down the line? Is it really worth your effort?
00:17:23.890 I argue that it almost always is, and hopefully, I can illustrate that point.
00:17:30.080 I pushed this one dependency out of my make request class and up a level, putting it in the request itself.
00:17:38.890 Ideally, I’d like to pass a signer instance into my request class. I don’t want to worry about toggling things in the request class.
00:17:47.130 I want this request class to build a request, make a request, and parse a request—that’s all I want it to do.
00:17:54.110 Other logic should be handled by someone else, so let’s outline what we want.
00:18:01.610 To make this work, we need to zoom out a little bit and rethink things. Let’s take a look at the request class and how it’s currently used.
00:18:08.950 At the bottom, on the left, we have a refresh stats worker that I made up. It takes an ID of the channel.
00:18:17.570 It looks at that channel, creates an instance of the request class, and passes in the channel so we can get the consumer ID and private key.
00:18:24.850 We can then call methods like refresh stats on that request instance.
00:18:32.340 The refresh stats method will forward along dynamic pieces of information we need, like what kind of HTTP method it is and what the actual endpoint is.
00:18:39.540 Then we return to our familiar make request method. That’s what we’re dealing with here.
00:18:46.360 We can actually pull this off. The channel already knows about the levels: gold, silver, and bronze. It already knows about our credentials.
00:18:54.690 We were only really using those to pass them along to the signer. Therefore, if we want to avoid toggling which signer is used in the request class, we can let the channel pick the signer.
00:19:05.640 If we examine the changes in a signer, we need to assess which ones are dynamic and which are static.
00:19:12.979 We saw that refresh stats passes in a couple of dynamic pieces, but the consumer ID and the private key are static.
00:19:20.290 So let’s quickly rework our signer. We want them to accept a consumer ID and private key.
00:19:27.440 We’ll retrieve an instance of a signer and whenever we need to call a sign request, we can just pass in the dynamic elements.
00:19:34.330 There’s another Friesian horse for you all. You wouldn’t even need to keep injecting your channel into the request.
00:19:41.250 This was only done so we could forward those details along to the signer. Now we’ll handle everything inside of the channel.
00:19:48.500 I realize this may be moving along a little quickly; you may or may not be losing track of where we are, but that’s okay.
00:19:53.040 I really just wanted to push this out there and show you how you can start thinking about seams.
00:19:59.440 Because I started to create one seam, there wasn’t one to begin with; we created a seam and these dependencies kind of naturally drifted apart.
00:20:06.760 Then we thought, 'Okay, I can move this out, make this class a little cleaner, and have this class take on certain responsibilities.'
00:20:13.270 Certain design flaws began revealing themselves without me even having to think about it.
00:20:20.560 You have to stop where you feel best; there’s a trade-off that we always have to make when software is changing.
00:20:26.909 I took it even further and thought, whose responsibility is it to determine which signing algorithm to use?
00:20:34.920 We accomplished our job already, but I thought, a signer is just applying certain algorithms.
00:20:43.530 This is a perfect instance for polymorphism. We can have a signer responsible for the credentials and apply a different algorithm based on the user's channel.
00:20:51.250 You can see how I ended up here. You could keep going, and that’s really the point: I have now been granted the space to do this refactoring.
00:20:58.640 Make things more readable and maintain less code overall, which is not always a win, but to me, it generally is.
00:21:05.360 In Ruby, you can do a lot of fun stuff with string interpolation and constantizing things that can get you into trouble.
00:21:11.590 So, I try to avoid that, though sometimes it’s a useful tool to reach for. Ultimately, I just separated all the algorithms.
00:21:20.410 This is just a point you can go as far with as you want.
00:21:27.620 I would like to end on this note: Michael Feathers focuses on how to get codebases that are not under test, under test.
00:21:34.850 From that work, the concept of seams emerges. It’s the ability to change the behavior of a method or object without changing the code.
00:21:44.390 This idea underscores how powerful this tool of seams can be. He focuses primarily on testing, and as you’ve noticed, I mentioned tests.
00:21:51.150 But I haven’t shown you any tests; we haven’t written any tests because that isn't the point here.
00:21:58.060 The goal here is that I’ve found that as I begin to see my codebase in terms of seams, refactoring naturally reveals itself.
00:22:06.850 Decoupling happens almost instinctively. When I say, "It would be nice if I could change the behavior of this class or this method," I’m automatically thinking of seams.
00:22:13.370 Hopefully, that’s a benefit you’ll start to notice for yourselves too. It’s a topic that’s not often discussed, but it’s a very useful one.
00:22:20.620 My goal today was to reintroduce that concept and help you identify places in your code.
00:22:29.070 To make that crucial incision, that first spot where your codebase can burst at the seams, facilitating future refactorings.
00:22:36.350 Inevitably, those requirements will come in. With that, that’s my Twitter handle there if you want to follow me.
00:22:42.930 Thank you! I can take a couple of questions.
00:22:55.900 I mentioned SOLID principles and also true principles. I attributed those true principles to Sandy Metz, who just spoke earlier.
00:23:03.480 I believe she mentioned them in her "Practical Object-Oriented Design with Ruby" book. I got a thumbs up in the back there; thank you!
00:23:11.820 But she’s written a lot of books, all of which I have read and I recommend.
00:23:17.660 It’s basically a pared-down version, specific to Ruby, of the SOLID principles.
00:23:24.410 So, there are four instead of five. Yeah, no... but I could goggle it.
00:23:31.540 I could google it.