The Bike Shed
The Bike Shed
thoughtbot
Support podcast
On The Bike Shed, hosts Joël Quenneville and Stephanie Minn discuss development experiences and challenges at thoughtbot with Ruby, Rails, JavaScript, and whatever else is drawing their attention, admiration, or ire this week.
423: Cognitive Strategies for Coders
Stephanie is back with a book recommendation: "Thinking in Systems" by Donella Meadows. This book has helped to bolster her understanding of complex systems in environmental, organizational, and software contexts, particularly through user interactions and system changes. Joël describes his transformative experience watching last week's total solar eclipse. Together, they explore how systems thinking influences software development and team dynamics by delving into practical applications in writing and reading code, suggesting that understanding complex systems can aid developers in navigating and optimizing codebases and team interactions. Transcript:  JOËL: Hello and welcome to another episode of The Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Joël Quenneville. STEPHANIE: And I'm Stephanie Minn, and together, we're here to share a bit of what we've learned along the way. JOËL: So, Stephanie, what's new in your world? STEPHANIE: I have a book recommendation today [laughs]. JOËL: Oh, I love book recommendations. STEPHANIE: It's been a little while, so I wanted to share what I've been reading that I think might be interesting to this audience. I'm reading Thinking in Systems by Donella Meadows. Joël, are you familiar with systems thinking theory at all? JOËL: Very superficially. Hearing people talk about it on, I guess, X, now Twitter. STEPHANIE: Yeah. Well, what I like about this book is the subtitle is A Primer on Thinking in Systems [chuckles], which is perfect for me as someone who also just kind of understood it very loosely, as just like, oh, like, I dunno, you look at things holistically and look at the stuff, not just its parts but from a higher perspective. JOËL: Yeah. Is that accurate sort of your pre-book reading overview? Or do you think there's a bigger thing, a bigger idea there that the book unpacks? STEPHANIE: Yeah. I think I'm only, like, a third of the way through so far. But what I have enjoyed about it is that, you know, in some ways, like, intuitively, that makes a lot of sense about, like, oh yeah, you want to make sure that you see the forest for the trees, right? But one thing I've been surprised by is how it's also teaching me more technical language to talk about complex systems. And, in this case, she is talking about, essentially, living systems or systems that change over time where things are happening. I think that can be a little bit confusing when we also are, you know, talking about computer systems, but, in this case, you know, systems like environments, or communities, or even, you know, companies or organizations, which is actually where I'm finding a lot of the content really valuable. But some of the language that I've learned that I am now trying to integrate a little bit more into how I view a lot of just, like, daily problems or experiences involve things like feedback loops that might be reinforcing or balancing and different, like, inputs and output flows and what is driving those things. So, I've appreciated just having more precise language for things that I think I kind of intuited but didn't exactly know how to, like, wrap up in a way to communicate to someone. JOËL: Do you think the idea of thinking in terms of things like self-balancing versus sort of diverging input loops is something that's useful when actually writing code? Or do you think of it a little bit more in terms of, like, teams and how they organize general problem-solving approaches, things like that? STEPHANIE: I think the answer is both. I actually gave this quite a bit of thought because I was trying to wrap my head around her definition of a system and how we talk about systems sometimes, like, a codebase, for example. And the conclusion I came to is that, really, it's not just the code static by itself that we care about. It's how it gets exercised, how users use it, how developers change it, how we interact with it when we, like, run tests, for example. So, that was really helpful in kind of thinking about some of the problems we see in engineering organizations as a result of software being a thing that is used and written by humans, as opposed to it just existing in memories [chuckles] or, like, it's in a storage system somewhere. Like, that means it's kind of lifeless, and it's not changing anymore. But the point of kind of this framework is trying to understand it as it changes. JOËL: So, kind of that blurry line between humans and computers and where those two overlap is where a lot of that systems thinking almost, like, mental model or vocabulary has been most helpful for you. STEPHANIE: Yeah, I would say so. So, Joël, what's new in your world? JOËL: So, I did the thing. I traveled to see the total solar eclipse this past weekend. It was mind-blowing. It was incredibly cool. I really loved it. For any of our listeners who have never seen a solar eclipse, in the coming years, have an opportunity to see one. I'd say it's worth traveling to see because it is really impressive. STEPHANIE: Cool. What did it look like when it happened, when it was 100% eclipsed? JOËL: So, what really impressed me was the fact that, like, most of the cool stuff happens in that, like, last half a percent. So, like, 95% eclipsed, still not that impressive. If that's all I'd seen, I would be disappointed. And then, in that last little bit, all of a sudden, everything goes dark. It's sort of, like, that twilight past sunset. You've got a glow on the horizon. The stars are out. STEPHANIE: Wow. JOËL: The animals are behaving like it's past sunset. They're getting ready to go to sleep. STEPHANIE: Whoa. JOËL: The sun itself is just a black dot with this, like, big fiery ring around it. Like all those pictures, icons, photos you see online, or drawings that look over the top, those things are real. That's what it looks like. STEPHANIE: Wow, that's really neat. Could you see it without looking through the eclipse viewers? JOËL: So, when you hit totality, you can look at it with a naked eye, and it is, yeah, magnificent. STEPHANIE: Oh, that's so cool. How long did it last? JOËL: So, it depends where you are in the path of totality. I was pretty much dead center. And it lasts, I think, three and a half minutes is what we had. STEPHANIE: That's so cool. So, for me, here in Chicago, we did not have complete totality. It was about, like, 95%. So, I was watching it, just from that perspective. And I would say, yeah, it was not nearly as cool as what you described. It kind of just was like, oh, it got dark. It almost looked like I was viewing the world through sunglasses. I did have one of those viewers that I used to, like, look at the sun and see how much of it had been covered. But yeah, it was cool. But what you said, I think now I feel like, wow, I really should have [laughter] traveled. I could have traveled just a few hours, you know, to, like, Indianapolis or something to have been on the path. That would have been really neat. And I don't think the next one will be until 2044 or something like that. JOËL: Yeah. And that's the thing, right? I think if you're within a few hours of the path of a total eclipse, it is absolutely worth traveling to totality. The downside of that is that everybody else has the same idea. And so, you will be fighting traffic and a lot of things, especially if it goes through some, like, populated areas, like it did this time. STEPHANIE: Yeah. Well, that's really neat that you got to see that. That's, I don't know, it sounds like not exactly once in a lifetime, but definitely very rare. JOËL: For sure. I think with this experience now; I would definitely consider traveling again if there's one, like, anywhere near where I live, or, you know, maybe even, like, planning a vacation around going somewhere else to see one because it's short. You know, you're there for three minutes, and you see something cool. But that was really impressive. So, something that really struck me when you were talking earlier about systems thinking is that you mentioned that it gave you a sort of a new vocabulary to talk about things. It almost gave you a sort of different way of thinking or some other mental models that you could use to apply when you are interacting in that sort of fussy boundary between people and code. And I think that this idea of having language and having mental models is something that is incredibly valuable for us as programmers in a few different areas. And I'd be curious to see particularly for when we're reading other code, reading code that someone else has written or, you know, yourself from six months ago, do you have any sort of mental models that you like to reach for or techniques that you like to use to sort of give yourself that almost vocabulary to understand what somebody else is trying to do with their code? STEPHANIE: Yeah, I would say so. You know, as you were talking about, like, how do you read code? I was thinking about how I read code is different from how I would read a book [laughs]. I almost rarely just read everything line by line and, like, file by file, you know, in some order that has been presented to me. I am usually a lot more involved. It's almost, like, more like a choose your own adventure kind of book [chuckles], where it's like, oh, go to this page to check if you want to check out what happened down this code path [chuckles]. JOËL: Right, right. Oh, if you're reading a novel, are you the kind of person that will read the ending first? STEPHANIE: Absolutely not. [laughter] JOËL: You have strong opinions here. STEPHANIE: Even when I, like, really want to... okay, sometimes I will, like, maybe just kind of flip to the back and just see, like, oh, how many more pages or chapters do I have [laughs] left? If I am itching to know what might happen. But I definitely don't start a book by reading the end. I think there are people who do that, and maybe that works for them, but I don't understand it. [laughter] JOËL: But maybe that's the thing that you do with your code. STEPHANIE: Yeah. When I read code, it's almost always with some kind of intention to understand a particular behavior, usually kind of kicked off by some action, like, done by the user or something automated. And I want to understand that process from start to finish. So, I'm less likely to read a whole class file [chuckles], as opposed to just following method and the messages that are sent along the way in a process. JOËL: That makes sense. Do you tend to sort of go from kind of the origin point and then follow it down, or sort of the opposite, find some, like, terminal node and then work your way back? STEPHANIE: Oh. JOËL: And I could imagine this in a more concrete sense in a Rails app. You find, like, the route that you're going to hit because you know it's a URL, and then you find the controller, and then you read through the action. And then, you maybe follow a service and something like that or look into the view. Or maybe the opposite: there's a particular page that gets rendered. You look at a method, a helper method that gets called in a view, and then you sort of, like, follow a backtrace from there. STEPHANIE: Yeah, I think both. It depends on what information I have available to me, I think. I can think of, recently, I was trying to figure out the process for which, like, a user in this application I'm working on can downgrade the tier of their account, and I didn't know what to grep for. And so, I asked, like, "Hey, like, what are the entry points for a user being able to do this?" And someone gave me a couple of routes, and that was great because then I got to see, oh, that this is possible in multiple ways. Like, the user can do it themselves, or the admin can do it, and that was really helpful. Other times, I think I have been able to find a keyword on a page and start from, like, a view or a component, or something like that, and then work upwards. JOËL: I love that question that you asked, "What are the entry points for this thing?" I feel like that's a fantastic question to sort of ask yourself when you're feeling stuck, but it's also a great question to ask other people that might know. Do you find that you read code differently when you're just trying to, like, maybe understand a broader subsystem? Maybe you're sort of new to this area and you have to add a feature, as opposed to maybe you're debugging something and trying to understand why things went wrong. Are those two different kinds of reading? STEPHANIE: Yeah, that's also a great point because I do think there's another time when I've just scanned the file structure of an app and looked at the model's directory and just kind of been like, okay, like, maybe some things are namespaced. And that helps me just know what the main concepts that I have to be dealing with or that I will be dealing with are. But I find that sometimes less fruitful because of kind of what I mentioned earlier about thinking in systems, where I'm not sure how important those things will be yet because I don't know how they're used. They could not be used at all [laughs]. And then, I think I'm potentially, like, storing information that is not actually relevant in my brain. JOËL: That's tough, right? Because systems are so big, we can't hold them entirely in our brain. So, sometimes, selectively deciding what will not be loaded in there is just as important as what will. STEPHANIE: Yes. And I think that is actually advice that I would give to devs who are trying to get better at reading code. And this one's hard because when I am working with more early-career developers, it's hard to figure out, like, what are they seeing? How are they interpreting the code on the page? Because oftentimes, I see that they are getting stuck on the details, whereas I would like to encourage them to just be like, you don't really need to know what's going on in that method right now. Does the method name kind of communicate enough to you, like, what this thing is doing without having to understand all of the details? But my advice would be to start figuring out what to ignore [laughs] because, like you said, it's impossible to, like, hold all of that information at one time. What do you think about that advice and, like, how do you teach that to someone? JOËL: I think you're sort of hinting at two different ways of reducing the amount you have to load in your mind. The way I think about it, I think of it sort of spatially, so you can reduce the breadth of things you have to load into your head, so, realize, wait, there's all of these methods, and I don't need to know all of the methods in the file. There's only this one entry point I care about and everything downstream of that, and you just sort of prune everything off to the side, ignore it. That's not relevant right now. But there's also sort of a depth. How deep of implementation do you really need to have? Maybe you only need to know about the high-level concepts. And then, you sort of, like, do this pruning where you say, "I'm not going to go deeper than this level," because the implementation is not really relevant to what I'm trying to understand right now. I mostly need to know what are these classes and how do they interact with each other? Or something along those lines. And, ideally, you're may be doing a little bit of both. You probably don't need to go all the way to the deep implementation of every method, but you also don't necessarily need to know all of the high-level concepts and all of the objects in the system that interact. So, being able to prune in sort of both dimensions, breadth and depth, helps you to, I think, narrow the window of what you need to learn. STEPHANIE: Yeah, that's a really great point. I have a couple more strategies that I just thought about as you were talking about that. One is kind of on the journey to let go of some things that I can't understand in the moment. If they seem important, I will write them down and, like, put them somewhere in a list to come back to later and be like, "This is a thing I don't fully understand yet," and just be okay with that. I think, for me, there is some anxiety of like, oh, like, what if I'll need to know about it later? And at least putting it down somewhere as like, okay, like, I've done something with that anxious [laughs] energy of, like, recognizing that I don't understand this right now, and that's okay. But I can revisit it later. And then, another one is almost the opposite, where it's like, what are my landmarks as I'm navigating through a codebase? Like, what are the files that I'm consistently opening? Because so many of the roads lead to this object. Even when I'm kind of going through different paths, it's like, I can hook into, like, the behavior that I'm looking for from these landmark objects or models because they are really important in this domain. So, it's like, I don't necessarily need to remember every step of the way, but if I can recall some of the more important methods, then I can kind of find my way back. JOËL: Do you just try to, like, memorize those, or do you write them down? Like, how do you make a method or an object a landmark for you? STEPHANIE: That has felt a little more, like, it becomes more, like, muscle memory, I think, because I'm revisiting them pretty frequently. I don't know, it's somehow the act of repeating, like, going through those files just gets encoded somewhere in my brain [laughs], and I don't have to worry as much about forgetting them. JOËL: Strengthening that neural pathway. STEPHANIE: Yeah, exactly. JOËL: Or whatever is happening in the brain there. STEPHANIE: [laughs] JOËL: I like what you were saying earlier, though, about taking notes and sort of almost, like, a breadcrumbs approach. We did an episode almost two years ago where we talked about note-taking for various purposes and note-taking as an exploration exercise, and then note-taking when debugging, where we went deeper into that topic. And I think that would be really relevant to any of our listeners. We'll link that in the show notes. STEPHANIE: Yeah. Leaving breadcrumbs. That's a great metaphor or just a way to describe it. Because I have a little shorthand for if I am leaving myself notes in a codebase as I'm trying to understand what's happening, and it's just, like, putting my initials in a comment and, like, including some observation or commentary about what I'm seeing or a question. JOËL: Also, just a kind of meta observation here, but in the last, you know, 10-15 minutes we've been talking about this, we're already creating our own set of metaphors, and language, and mental models around understanding code. We're talking about breadcrumbs, and landmarks, and looking at code through a broad versus deep lens. That's exactly what we're talking about. STEPHANIE: Joël, do you have any mental models that you use that we haven't really gotten into yet? JOËL: I don't know if they're mental models per se, but I lean very heavily into diagramming as a form of understanding code. And maybe that's a way of sort of reducing the number of concepts because instead of now sort of thinking in terms of, like, lines of code, I'm thinking in terms of maybe some boxes and arrows, and that's a much higher-level way of looking at a system and can give me some really interesting insights. And there are a ton of different diagrams you can use for different things, and I guess all of them are based on a different maybe mental model of what a system is. So, for example, I might actually write out the method call graph starting from some endpoint and just sort of saying, "Hey, when I call this method, what are all of the methods downstream that get called? And is there anything interesting at any of those steps?" Variation on that if you're looking at, let's say, some kind of performance thing would be, like, a flame graph where you have sort of that but then it also shows you the amount of time spent in each of the methods. And that can give you a sense of where your bottlenecks are. Another one that I really like is thinking in terms of a finite state machine. So, sort of following data, how does it change in response to different events that can come into the system? And I'm not talking about, oh, you're using one of the, like, state machine gems out there for your Rails app. This is more of a way of thinking about programs and how they act. You can have just a plain, old Rails app, and you're thinking about, okay, well, how does a cart turn into an order, turn into a fulfillment request at the warehouse, turns into a tracking number for shipping? Modeling that as a state machine. And also, you know, can it move back along that path, or is it only linear move forward? Any kind of multi-state form a wizard often has paths where you move back. It's not linear. That very easily can be drawn out as a state machine. So, that is something that I really like to pull out when I'm trying to understand a, like, complex workflow. STEPHANIE: Yeah, I think we've talked about this before a little bit, or maybe not even a little bit, a lot [laughs]. But I know that you're a big fan of Mermaid.js for creating diagrams in markdown that can be embedded in a pull request description or even in a commit message. When I was hearing you talk about state machines and just all the different paths that can lead to different states, I was like, I bet that's something that you would create using a diagram and stick for yourself and others when sharing code. JOËL: Yes, Mermaid does support state machines as a graph type, which is really cool. Another thing that you can do is embed those in tools like Obsidian, which is my current note-taking tool. So, if I'm doing some sort of notes as a sort of exploratory tool, I will often start writing a Mermaid graph directly in line, and it will render and everything. That's really nice. If I'm not in Obsidian and I just need some sort of one-off graph, I'll often lean on Mermaid.live, which just gives you an editor where you can write up some Mermaid code. It will render it, and then you can copy the PNG into somewhere else and share that with other people. So, if I just need a one-off thing to share in Slack or something like that, I like to lean on that. Another type of diagram that I use pretty frequently is an entity-relationship diagram, so sort of what database tables are related to what others. On larger apps, there's just so many tables, and maybe a bunch of JOINS and things like that, and it's sometimes difficult to get the picture of what is happening, so I'll often draw out a graph of those. Now, it's not worth doing the entire database because that will be huge and overwhelming. So, I'll find, like, five or six tables that are relevant to me and then try to answer the question: How are they related to each other? STEPHANIE: Yeah, I like that. I was going to ask if you do it manually or if you use a tool because I've worked in various apps that have used the Rails ERD gem that will generate an entity-relationship diagram for you every time the schema changes. But there's something very compelling, to me, about the idea of trying to just figure out if you know the relationships, if you could draw them out, as opposed to having a tool do it for you. JOËL: Exactly. STEPHANIE: And I think, like, also, you do have information that might not be encoded in the system. Like, you actually know, oh, these two tables are related, even if no one has defined an association on them. I think that is important in understanding actually how the system is working in real life, I guess. JOËL: Agreed. So, we've been talking a lot about how we can use different tools, different mental models to take code that somebody else has written and kind of, like, almost read it from disk and load it into our brains. But what about the opposite? We're faced with a business problem, and we want to sort of write it to disk, turn it into code that somebody else will then read or that a machine will execute. I hear that happens occasionally. Are there sort of mental models or ways of approaching tackling a more, like, amorphous problem in the real world and turning that into code? Like, are they just the inverse of what we do when we read code, or are they, like, totally different set of skills? STEPHANIE: For me personally, I don't follow this framework very strictly, but I think more intuitively how I like to go about it is more behavior-driven where...because that is the language of maybe our cross-functional partners. They're saying like, "Hey, like, when this happens, I want to be able to do this," and I kind of start there. Maybe I'll pick up some of the keywords that they're repeating pretty frequently as like, oh, like, this is a concept. Actually, lately, the past couple of weeks, I've been test-driving almost all of my code as I work on a totally, like, greenfield feature. And that has been working really well for me, I think, because we did explore more granular, both, like, granular and abstract concepts when we were spiking this feature. And so, we had come up with some domain models. I had kind of thought about, like, how they might interact with each other. But when you then have to actually, like, code that, there are so many little nuances and things to keep track of that I found test driving things from, like, behavior and user stories. Those are really helpful in keeping me, like, on track to making sure that I didn't just have all these little pieces of domain concepts that then didn't really interact in a meaningful way. JOËL: Yeah, the sort of very, like, user or customer-centric approach to thinking about what is this app doing? Is a great way to think about it. And I guess the sort of translation of that, that first step of translation into code is some sort of, like, system spec. STEPHANIE: Yeah, exactly. JOËL: I like that because, you know, we have all these other abstractions that we use as developers. But at the end of the day, our customers and even, you know, our product people aren't thinking in terms of, like, objects and classes and all these other fun abstractions that we have. They're thinking in terms of behaviors and, you know, maybe subsystems, workflows, things like that. And then it's up to us to translate that into whatever paradigm of our language that we're using. STEPHANIE: Do you do things differently from me? JOËL: I don't think that I do it necessarily differently. I think it's one of several tools I have in my tool belt. Something that is similar but from a slightly different angle is inspiring myself with a lot of the ideas from domain-driven design. You know, we've been talking a lot about this idea of, like, mental models and having a vocabulary, things like that, about sort of the way that we work, but that exists at the product level as well. And what if we could encode a lot of that into our application itself? So, is there a distinction between a subscriber and a payer in our system? Is there specialized vocabulary around different other concepts in the app? Maybe instead of just having those be things that product people talk about, what if we made them actual named entities in the system and have maybe our object graph, at least in some way, reflect the sort of idealized model of what our business actually does? That often means that you're thinking of things at a higher level because you're thinking of things at the level that our product people are thinking about them. You might be thinking of things in terms of user journeys, or product workflows, or things like that, because you say, "Oh, well, a new payer has been added to this group account. And that has started a subscription, which then means that a user has access to these corporate features that they didn't have when they were in a solo account." Like, I've just thrown ten different sort of product terms out there that, you know, if there are concepts in our code can help us think about less of the implementation. What does the app do, or how does the app do it? And more in terms of, like, product terms, what does the app do? How do people experience the behavior, or maybe how does data change over the life cycle of the app? So, those perspectives, I think, have helped me distill down sort of more vague product ideas into things that I can then start turning into code. STEPHANIE: Absolutely. I think one way that this framework ends up falling short, at least for me a little bit sometimes, is making connections between behaviors that are similar but not exactly the same. Or when you think about them in more isolated ways, like, it's easy to miss that, like, they are the same idea and that there is, like, something a bit higher level that you can connect them, that you can create a more abstract class for, even though that's not actually how people talk about the things. One example I can think of is things like concerns that are both related to domain language but then also, like, kind of specific to how things work in the code as a system because you might not necessarily call something a subscribable from a product perspective. Do you have any thoughts about identifying those pieces? JOËL: So, what's interesting is I think there's a little bit of, like, layers above and below, the sort of domain layer where you're talking in terms of, like, what the product team would use. When you're doing a lot of the implementation, there will be things that are just, like, that's how we implemented them. They're in the nitty gritty, and they're not terms that the product team would necessarily use. Things like array and string they're low-level details. We have to use them. That's not really relevant to the world of payers, and subscribers, and things like that. So, they're sort of lower layer. And I think that's totally fine to have things where we sort of have things that are sort of programmer only, as long as they're sort of contained within this higher-level layer because that allows people new to the app to sort of see what are the different things in the application to think about things in a higher level. It also allows for smoother communication with the product team. So, ideally, you don't have a concept in the app that is the same as something that the product team, but you just both gave it different names, and then that's really annoying. Or maybe the dev team created something that's, like, almost exactly the same as what the product team talks about, but with some, like, slight variations. Now, you're just going to be talking past each other in every planning meeting, and that will be incredibly annoying. STEPHANIE: Yeah. At one point, when I was trying to communicate, like, async about how a feature works, and there was like the product word for it and then the dev word for it, I would have to type out both [chuckles] because I wanted to make sure that no one was confused about what we were talking about, which was the same thing that just had two names. And yeah, I don't know how many seconds of my life I'll never get back as a result [chuckles]. JOËL: Were these concepts that were identical and had just different names, or was this like, oh, well, our internal subscribed user is almost the same as when product talks about and, I don't know, employee, but our subscribed user has a couple of other extra behaviors that employees don't have, and now there's, like, this weird, like, overlap? STEPHANIE: Yeah, both situations I have found myself in, but I think this one they were virtually identical. Like, they could be used interchangeably to mean the same thing by people who understand both of those definitions, but the problem was that we still had two words [laughs]. JOËL: Yeah, yeah. I'm a big fan of, where possible, converging on the product team's definition. Although because code forces you to be more precise, sometimes that can then force some conversations with the product team about, like, "Hey, so we've been hand waving around this concept of a subscriber. Turns out we think there's actually two different kinds of concepts at work here: the person who's consuming the content and the person who's paying for it. And are they really the same thing, or should we sort of think about these as two different entities? And, in that case, what should the name be?" And that can force a really, I think, healthy conversation between development and product. STEPHANIE: Yeah, I like that. You mentioned there was, like, a higher level and a lower level, but I don't think we've gotten to the higher one yet. JOËL: Yeah. Sometimes, you want to build abstraction sort of over. You're talking about the idea of, like, subscribable things. I think that's where I'm a lot fuzzier. It's much more case-by-case. Where possible, I'd like to introduce some of those things as domain vocabulary so that we'd say, "Well, look, we have a, like, family of products, and they're all subscribable." And maybe, like, the adjective doesn't matter quite as much to our product people, but, you know, because we're using a module in Ruby, we want to lean into the adjective form, and that's fine. But I would at least want some loose connection there. STEPHANIE: Yeah, that makes sense because I think that ultimately makes for a better product. If we're thinking about, like, how to present a hierarchy of information to a user, like a navigation menu, we would want to group those things that are under that family together, ideally, so that they know how to interact with it. JOËL: Another thing that I think falls maybe under, like, this higher-level umbrella are things like design patterns. So, maybe because we want to be able to sort of, like, swap things in and out, we're using some form of strategy pattern. That feels like maybe it's a little bit higher level. It interacts with a lot of the domain concepts, but our product team doesn't really need to think in terms of, like, oh, strategies, and swappable things, and, like, flex points in your architecture. So, those would not necessarily be domain vocabulary. Although I could see, like, maybe there's a way where they do get a domain name, and that's great. STEPHANIE: Oh, I think maybe this is where I disagree with you a little bit. Well, actually, I agreed with what you said at the end [laughs] in terms of how maybe they should be part of the domain vocabulary because I think...I've seen product not fully understand the complexity of the application as it grows over time. And that can lead to sometimes, like, not as great product experience or experience for the user, like, interacting with this product. And maybe that is something we want to, as developers, if we're starting to see and feel and have maybe even introduced a pattern for...I can't claim to have done this too much, but it's definitely a skill I want to hone in on. But, like, how do I communicate to product folks so that we understand, oh, like, where is it possible for these different types of a subscriber to diverge? Because that is important, I think, in determining the future of a product and, like, where we want to invest in it and where we should focus, like, new features. JOËL: And oftentimes, when there is that kind of divergence, there probably will be some sort of product-level thinking that needs to happen there. Are we saying, "Hey, we have one of three types of subscribers, and we want to think about that"? Or maybe we want to say, "We have three different ways of processing an application." Maybe it's derived automatically. Maybe it's a dropdown that you have to pick. But let's say it's a dropdown. What do we name that dropdown with the, like, kind of processing that we want to do to an application? The thing that we want to name that dropdown that's probably a good name for that, like, group of strategies, assuming we implement with a strategy pattern. Maybe we're doing it differently. STEPHANIE: Yeah. The more you talk about that, the more I'm convinced that that's, like, the way I want to be working at least, because you have to know what's there in order to, like, name it. You know, you have to face it, essentially [laughs]. Whereas I think a lot of applications I've worked on fall into the trap of all of those things are obscured way down in the depths of the user flow, where it's like, oh, suddenly, for some reason, you can, like, have a dropdown here that totally changes the behavior, even though you've gotten this far in either the stack trace or even just, like the user journey, as I know you like to branch early in your code. JOËL: [laughs]. STEPHANIE: But you should also branch early from a user's experience [laughs]. JOËL: In general, I'm just a big fan of having a communication loop between development and product, not only sort of receiving a lot of useful information from the product team about what we want to build. But then because we're encountering this more, like, technical spec that we're writing, have those conversations bubble back to product and say, "Hey, so we talked about a dropdown where there are sort of three different ways of processing an application. Let's talk a little bit more about what it means to have three different ways of processing. And what do we want to name that? Is that accessible to everyone, or are they sort of one-to-one tied with a type of user?" And all of a sudden, that has just generated probably a lot of questions that product never even thought to ask because they're working on an infinite canvas of possibilities. And it's really helped you as a developer to have better names to write your code and sort of better sketch out the boundaries of the problem you're trying to solve. So, I think it's a really healthy loop to have. I strongly encourage it. So, we've spent a lot of time talking about thinking about behavior and things like the domain-driven design movement. But a few other things I want to shout out as being really helpful, one is an exercise where you take a problem statement and just underline all of the nouns. That is a great way to get a sense of, like, what is going on here. More generally, I think a lot of what we're talking about falls under the umbrella of what you might call analysis. And so, digging into different analytic techniques can be a great way to better understand the problem that you're working through. One such tool would be decision tables. So, you have a problem, and you say, "Well, given these inputs, what should the outputs be?" STEPHANIE: Cool. If there were any techniques or tools that we missed in terms of how you load code in your brain or generate code from your brain [laughs], we would love to know. You can write in to us at hosts@bikeshed.fm. JOËL: On that note, shall we wrap up? STEPHANIE: Let's wrap up. STEPHANIE: Show notes for this episode can be found at bikeshed.fm. JOËL: This show has been produced and edited by Mandy Moore. STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show. JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter. STEPHANIE: Or reach both of us at hosts@bikeshed.fm via email. JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week. ALL: Byeeeeeeee!!!!!!! AD: Did you know thoughtbot has a referral program? If you introduce us to someone looking for a design or development partner, we will compensate you if they decide to work with us. More info on our website at: tbot.io/referral. Or you can email us at: referrals@thoughtbot.com with any questions.Support The Bike Shed
Apr 15
39 min
422: Listener Topics Grab Bag
Joël conducted a thoughtbot mini-workshop on query plans, which Stephanie found highly effective due to its interactive format. They then discuss the broader value of interactive workshops over traditional talks for deeper learning. Addressing listener questions, Stephanie and Joël explore the strategic use of if and else in programming for clearer code, the importance of thorough documentation in identifying bugs, and the use of Postgres' EXPLAIN ANALYZE, highlighting the need for environment-specific considerations in query optimization. Episode mentioning query plans Query plan visualizer RailsConf 2024 Episode 349: Unpopular Opinions Squint test Episode 405: Retro on Sandi Metz rules Structuring conditionals in a wizard Episode 417: Module docs Episode 416: Multidimensional numbers ruby-units gem Solargraph parity Transcript: STEPHANIE:  Hello and welcome to another episode of The Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Stephanie Minn. JOËL: And I'm Joël Quenneville, and together, we're here to share a bit of what we've learned along the way. STEPHANIE: So, Joël, what's new in your world? JOËL: Just recently, I ran a sort of mini workshop for some colleagues here at thoughtbot to dig into the idea of query plans and, how to read them, how to use them. And, initially, this was going to be more of a kind of presentation style. And a colleague and I who were sharing this decided to go for a more interactive format where, you know, this is a, like, 45-minute slot. And so, we set it up so that we did a sort of intro to query plans in about 10 minutes then 15 minutes of breakout rooms, where people got a chance to have a query plan. And they had some sort of comprehension questions to answer about it. And then, 15 minutes together to have each group share a little bit about what they had discovered in their query plan back with the rest of the group, so trying to balance some understanding, some application, some group discussion, trying to keep it engaging. It was a pretty fun approach to sharing information like that. STEPHANIE: Yeah. I wholeheartedly agree. I got to attend that workshop, and it was really great. Now that I'm hearing you kind of talk about the three different components and what you wanted people attending to get out of it, I am impressed because [laughs] there is, like, a lot more thought, I think, that went into just participant engagement that reflecting on it now I'm like, oh yeah, like, I think that was really effective as opposed to just a presentation. Because you had, you know, sent us out into breakout rooms, and each group had a different query that they were analyzing. You had kind of set up links that had the query set up in the query analyzer. I forget what the tool was called that you used. JOËL: I forget the name of it, but we will link it in the show notes. STEPHANIE: Yeah. It was helpful for me, though, because, you know, I think if I were just to have learned about it in a presentation or even just looked at, you know, screenshots of it on a slide, that's different still from interacting with it and feeling more confident to use it next time I find myself in a situation where it might be helpful. JOËL: It's really interesting because that was sort of the goal of it was to make it a bit more interactive and then, hopefully, helping people to retain more information than just a straight up, like, presentation would be. I don't know how you feel, I find that often when I go to a place like, let's say, RailsConf, I tend to stay away from more of the workshop-y style events and focus more on the talks. Is that something that you do as well? STEPHANIE: Yeah. I have to confess that I've never attended a workshop [laughs] at a conference. I think it's partly my learning style and also partly just honestly, like, my energy level when I'm at the conference. I kind of just want to sit back. It's on my to-do list. Like, I definitely want to attend one just to see what it's like. And maybe that might even inspire me to want to create my own workshop. But it's like, once I'm in it, and, you know, like, everyone else is also participating, I'm very easily peer pressured [laughs]. So, in a group setting, I will find myself enjoying it a lot more. And I felt that kind of same way with the workshop you ran for our team. Though, I will say a funny thing that happened was that when I went out into my breakout group with another co-worker, and we were trying to grok this query that you gave us, we found out that we got the hardest one, the most complicated one [laughs] because there were so many things going on. There was, like, multiple, like, you know, unions, some that were, like, nested, and then just, like, a lot of duplication as well, like, some conditions that were redundant because of a different condition happening inside of, like, an inner statement. And yeah, we were definitely scratching our heads for a bit and were very grateful that we got to come back together as a group and be like, "Can someone please help? [laughs] Let's figure out what's going on here." JOËL: Sort of close that loop and like, "Hey, here's what we saw. What does everybody else see?" STEPHANIE: Yeah, and I appreciated that you took queries from actual client projects that you were working on. JOËL: Yeah, that was the really fun part of it was that these were not sort of made-up queries to illustrate a point. These were actual queries that I had spent some time trying to optimize and where I had had to spend a lot of time digging into the query plans to understand what was going on. And it sounds like, for you, workshops are something that is...they're generally more engaging, and you get more value out of them. But there's higher activation energy to get started. Does that sound right? STEPHANIE: Yeah, that sounds right. I think, like, I've watched so many talks now, both in person and on YouTube, that a lot of them are easily forgettable [laughs], whereas I think a workshop would be a lot more memorable because of that interactivity and, you know, you get out of it what you put in a little bit. JOËL: Yeah, that's true. Have you looked at the schedule for RailsConf 2024 yet? And are there any workshops on there that you're maybe considering or that maybe have piqued your interest? STEPHANIE: I have, in fact, and maybe I will check off attending a workshop [laughs] off my bucket list this year. There are two that I'm excited about. Unfortunately, they're both at the same time slot, so I -- JOËL: Oh no. You're going to have to choose. STEPHANIE: I know. I imagine I'll have to choose. But I'm interested in the Let's Extend Rails With A Gem by Noel Rappin and Vision For Inclusion Workshop run by Todd Sedano. The Rails gem one I'm excited about because it's just something that I haven't had to do really in my dev career so far, and I think I would really appreciate having that guidance. And also, I think that would be motivation to just get that, like, hands-on experience with it. Otherwise, you know, this is something that I could say that I would want to do and then never get [chuckles] around to it. JOËL: Right, right. And building a gem is the sort of thing that I think probably fits better in a workshop format than in a talk format. STEPHANIE: Yeah. And I've really appreciated all of Noel's content out there. I've found it always really practical, so I imagine that the workshop would be the same. JOËL: So, other than poring over the RailsConf schedule and planning your time there, what has been new for you this week? STEPHANIE: I have a really silly one [laughs]. JOËL: Okay. STEPHANIE: Which is, yesterday I went out to eat dinner to celebrate my partner's birthday, and I experienced, for the first time, robots [laughter] at this restaurant. So, we went out to Hot Pot, and I guess they just have these, like, robot, you know, little, small dish delivery things. They were, like, as tall as me, almost, at least, like, four feet. They were cat-themed. JOËL: [laughs] STEPHANIE: So, they had, like...shaped like cat...they had cat ears, and then there was a screen, and on the screen, there was, like, a little face, and the face would, like, wink at you and smile. JOËL: Aww. STEPHANIE: And I guess how this works is we ordered our food on an iPad, and if you ordered some, like, side dishes and stuff, it would come out to you on this robot cat with wheels. JOËL: Very fun. STEPHANIE: This robot tower cat. I'm doing a poor job describing it because I'm still apparently bewildered [laughs]. But yeah, I was just so surprised, and I was not as...I think I was more, like, shocked than delighted. I imagine other people would find this, like, very fun. But I was a little bit bewildered [laughs]. The other thing that was very funny about this experience is that these robots were kind of going down the aisle between tables, and the aisles were not quite big enough for, like, two-way traffic. And so, there were times where I would be, you know, walking up to go use the restroom, and I would turn the corner and find myself, like, face to face with one of these cat robot things, and, like, it's starting to go at me. I don't know if it will stop [laughs], and I'm the kind of person who doesn't want to find out. JOËL: [laughs] STEPHANIE: So, to avoid colliding with this, you know, food delivery robot, I just, like, ran away from it [laughs]. JOËL: You don't know if they're, like, programmed to yield or something like that. STEPHANIE: Listen, it did not seem like it was going to stop. JOËL: [laughs] STEPHANIE: It got, like, I was, you know, kind of standing there frozen in paralysis [laughs] for a little while. And then, once it got, I don't know, maybe two or three feet away from me, I was like, okay, like, this is too close for comfort [laughs]. So, that was my, I don't know, my experience at this robot restaurant. Definitely starting to feel like I'm in the, I don't know, is this the future? Someone, please let me know [laughs]. JOËL: Is this a future that you're excited or happy about, or does this future seem a little bit dystopian to you? STEPHANIE: I was definitely alarmed [laughter]. But I'm not, like, a super early adopter of new technology. These kinds of innovations, if you will, always surprise me, and I'm like, oh, I guess this is happening now [laughs]. And I will say that the one thing I did not enjoy about it is that there was not enough room to go around this robot. It definitely created just pedestrian traffic issues. So, perhaps this could be very cool and revolutionary, but also, maybe design robots for humans first. JOËL: Or design your dining room to accommodate your vision for the robots. I'm sure that flying cars and robots will solve all of this, for sure. STEPHANIE: Oh yeah [laughter]. Then I'll just have to worry about things colliding above my head. JOËL: And for the listeners who cannot see my face right now, that was absolutely sarcasm [laughs]. Speaking of our listeners, today we're going to look at a group of different listener questions. And if you didn't know that, you could send in a question to have Stephanie and I discuss, you can do that. Just send us an email at hosts@bikeshed.fm. And sometimes, we put it into a regular episode. Sometimes, we combine a few and sort of make a listener question episode, which is what we're doing today. STEPHANIE: Yeah. It's a little bit of a grab bag. JOËL: Our first question comes from Yuri, and Yuri actually has a few different questions. But the first one is asking about Episode 349, which is pretty far back. It was my first episode when I was coming on with Chris and Steph, and they were sort of handing the baton to me as a host of the show. And we talked about a variety of hot takes or unpopular opinions. Yuri mentions, you know, a few that stood out to him: one about SPAs being not so great, one about how you shouldn't need to have a side project to progress in your career as a developer, one about developer title inflation, one about DRY and how it can be dangerous for a mid-level dev, avoiding let in RSpec specs, the idea that every if should come with an else, and the idea that developers shouldn't be included in design and planning. And Yuri's question is specifically the question about if statements, that every if should come with an else. Is that still an opinion that we still have, and why do we feel that way? STEPHANIE: Yeah, I'm excited to get into this because I was not a part of that episode. I was a listener back then when it was still Steph and Chris. So, I am hopefully coming in with a different, like, additional perspective to add as well while we kind of do a little bit of a throwback. So, the one about every if should come with an else, that was an unpopular opinion of yours. Do you mind kind of explaining what that means for you? JOËL: Yeah. So, in general, Ruby is an expression-oriented language. So, if you have an if that does not include an else, it will implicitly return nil, which can burn you. There may be some super expert programmers out there that have never run into undefined method for nil nil class, but I'm still the kind of programmer who runs into that every now and then. And so, implicit nils popping up in my code is not something I generally like. I also generally like having explicit else for control flow purposes, making it a little bit clearer where flow of control goes and what are the actual paths through a particular method. And then, finally, doing ifs and elses instead of doing them sort of inline or as trailing conditionals or things like that, by having them sort of all on each lines and balancing out. The indentation itself helps to scan the code a little bit more. So, deeper indentation tells you, okay, we're, like, nesting multiple conditions, or something like that. And so, it makes it a little bit easier to spot complexity in the code. You can apply, and I want to say this is from Sandi Metz, the squint test. STEPHANIE: Yeah, it is. JOËL: Where you just kind of, like, squint at your code so you're not looking at the actual characters, and more of the structure, and the indentation is actually a friend there rather than something to fight. So, that was sort of the original, I think, idea behind that. I'm curious, in your experience, if you would like to balance your conditionals, ifs with something else, or if you would like to do sort of hanging ifs. STEPHANIE: Hanging ifs, I like that phrase that you just coined there. I agree with your opinion, and I think it's especially true if you're returning values, right? I mean, in Ruby, you kind of always are. But if you are caring about return values, like you said, to avoid that implicit nil situation, I find, especially if you're writing tests for that code, it's really easy, you know, if you spot that condition, you're like, okay, great. Like, this is a path I need to test. But then, oftentimes, you don't test that implicit path, and if you don't enter the condition, then what happens, right? So, I think that's kind of what you're referring to when you talk about both. It's, like, easier to spot in terms of control flow, like, all the different paths of execution, as well as, yeah, like, saving you the headaches of some bugs down the line. One thing that I thought about when I was kind of revisiting that opinion of yours is the idea of like, what are you trying to communicate is different or special about this condition when you are writing an if statement? And, in my head, I kind of came up with a few different situations you might find yourself in, which is, one, where you truly just have, like, a special case, and you're treating that completely differently. Another when you have more of a, like, binary situation, where it's you want to kind of highlight either...more of a dichotomy, right? It's not necessarily that there is a default but that these are two opposite things. And then, a third situation in which you have multiple conditions, but you only happen to have two right now. JOËL: Interesting. And do you think that, like, breaking down those situations would lead you to pick different structures for writing your conditionals? STEPHANIE: I think so. JOËL: Which of those scenarios do you think you might be more likely to reach for an if that doesn't have an else that goes with it? STEPHANIE: I think that first one, the special case one. And in Yuri's email, he actually asked, as a follow-up, "Do we avoid guard clauses as a result of this kind of heuristic or rule?" And I think that special case situation is where a guard clause would shine because you are highlighting it by putting it at the top of a method, and then saying like, you know, "Bail out of this" or, like, "Return this particular thing, and then don't even bother about the rest of this code." JOËL: I like that. And I think guard clauses they're not the first thing I reach for, but they're not something I absolutely avoid. I think they need to be used with care. Like you said, they have to be in the top of your method. If you're adding returns and things that break out of your method, deep inside a conditional somewhere, 20 lines into your method, you don't get to call that a guard clause anymore. That's something else entirely. I think, ideally, guard clauses are also things that will break out of the method, so they're maybe raising exception. Maybe they're returning a value. But they are things that very quickly check edge cases and bail so that the body of the method can focus on expecting data in the correct shape. STEPHANIE: I have a couple more thoughts about this; one is I'm reminded of back when we did that episode on kind of retroing Sandi Metz's Rules For Developers. I think one of the rules was: methods should only be five lines of code. And I recall we'd realized, at least I had realized for the first time, that if you write an if-else condition in Ruby, that's exactly five lines [laughs]. And so, now that I'm thinking about this topic, it's cool to see that a couple of these rules converge a little bit, where there's a bit of explicitness in saying, like, you know, if you're starting to have more conditions that can't just be captured in a five-line if-else statement, then maybe you need something else there, right? Something perhaps like polymorphic or just some way to have branched earlier. JOËL: That's true. And so, even, like, you were talking about the exceptional edge cases where you might want to bail. That could be a sign that your method is doing too much, trying to like, validate inputs and also run some sort of algorithm. Maybe this needs to be some sort of, like, two-step thing, where there's almost, like, a parsing phase that's handled by a different object or a different method that will attempt to standardize your inputs and raise the appropriate errors and everything. And then, your method that has the actual algorithm or code that you're trying to run can just assume that its inputs are in the correct shape, kind of that pushing the uncertainty to the edges. And, you know, if you've only got one edge case to check, maybe it's not worth to, like, build this in layers, or separate out the responsibilities, or whatever. But if you're having a lot, then maybe it does make sense to say, "Let's break those two responsibilities out into two places." STEPHANIE: Yeah. And then, the one last kind of situation I've observed, and I think you all talked about this in the Unpopular Opinions episode, but I'm kind of curious how you would handle it, is side effects that only need to be applied under a certain condition. Because I think that's when, if we're focusing less on return values and more just on behavior, that's when I will usually see, like, an if something, then do this that doesn't need to happen for the other path. JOËL: Yes. I guess if you're doing some sort of side effect, like, I don't know, making a request to an API or writing to a file or something, having, like, else return nil or some other sentinel value feels a little bit weird because now you're caring about side effects rather than return values, something that you need to keep thinking of. And that's something where I think my thing has evolved since that episode is, once you start having multiple of these, how do they compose together? So, if you've got if condition, write to a file, no else, keep going. New if condition, make a request to an API endpoint, no else, continue. What I've started calling independent conditions now, you have to think about all the different ways that they can combine, and what you end up having is a bit of a combinatorial explosion. So, here we've got two potential actions: writing to a file, making a request to an API. And we could have one or the other, or both, or neither could happen, depending on the inputs to your method, and maybe you actually want that, and that's cool. Oftentimes, you didn't necessarily want all of those, especially once you start going to three, four, five. And now you've got that, you know, explosion, like, two to the five. That's a lot of paths through your method. And you probably didn't really need that many. And so, that can get really messy. And so, sometimes the way that an if and an else work where those two paths are mutually exclusive actually cuts down on the total number of paths through your method. STEPHANIE: Hmm, I like that. That makes a lot of sense to me. I have definitely seen a lot of, like, procedural code, where it becomes really hard to tell how to even start relating some of these concepts together. So, if you happen to need to run a side effect, like writing to a file or, I don't know, one common one I can think of is notifying something or someone in a particular case, and maybe you put that in a condition. But then there's a different branching path that you also need to kind of notify someone for a similar reason, maybe even the same reason. It starts to become hard to connect, like, those two reasons. It's not something that, like, you can really scan for or, like, necessarily make that connection because, at that point, you're going down different paths, right? And there might be other signals that are kind of confusing things along the way. And it makes it a lot harder, I think, to find a shared abstraction, which could ultimately make those really complicated nested conditions a little more manageable or just, like, easier to understand at a certain complexity. I definitely think there is a threshold. JOËL: Right. And now you're talking about nested versus non-nested because when conditions are sort of siblings of each other, an if-else behaves differently from two ifs without an else. I think a classic situation where this pops up is when you're structuring code for a wizard, a multi-step form. And, oftentimes, people will have a bunch of checks. They're like, oh, if this field is present, do these things. If this field is present, do these things. And then, it becomes very tricky to know what the flow of control is, what you can expect at what moment, and especially which actions might get shared across multiple steps. Is it safe to refactor in one place if you don't want to break step three? And so, learning to think about the different paths through your code and how different conditional structures will impact that, I think, was a big breakthrough for me in terms of taking the next logical step in terms of thinking, when do I want to balance my ifs and when do I not want to? I wrote a whole article on the topic. We'll link it in the show notes. So, Yuri, thanks for a great question, bringing us back into a classic developer discussion. Yuri also asks or gives us a bit of a suggestion: What about revisiting this topic and doing an episode on hot takes or unpopular topics? Is that something that you'd be interested in, Stephanie? STEPHANIE: Oh yeah, definitely, because I didn't get to, you know, share my hot topics the last episode [laughs]. [inaudible 24:23] JOËL: You just got them queued up and ready to go. STEPHANIE: Yeah, exactly. So, yeah, I will definitely be brainstorming some spicy takes for the show. JOËL: So, Yuri, thanks for the questions and for the episode suggestion. STEPHANIE: So, another listener, Kevin, wrote in to us following up from our episode on Module Docs and about a different episode about Multi-dimensional Numbers. And he mentioned a gem that he maintains. It's called Ruby Units. And it basically handles the nitty gritty of unit conversions for you, which I thought was really neat. He mentioned that it has a numeric class, and it knows how to do math [laughs], which I would find really convenient because that is something that I have been grateful not to have to really do since college [laughs], at least those unit conversions and all the things that I'd probably learned in math and physics courses [laughs]. So, I thought that was really cool, definitely is one to check out if you frequently work with units. It seemed like it would be something that would make sense for a domain that is more science or deals in that kind of domain. JOËL: I'm always a huge fan of anything that tags raw numbers that you're working with with a quantity rather than just floating raw numbers around. It's so easy to make a mistake to either treat a number as a quantity you didn't think of, or make some sort of invalid operation on it, or even to think you have a value in a different size than you do. You think you're dealing with...you know you have a time value, but you think it's in seconds. It's actually in milliseconds. And then, you get off by some big factor. These are all mistakes that I have personally made in my career, so leaning on a library to help avoid those mistakes, have better information hiding for the things that really aren't relevant to the work that I'm trying to do, and also, kind of reify these ideas so that they have sort of a name, and they're, like, their own object, their own thing that we can interact with in the app rather than just numbers floating around, those are all big wins from my perspective. STEPHANIE: I also just thought of a really silly use case for this that is, I don't know, maybe I'll have to experiment with this. But every now and then, I find the need to have to convert a unit, and I just pop into Google, and I'm like, please give me, you know, I'll search for 10 kilometers in miles or something [laughs]. But then I have to...sometimes Google will figure it out for me, and sometimes it will just list me with a bunch of weird conversion websites that all have really old-school UI [laughs]. Do you know what I'm talking about here? Anyway, I would be curious to see if I could use this gem as a command-line interface [laughs] for me without having to go to my browser and roll the dice with freecalculator.com or something like that [laughs]. JOËL: One thing that's really cool with this library that I saw is the ability to define your own units, and that's a thing that you'll often encounter having to deal with values that are maybe not one of the most commonly used units that are out there, dealing with numbers that might mean a thing that's very particular to your domain. So, that's great that the library supports that. I couldn't see if it supports multi-dimensional units. That was the episode that inspired the comment. But either way, this is a really cool library. And thank you, Kevin, for sharing this with us. STEPHANIE: Kevin also mentions that he really enjoys using YARD docs. And we had done that whole episode on Module Docs and your experience writing them. So, you know, your people are out there [laughs]. JOËL: Yay. STEPHANIE: And we talked about this a little bit; I think that writing the docs, you know, on one hand, is great for future readers, but, also, I think has the benefit of forcing the author to really think about their inputs and outputs, as Kevin mentions. He's found bugs by simply just going through that process in designing his code, and also recommends Solargraph and Solargraph's VSCode extension, which I suspect really kind of makes it easy to navigate a complex codebase and kind of highlight just what you need to know when working with different APIs for your classes. So, I recently kind of switched to the Ruby LSP, build with Shopify, but I'm currently regretting it because nothing is working for me right now. So, that might be the push that I need [laughs] to go back to using Solargraph. JOËL: It's interesting that Kevin mentions finding bugs while writing docs because that has absolutely been my experience. And even in this most recent round, I was documenting some code that was just sort of there. It wasn't new code that I was writing. And so, I had given myself the rule that this would be documentation-only PRs, no code changes. And I found some weird code, and I found some code that I'm 98% sure is broken. And I had to have the discipline to just put a notice in the documentation to be like, "By the way, this is how the method should work. I'm pretty sure it's broken," and then, maybe come back to it later to fix it. But it's amazing how trying to document code, especially code that you didn't write yourself, really forces you to read it in a different way, interact with it in a different way, and really, like, understand it in a deep way that surprised me a little bit the first time I did it. STEPHANIE: That's cool. I imagine it probably didn't feel good to be like, "Hey, I know that this is broken, and I can't fix it right now," but I'm glad you did. That takes a lot of, I don't know, I think, courage, in my opinion [laughs], to be like, "Yeah, I found this, and I'm going to, you know, like, raise my hand acknowledging that this is how it is," as supposed to just hiding behind a broken functionality that no one [laughs] has paid attention to. JOËL: And it's a thing where if somebody else uses this method and it breaks in a way, and they're like, "Well, the docs say it should behave like this," that would be really frustrating. If the docs say, "Hey, it should behave like this, but it looks like it's broken," then, you know, I don't know, I would feel a little bit vindicated as a person who's annoyed at the code right now. STEPHANIE: For sure. JOËL: Finally, we have a message from Tim about using Postgres' EXPLAIN ANALYZE. Tim says, "Hey, Joël, in the last episode, you talked a bit about PG EXPLAIN ANALYZE. As you stated, it's a great tool to help figure out what's going on with your queries, but there is a caveat you need to keep in mind. The query planner uses statistics gathered on the database when making decisions about how to fetch records. If there's a big difference between your dev or staging database and production, the query may make different decisions. For example, if a table has a low number of records in it, then the query planner may just do a table scan, but in production, it might use an index. Also, keep in mind that after a schema changes, it may not know about new indexes or whatever unless an explicit ANALYZE is done on the table." So, this is really interesting because, as Tim mentions, EXPLAIN ANALYZE doesn't behave exactly the same in production versus in your local development environment. STEPHANIE: When you were trying to optimize some slow queries, where were you running the ANALYZE command? JOËL: I used a combination. I mostly worked off of production data. I did a little bit on a staging database that had not the same amount of records and things. That was pretty significant. And so, I had to switch to production to get realistic results. So, yes, I encountered this kind of firsthand. STEPHANIE: Nice. For some reason, this comment also made me think of..., and I wanted to plug a thoughtbot shell command that we have called Parity, which lets you basically download your production database into your local dev database if you use Heroku. And that has come in handy for me, obviously, in regular development, but would be really great in this use case. JOËL: With all of the regular caveats around security, and PII, and all this stuff that come with dealing with production data. But if you're running real productions on production, you should be cleared and, like, trained for access to all of that. I also want to note that the queries that you all worked with on Friday are also from the production database. STEPHANIE: Really? JOËL: So, you got to see what it actually does, what the actual timings were. STEPHANIE: I'm surprised by that because we were using, like, a web-based tool to visualize the query plans. Like, what were you kind of plugging into the tool for it to know? JOËL: So, the tool accepts a query plan, which is a text output from running a SQL query. STEPHANIE: Okay. So, it's just visualizing it. JOËL: Correct. Yeah. So, you've got this query plan, which comes back as this very intimidating block of, like, text, and arrows, and things like that. And you plug it into this web UI, and now you've got something that is kind of interactive, and visual, and you can expand or collapse nodes. And it gives you tooltips on different types of information and where you're spending the most time. So, yeah, it's just a nicer way to visualize that data that comes from the query plan. STEPHANIE: Gotcha. That makes sense. JOËL: So, that's a very important caveat. I don't think that's something that we mentioned on the episode. So, thank you, Tim, for highlighting that. And for all of our listeners who were intrigued by leaning into EXPLAIN ANALYZE and query plan viewers to debug your slow queries, make sure you try it out in production because you might get different results otherwise. STEPHANIE: So, yeah, that about wraps up our listener topics in recent months. On that note, Joël, shall we wrap up? JOËL: Let's wrap up. STEPHANIE: Show notes for this episode can be found at bikeshed.fm. JOËL: This show has been produced and edited by Mandy Moore. STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show. JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter. STEPHANIE: Or reach both of us at hosts@bikeshed.fm via email. JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week. ALL: Byeeeeeeeee!!!!!!! AD: Did you know thoughtbot has a referral program? If you introduce us to someone looking for a design or development partner, we will compensate you if they decide to work with us. More info on our website at: tbot.io/referral. Or you can email us at: referrals@thoughtbot.com with any questions.Support The Bike Shed
Apr 8
35 min
421: The Idealistic vs. Pragmatic Programmer
Stephanie revisits the concept of "spiking"—a phase of exploration to determine the feasibility of a technical implementation or to address unknowns in feature requests—sharing her recent experiences with a legacy Rails application. Joël brings a different perspective by discussing his involvement with a client project that heavily utilizes the dry-rb suite of gems, highlighting the learning curve associated with adapting to new patterns and libraries. Joël used to be much more idealistic and has moved to be more pragmatic. Stephanie has moved the other way. So together, Stephanie and Joël engage in a philosophical discussion on being an idealistic versus a pragmatic programmer. They explore the concept of programming as a blend of science and art, where technical decisions are not only about solving problems but also about expressing ideas and building shared understandings within a team. Spike tasks episode dry-rb Working with Maybe talk Problem solving with maybe Programming as Theory Building The Pragmatic Programmer Transcript:  JOËL: Hello and welcome to another episode of the Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Joël Quenneville. STEPHANIE: And I'm Stephanie Minn, and together, we're here to share a bit of what we've learned along the way. JOËL: So, Stephanie, what's new in your world? STEPHANIE: So, a few weeks ago, we did an episode on spiking in response to a listener question. And I wanted to kind of revisit that topic for a little bit because I've been doing a lot of spiking on my client project. And for those who are not familiar, the way that I understand or define spikes is kind of as an exploration phase to figure out if a technical implementation might work. Or if you have a feature request with some unknowns, you can spend some time-boxed spiking to figure out what those unknowns might be. And I'm working on your typical legacy Rails application [laughs]. And I think one thing that we talked about last time was this idea of, at what point does spiking end up being just working on the feature [laughs]? And I think that's especially true in an older codebase, where you kind of have to go down a few rabbit holes, maybe, just to even find out if something will trip you up down the line. And the way I approached that this time around was just, like, identifying the constraints and putting a little flag there for myself. Like, these were rabbit holes that I could go down, but, you know, towards the initial beginning phase of doing the spiking, I decided not to. I just kind of bookmarked it for later. And once I had identified the main constraints, that was when I was like, okay, like, what kind of solutions can I come up with for these constraints? And that actually then helped me kind of decide which ones we're pursuing a little bit more to get, like, the information I needed to ultimately make a decision about whether this was worth doing, right? It kind of kept me...I'm thinking about, you know, when you are bowling with those safety guards [laughs], it keeps your ball from just rolling into the gutter. I think it helped with not going too deep into places that I may or may not be super fruitful while also, I think, giving me enough information to have a more realistic understanding of, like, what this work would entail. JOËL: Would you say that this approach that you're taking is inspired or maybe informed by the conversation we had on the episode? STEPHANIE: I was especially interested in avoiding the kind of binary of like, no, we can't do this because the system just, you know, isn't able to support it, or it's just too...it would be too much work. That was something I was really, like you said, kind of inspired by after that conversation because I wanted to avoid that trap a little bit. And I think another really helpful framing was the idea of, like, okay, what would need to be done in order to get us to a place where this could be possible? And that's why I think identifying those constraints was important because they're not constraints forever. Like, we could do something about them if we really wanted to, so kind of avoiding the, like, it's not possible, right? And saying like, "It could be. Here's all the things that we need to do in order to make it possible." But I think that helped shift the conversation, especially with stakeholders and stuff, to be a little bit more realistic and collaborative. So, Joël, what's new in your world? JOËL: So, I'm also on a new client project, and a thing that's been really interesting in this codebase is that they've been using the dry-rb suite of gems pretty heavily. And I've seen a lot about the suite of gems. I've read about them. Interestingly, this is kind of the first time that I've been on a codebase that sort of uses them as a main pattern in the app. So, there's been a bit of a learning curve there, and it's been really interesting. STEPHANIE: This is exciting to me because I know you have a lot of functional programming background, also, so it's kind of surprising that you're only now, you know, using something that explicit from functional languages in Ruby. And I'm curious: what's the learning curve, if not the paradigm? Like, what are you kind of encountering? JOËL: I think there's a little bit of just the translation. How do these gems sort of approach this? So, they have to do a couple of, like, clever Ruby things to make some of these features work. Some of these also will have different method names, so a lot of just familiarizing myself with the libraries. Like, oh, well, this thing that I'm used to having called a particular thing has a slightly different name here or maybe not having all of the utilities. I was like, oh, how do we traverse with this particular library? Then you have to, like, look it up. So, it's a lot of like, how do I do this thing I know how to do in, let's say, Elm? How do I translate that into Ruby? But then, also, some of the interplay of how that works in code that also does some very kind of imperative side effecty things also written by a team that is getting used to the pattern. And so, you'll sort of see things where people are pulling things in, but maybe you don't fully understand the deeper underlying approach that's meant to be used. STEPHANIE: Have you noticed any use cases where the dry-rb patterns really shine in your application? JOËL: A thing that's nice is that I think it really forces you to think about your edge cases in a way that sometimes Ruby developers play very fast and loose with "Yeah, whatever, it will never be nil." Push to production immediately start getting NoMethodError in your bug tracker. I never do this, by the way, but you know. STEPHANIE: [laughs]. JOËL: Speaking from a friend's experience [laughs]. STEPHANIE: Asking for a friend, yeah [laughs]. JOËL: I think a thing that I've sort of had to figure out sort of every time I deal with these patterns in different languages is just the importance of good composition and good separation. Because you're adding these sort of wrapper context around things, if you're constantly wrapping and unwrapping, you're like, check things inside, and then do the next thing, and then unwrap again and branch and check and do the next thing, that code becomes really clunky in a way that you just sort of expect to do if you're just writing code in regular Ruby with a nil. But it doesn't really work with a dry-rb maybe or a result. So, the pattern that I have found that works really well is to extract sort of every operation that can be, let's say, that could fail so that it would give you a result back. Extract that out into its own separate function that will construct a success or a failure, and then have your sort of main code that wants to then do a bunch of these things together. All it does is use some of the dry-rb helper methods to compose all of these together, whether that's just some sort of, like, do notation, or binding, or fmap, or something like that, which allows you to have sort of individual chunks that can fail, and then one sort of aggregator piece of code that just finds a way to combine all of them nicely. And that avoids you having to do all this repetition. STEPHANIE: Yeah, that makes a lot of sense. JOËL: It's a pattern, I think; I had to learn the hard way when I was working with Elm. Because if you're taking a potential nullable value and then you want to do things with it but then that potential operation is also nullable because the input was potentially null, and then that just sort of propagates all the way down the chain. So, my whole chain of functions now is doing checks for nullability. And in Ruby, I could just be like, no, I checked it in the first function. I can then just trust that it's not null down the chain. Elm doesn't do the like, trust me, bro. The compiler will force you to validate every time, and then the code just blows up, and it gets really painful. So, I had to start thinking about new models of thinking that would separate out code that actually needs to care and code that doesn't need to care about nullability. And I wrote an article about that. That turned into actually a conference talk as well. And these sort of ideas have served me really well at Elm. And I think these translate pretty well to dry-rb as well. That's something that I'm exploring, but the principles seem like they're not tied to a particular language. STEPHANIE: Yeah, and it's kind of cool that you experienced all of that in working with Elm, where a compiler was there to yell at you [laughs] and kind of forcing you to...I don't know if do the right thing is the right word, but kind of think in the way that it wants you to think. And I can see people who are coming from Ruby and starting to experiment with dry-rb maybe needing a bit of that since it's not built-in in the tooling, just in a recoder view or just in conversations among devs. JOËL: [inaudible 09:26] Beyond just the idea of wrapping your values and making sure you check them all the time, there are patterns that make that easier or more painful. And even in something like Elm, the compiler would yell at me would make sure I could not have a runtime error by forgetting to check for nullability. It did not prevent me from writing monstrosities of nested repeated conditionals checking if nil, if nil, if nil. That I had to figure out some sort of, like, higher-level patterns that play nicely with that kind of software. And I think these are things that people have to sort of encounter, feel the pain, feel the frustration, and then move into those better patterns after the fact. And sometimes that's not easy because it's not obvious why that's a valuable pattern to approach. STEPHANIE: Yeah, I agree completely. Speaking of following patterns and kind of arriving at maybe an ideal version of [chuckles], you know, what you'd like your code to do, you know, to build what you are looking to build [laughs]...this is my very poor attempt at a smooth transition that Joël [laughter] manages to be able to do [laughs] whenever we're trying to shift into the topic of the episode. Anyway, today, we were hoping to talk a little bit about this idea between being an idealistic programmer and a pragmatic programmer and the different journeys that we've each been on in arriving kind of how to balance the two. JOËL: Yeah, you know, I think neither of these are absolutes, right? It's a spectrum. You probably move around that spectrum from day to day, and then probably, like, more general trends over your career. But I'm curious, for you today, if you had to pick one of those labels, like, which sort of zone of the spectrum would you put yourself in? Do you think you're more idealistic or more pragmatic? STEPHANIE: I think I'm in a more of an idealistic zone right now. JOËL: Would you say you're kind of like middle trending idealistic or kind of, like, pretty far down the idealistic side? STEPHANIE: Middle trending idealistic. I like that way of describing it. I want to know where you are. And then I kind of wanted to try to take a step back and even define what that means for both of us. JOËL: Right, right. I think the way I'd probably describe myself is a recovering idealist. STEPHANIE: Oof. Yeah [laughs]. JOËL: I think there was a time where I was really idealistic. I really like knowing sort of underlying theory of software construction, broader patterns. By patterns here, I don't mean necessarily, like, you know, the Gang of Four, but just general sort of approaches that work well and using that to guide my work. But I've also been trending a lot more into the, like, pragmatic side of things in the past few years. STEPHANIE: So, could you kind of tell me a little bit about what does pragmatic mean for you and what does ideal mean for you? JOËL: So, I think the pragmatic side of me it's about delivering working software. If you're not shipping anything, you know, the most beautiful piece of art that you've created just warms your heart is useless. So, I think I'm sort of at the extreme end of pragmatism, right? It's all about shipping and shipping fast. And, in the end, that's generally the goal of software. On the more idealistic side, the sort of doing everything kind of perfect or by the book, or, you know, maybe in a way that brings you personal satisfaction, oftentimes, at the expense of shipping and vice versa. Sometimes shipping comes at the expense of writing absolutely terrible code, but, of course, you know, there's value in both. Shipping is what actually delivers value to your users, your company, yourself if you're using the software. But if you're not following patterns and things, you're often stuck in a really short-term thinking loop, where you are maybe delivering value today at the cost of being able to deliver value tomorrow or writing code that is unreadable or code that is difficult to collaborate on. So, more than just me shipping an individual feature, I've got to think about, while I'm working with a team, how can I help them be able to ship features or build on top of my work for tomorrow? So, that's sort of how I visualize the field. I'm curious what the words idealism and pragmatism mean to you. STEPHANIE: Yeah. I agree with you that pragmatism is, you know, this idea of delivering working software. And I think I have seen it very, you know, kind of condensed as, like, moving quickly, getting stuff out the door, basically, like, end result being, like, a thing that you can use, right? I think I've personally been reassessing that idea a lot because I'm kind of almost wondering like, well, what are we moving quickly for [laughs]? I sometimes have seen pragmatism just end there being like, okay, like, it's all about velocity. And then, I'm kind of stuck being like, well, if you write working software for, you know, completely the wrong thing, is that still pragmatic? I don't know. So, that's kind of where I'm at these days with–I'm feeling a little bit more suspect of pragmatism, at least wanting to make sure that, especially with the people that I'm working with day to day, that we're agreeing on what that means and what success means. And then, as for idealism, I think also, actually, I now have a little bit of duality in terms of how I understand that as well. One of them being, yes, definitely, like, by the book or, like, by the ideas that we've, you know, some very smart people [laughs] have figured out as, like, this is clean or good quality, or these are the patterns to, you know, make your code as, again, as clean, I don't know, kind of putting air quotes around that, as possible. And then, I actually like what you really said about code that warms your heart [laughs] that you feel, like, really moved by or, like, just excited about or inspired by because I think that can also be a little bit different from just following theories that other people have defined. The more I spend doing this stuff, the more I am convinced that writing software is actually a very creative practice. And that's something that I've, like, definitely had to balance with the pragmatism a bit more because there are days when it's just not coming [chuckles], you know, like, I just stare at a blank, new file. And I'm like, I can't even imagine what these classes would be because, like, that creative part of my brain just, like, isn't on that day. So, that's kind of where I'm sitting in terms of, like, what idealistic programming kind of seems to me. JOËL: There's definitely an element of programming that feels like self-expression, you know, there are parameters around that. And working with a team, you probably all sort of, like, move towards some average. But I would definitely say that there is some element of self-expression in coding. STEPHANIE: Yeah, 100%. Have you heard about this paper called Programming as Theory Building? JOËL: The name sounds vaguely familiar, but I can't place the main idea in my mind right now. STEPHANIE: It's, like, an academic-ish paper from the 80s. And I'll link to it in the show notes because I can't remember the author right now. But the idea is writing code is actually just one way of expressing a theory that we are building. In fact, that expression doesn't even....it's like, it's impossible for it to fully encapsulate everything that was involved in the building of the theory because every decision you make, you know, you decide what not to do as well, right? Like, all the things that you didn't encode in your application is still part of this theory, like stuff that you rejected in order to interpret and make abstract the things that you are translating from the quote, unquote "real world" into code. That really stuck with me because, in that sense, I love this idea that you can create your own little world, right? Like, you're developing it when you code. And that is something that gets lost a little bit when we're just focused on the pragmatic side of things. JOËL: Where things get tricky as well is that when you're working with a team, you're not just building your own little world. You're building a shared world with shared mental models, shared metaphors. That's where oftentimes it becomes important to make sure that the things that you are thinking about are expressed in a way that other people could read your code and then immediately pick up on what's happening. And that can be through things like documentation, code comments. It can also be through more rigorous data modeling. So, for example, I am a huge fan of value objects in general. I tend to not have raw numbers floating around in an app. I like to wrap them in some kind of class and say, "Hey, these numbers that are floating around they actually represent a thing," and I'll name that thing so that other people can get a sense that, oh, it is one of the moving parts of this app, and then here are the behaviors that we expect on it. And that is partly for sort of code correctness and things like that but also as a sort of way of communicating and a way of contributing to that shared reality that we're creating with the team in a way that if I just left a raw number, that would be almost, like, leaving something slightly undefined. Like, the number is there. It does a thing, but what it does is maybe a little bit more implied. I know in my mind that this is a dollar amount, and maybe there's even a comment above it that says, "Dollar amount." But it makes it a little bit harder for it to play in with everybody else's realities or view of the system than if it were its own object. STEPHANIE: Yeah, I like what you said about you're building a shared world with your fellow colleagues. And that helped explain to me why, as some people say, naming is the hardest part about building software because, yeah, like you said, even just saying you are wanting to make a method or class expressive. And we talked about how code is a way of expressing yourself. You could, like, name all your stuff in Wingdings [laughs], but we don't. I actually don't know if you could do that. But that was, for some reason, what I imagined. I was like, it's possible, and you could deliver software in complete gibberish [laughs]. JOËL: In theory, could you say that naming your variables as emoji is the most expressive way? Because now it's all emotions. STEPHANIE: A picture is worth a thousand words, as they say. JOËL: So, this variable is the frowny face, upside-down smile face. It doesn't get more expressive than that. STEPHANIE: At a former company, in our Slack workspace, I had a co-worker who loved to use the circus tent emoji to react to things. And, like, I'm convinced that no one really knew what it meant, but we also kind of knew what it meant. We were just like, oh yeah, that's the emoji that she uses to express amusement or, like, something a little bit ironic. And we all kind of figured it out [laughs] eventually. So, again, I do think it's possible. I bet someone has done, like, a creative experiment with writing an application in just emojis. This is now going to be some research I do after this episode [laughter]. JOËL: It is fun when you have, like, a teammate. You know they have the signature emoji that they respond to on things. STEPHANIE: Yep. Absolutely. So, you know, we kind of spent a little bit of time talking about idealism. I actually wanted to pull back to the idea of pragmatism because, in preparation for this episode, I also revisited my copy of The Pragmatic Programmer. Are you familiar with this book? Have you read it at all? JOËL: I have read it. It's been probably ten years. We did, I think, a book club at thoughtbot to go through the book. STEPHANIE: I was skimming the table of contents because I was curious about, again, that, like, definition of pragmatism. You and I had kind of talked about how it can be short-sighted. But what I was actually pretty impressed with, and I imagine this is why the book holds up, you know, after decades, is success for them also means being able to continue to deliver quality software. And that idea of continuity kind of implied, to me, that there was an aspect of, like, making sure the quality meets a certain threshold and, like, incorporating these theories and doing the best practices because they're thinking about success over time, right? Not just the success of this particular piece that you're delivering. JOËL: I would say most people in our industry are sort of balancing those two objectives, right? They're like, we want to have a decent velocity and ship things, but at the same time, we want to be able to keep delivering. We want a certain threshold of quality. In between those two objectives, there is a sea of trade-offs, and how you manage them are probably a little bit part of your personality as a developer and is probably also, to a certain extent, a function of your experience, learning sort of when to lean more into taking some shortcuts to ship faster and when to double down on certain practices that increase code quality, and what aspects of quality value more than others because not all forms of quote, unquote, "quality" are the same. I think a sort of source of danger, especially for newer developers, is you sort of start on almost, like, a hyper-pragmatic side of things because most people get into software because they want to build things. And the ultimate way to build is to ship, and then you sort of encounter problems where you realize, oh, this code is really clunky. It's harder and harder to ship. Let me learn some elements of code quality. Let's get better at my craft so that I can build software that has fewer bugs or that I can ship more consistently. And that's great. And then, you sort of run into some, like, broader sort of theories of programming: patterns, structures, things like that. And it becomes very easy to sort of blindly copy-paste that everywhere to the point where I think it's almost a bit of a meme, the, like, intermediate programmer who's read Clean Code or the Design Patterns book and is just now, like, applying these things blindly to every piece of code they encounter to the annoyance of the entire team. STEPHANIE: I think you just about described my trajectory [laughter], though hopefully, I was not so obnoxious about [laughs] it for my team having to deal with my, like, discovering [laughs] theories that have long been used. JOËL: I think we kind of all go through that journey to a certain extent, right? It's a little bit different for every one of us, but I think this is a journey that is really common for developers. STEPHANIE: Yeah. One thing I frequently think about a lot is how much I wished I had known some of that theory earlier. But I don't think I have an answer one way or another. It's like; I'm not sure if having that knowledge earlier really would have helped me because I've also definitely been in...I'm just thinking about, like, when I was in college in lectures trying to absorb theories that made no sense to me because I had no, like, practical experience to connect it to. It's almost, like, maybe there is, like, that perfect time [laughs] where it is the most valuable for what you're doing. And I don't know. I kind of believe that there is a way to bridge that gap. JOËL: I mean, now we're kind of getting into an element of pedagogy. Do you sort of teach the theory first, and then show how to apply it to problems? Or do you show problems and then introduce bits of theory to help people get unstuck and maybe then cap it off by like, oh, these, like, five different, like, techniques I showed you to, like, solve five different problems, turns out they all fit in some grand unified theory? And, like, here's how the five things you thought were five different techniques are actually the same technique viewed from five different perspectives. Let me blow your mind. STEPHANIE: That's a Joël approach [laughter] to teaching if I've ever heard one. JOËL: I'm a huge fan of that approach. Going back to some of the, like, the functional programming ideas, I think that's one that really connected for me. I struggled to learn things like monads, and functors, and things like that. And I think, in my mind, these two approaches is like the Haskell school of teaching and the Elm school of teaching. Haskell will sort of say, "Hey, let me teach you about this theory of monads and all these things, and then, we'll look at some ways where that can be applied practically." Whereas Elm will say, "No, you don't need to know about this. Let's look at some practical problems. Oh, you've got null values you need to check. Here's how you can, like, handle nullability in a safe way. Oh, you've got a bunch of HTTP requests that might resolve in random order, and you want to, like, deal with them when they all come back. Here's some tips on how you can do that." And then, you have three or four things, and then, eventually, it just sort of lets you say, "Wait a minute, all of these problems are sort of all the same, and it turns out they all fit in some unified theory." And then, the light bulb goes off, and you're like, "Ooh, so now when I'm dealing with unknown blobs of Jason trying to parse data out of them, I'll bet I can use the same techniques I used for chaining HTTP requests to dig multiple dependent pieces of JSON." STEPHANIE: Yeah. And that's so satisfying, right? It really is kind of leveling up in that Galaxy Brain meme sort of way. JOËL: Yeah. And that's maybe to a certain extent even a value of idealism because if you build your system in such a way that it follows some of these patterns, then insights and intuitions that people have in one part of your code can then carry to other parts of your code, and that's incredibly powerful. STEPHANIE: Yeah. And I almost wonder because you also mentioned kind of where you end up on the spectrum is a function of your experience. I wonder if us, you know, being consultants and seeing patterns across many applications also kind of contributes to the striving for idealism [laughs]. JOËL: It's kind of both, right? Because there's very high incentive to ship pretty rapidly, especially if you're on a shorter engagement or if you're on a project that has a shorter timescale. But also, yes, because you've seen so many projects, you've seen how things can go wrong. Also, you've seen the same problem from 20 different perspectives that are all slightly different. And so, some of those broader patterns can start emerging in your head. STEPHANIE: Yeah, honestly, I think that's kind of the work that I enjoy the most in consulting because a lot of clients bring us on when they're like, "Hey, like, we've reached a point where our velocity has slowed down. Like, can you help us unstick our developers?" And that's actually when I've found that leaning on the theories and maybe a little bit of idealism is actually really useful because I'm kind of providing those tools to developers at this time when they need it. That's kind of why I have been saying trending idealism because I have found that particularly useful at work. JOËL: There's an element here of, like, looking at a bunch of different use cases and then finding some sort of unifying model or theory. And that's a word that I think programmers have a love-hate relationship with: Abstraction. I don't know about you, but designing abstractions is a lot of fun for me. I love designing abstractions. I have always loved designing abstractions. It's not always the best use of my time, and it's not always the best thing for a codebase. STEPHANIE: Ooh, okay, okay. This was a good transition. I hear you that, like, yeah, love-hate relationship. It's hard. That's kind of where I've ended up. It's really hard. And I think it's because it requires that creative thinking. JOËL: It requires that creative thinking. And then also, like, it requires you to sort of see more broadly, a more broad picture. What are the things that are connected, the things that are disconnected, even though they seem related? And, like, being able to sort of slice those similarities from each other. STEPHANIE: Yeah. I agree. And the interesting part is that, like, a lot of the time you just don't know yet. And you kind of have to come back to reality and admit that you don't know yet, you know, got to come back to earth, take a look around, and, yeah, you can go through the thought exercise of thinking [laughs] about all of the possibilities, and I imagine you could do that forever [laughs]. JOËL: I mean, that's why we have heuristics like the rule of three that says, "Don't abstract something out or attempt to DRY code until you've seen three use cases of it." So, maybe leave a little bit of duplication or a little bit of maybe not perfectly factored code until you have a couple of more examples. And the sort of real picture starts emerging a little bit more. STEPHANIE: So, I think we are kind of at this topic already, but was there a moment or was there something that kind of helped you realize, like, oh, I can't be in that space of imagining abstractions [laughs] forever when I have to deliver software? Like, what changed for you to be the, as you said yourself, recovering idealist and having to maybe employ some more pragmatic heuristics? JOËL: And I think, for me, it's partly being a consultant and being in a lot of projects and having that pressure to work with deadlines and sort of not having an infinite canvas to paint with, having to sort of fit some of my grand ideas into the reality of, we've got a week or two weeks to get this thing done, and also working with a team, and some ideas don't work well with every team. Every team is kind of at a different place. And abstractions sort of only serve you as well as they are useful to not only you but the team at large. So, if a team is not comfortable with a set of abstractions, or it's sort of, like, too far down a path, then that can be really challenging. And that's where something like the dry-rb set of gems, which has some really fun abstractions like a mental model for doing things, depending on the team, that can be a really heavy lift. And so, as much as I like those patterns, I might think long and hard before I try to push this on a whole team. STEPHANIE: Yeah, I kind of had to navigate a situation like that recently, where I was doing a code review, and I had left some suggestions about refactoring to encapsulate some responsibilities better. And then, I was like, oh, and then I noticed another thing that we could do to make that easier. And it, you know, definitely can start to spiral. And the author, you know, kind of responded to me and said, "Hey, like, I really appreciate these comments, but we are a bit tight on deadline for this project. So, is it okay if I, like, revisit this when we've delivered it?" And, you know, I was just like, "Yeah, it's totally up to you." At the end of the day, I want whoever's authoring this code to have, like, full agency about how they want to move forward. And it was really helpful for me to get that context of, like, oh, they're a bit tight on the deadline because then I can start to meet them where they're at. And maybe I can give some suggestions for moving towards that ideal state, but ones that are lower left, and that is still better than nothing. JOËL: That sounds awfully pragmatic. STEPHANIE: [laughs] JOËL: Moving in a positive direction, we're getting halfway. It's better than nothing. That's very pragmatic. STEPHANIE: Hmm. Wow. But it's pragmatically moving towards idealism. JOËL: [laughs] STEPHANIE: If that is even possible [laughs]. JOËL: Uh-huh. STEPHANIE: That's maybe the book that I'm going to write, not The Pragmatic Programmer, but The Pragmatically Idealistic Programmer [laughs]. JOËL: The Pragmatic Idealist. STEPHANIE: Ooh, yeah, I like that. Okay. Watch out for that book coming 2030 [laughter], written by me and Joël. JOËL: So, I think you brought up a really interesting point, which is the idea of pragmatism versus idealism when it comes to code review. Do you find that you think about these ideas differently when reviewing somebody else's code versus when you write your own? STEPHANIE: Oooh, yeah. I'm not sure exactly why, but definitely, when I'm reviewing someone else's code, I'm already in the headspace of, you know, I have some separation, right? Like, I'm not in the mode of thinking very hard [laughs] about what I'm creating. I'm just, like, in the editing kind of phase. And then, I can actually pull more from different theories and ideas, and I find that actually quite easier. When I'm writing my own code, it's just whatever comes out, right? And then, hopefully, I have the time to revisit it and give it a scan, and then start to integrate the, like, idealistic theories and the patterns that I would like to be using. But it definitely...for patterns that I feel a lot more confident about or more familiar with, they just come out mostly kind of oriented in that way if I have the time, or sometimes I will make the time, you know. I'll just say, "It's not done yet," because I know it can be better. I think that could be another, like, pragmatically idealist way of handling that. JOËL: [laughs] STEPHANIE: Right? It's just telling people, "I'm not done." [laughs] It's not done until I do at least give it an attempt. JOËL: So, it's kind of a two-phase thing when you're writing your own code, whereas it's only a single phase when you're reviewing somebody else's. STEPHANIE: Yeah. Yeah. But, like I said earlier, it's like, I also really believe that I don't want to impose any of my ideas [laughs] onto others. I really believe that people have to arrive at it on their own. So, it used to bother me a little bit more when I was just like, oh, but this way is better [laughs]. When people wouldn't get on board, I would be sad about it. But as long as I know that I, like, left that comment, then I can give myself a pat on the back for trying to move towards that ideal state. What about you [laughs]? JOËL: I think this is probably also where I'm, like, now a recovering idealist. There was a time where I would leave a ton of comments on someone's PR. I almost had a view of like, how can I help you get your PR to be the best it can possibly be? And sometimes, if you start with something that's very rough around the edges, you're leaving a lot of comments. And I've been that guy who's left 50 comments on a PR. In retrospect, I think that was not being a good teammate. STEPHANIE: Hmm. JOËL: So, I think maybe my mental model or my, like, goal for PR review has changed a little bit. It's less about how can I help you make your code the best it can possibly be? And a how can I help you get your code to mergeable? And it's possible that mergeable means best that it can possibly be, but that's usually not the case. So, I'm going to give you some feedback: some things that confuse me, maybe raise one or two patterns that are existing in the app that maybe you weren't aware of that you should maybe consider applying. Maybe I'll raise a couple of ideas that are new, but that apply here. And those might just be a, "Hey, let's just think about this. Maybe we don't want to do this in this PR, but maybe we want to look at them at some point. Or we should be thinking about this in a sort of rule of three situation. If we see this come up another time, maybe consider introducing a strategy pattern here, or maybe consider making this a value object, or separating these side effects from these pure behavior." But it's more of a dialogue about how can I help you get your PR to the point where it is mergeable? STEPHANIE: Yeah. Another thing I thought about just now is both are meaningful or, like, both can provide meaning in different ways, and people ascribe different amounts of meaning to both; where I had worked with someone, a client developer before, who was not super interested in doing any kind of refactoring or, like, any, you know, second passes for quality. Because, for him, like, he just wanted to ship, right? That was where he found meaning in his work. Whereas that actually made my work feel a lot more meaningless [chuckles] because I'm like, well, if we're just kind of hands on a keyboard, like robots shipping code, I don't know, that doesn't feel particularly motivating for me. You know, I do want to employ some of that craft a little bit more. JOËL: And, I guess, yeah, idealism versus pragmatism is also...it's a personal individual thing. There's an element where it's a team decision, or at least a sense of, like, how much quality do we need at this point in the life cycle of the project? And what are the areas where we particularly want to emphasize quality? What are our quality standards? And that's, to a certain extent, consensus among the team that it's individual members. And it's also coming from team leadership. STEPHANIE: Yeah. Yeah, exactly. I mentioned that, you know, just to, I think, shed a little bit of light that it's usually not personal, right [laughs]? There's that part of understanding that is really important to, yeah, like, keep building this shared world of writing software, and, hopefully, it should be meaningful for all of us. JOËL: I think a few takeaways that I have would be, one, the value of, like, theory and idealism. These things help you to become a better developer. They help you to spot patterns. It's probably good to sort of have in the background always be learning some new thing, whether that's learning a new set of patterns, or learning some mental models, thinking about, oh, the difference between side effects and pure code, learning about particular ways of structuring code. These are all things that are good to have in your back pocket to be able to apply to the code that you're doing, even if it's a sort of after-the-fact, hey, I've done a similar task three different times. Is there a broader principle? But then, also, take the time to really make sure that you're focusing on shipping code, and maybe that's learning to work in smaller chunks, working iteratively, learning to scope your work well. Because, in the end, delivering value is a thing that is something that we could all probably benefit from doing more of. And then, finally, taking some time to self-reflect, a little bit of self-awareness in this area. What are the aspects of pragmatism and idealism that you find personally meaningful? What are the elements that you think bring value to your work, to your team? And let that sort of guide you on your next code writing or PR review. STEPHANIE: On that note, shall we wrap up? JOËL: Let's wrap up. STEPHANIE: Show notes for this episode can be found at bikeshed.fm. JOËL: This show has been produced and edited by Mandy Moore. STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show. JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter. STEPHANIE: Or reach both of us at hosts@bikeshed.fm via email. JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week. ALL: Byeeeeeeee!!!!!! AD: Did you know thoughtbot has a referral program? If you introduce us to someone looking for a design or development partner, we will compensate you if they decide to work with us. More info on our website at: tbot.io/referral. Or you can email us at: referrals@thoughtbot.com with any questions.Support The Bike Shed
Apr 1
41 min
420: Test Database Woes
Joël shares his recent project challenge with Tailwind CSS, where classes weren't generating as expected due to the dynamic nature of Tailwind's CSS generation and pruning. Stephanie introduces a personal productivity tool, a "thinking cap," to signal her thought process during meetings, which also serves as a physical boundary to separate work from personal life. The conversation shifts to testing methodologies within Rails applications, leading to an exploration of testing philosophies, including developers' assumptions about database cleanliness and their impact on writing tests. Avdi’s classic post on how to use database cleaner RSpec change matcher Command/Query separation When not to use factories Why Factories? Transcript:  STEPHANIE: Hello and welcome to another episode of The Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Stephanie Minn. JOËL: And I'm Joël Quenneville. And together, we're here to share a bit of what we've learned along the way. STEPHANIE: So, Joël, what's new in your world? JOËL: I'm working on a new project, and this is a project that uses Tailwind CSS for its styling. And I ran into a bit of an annoying problem with it just getting started, where I was making changes and adding classes. And they were not changing the things I thought they would change in the UI. And so, I looked up the class in the documentation, and then I realized, oh, we're on an older version of the Tailwind Rails gem. So, maybe we're using...like, I'm looking at the most recent docs for Tailwind, but it's not relevant for the version I'm using. Turned out that was not the problem. Then I decided to use the Web Inspector and actually look at the element in my browser to see is it being overwritten somehow by something else? And the class is there in the element, but when I look at the CSS panel, it does not show up there at all or having any effects. And that got me scratching my head. And then, eventually, I figured it out, and it's a bit of a facepalm moment [laughs]. STEPHANIE: Oh, okay. JOËL: Because Tailwind has to, effectively, generate all of these, and it will sort of generate and prune the things you don't need and all of that. They're not all, like, statically present. And so, if I was using a class that no one else in the app had used yet, it hadn't gotten generated. And so, it's just not there. There's a class on the element, but there's no CSS definition tied to it, so the class does nothing. What you need to do is there's a rake task or some sort of task that you can run that will generate things. There's also, I believe, a watcher that you can run, some sort of, like, server that will auto-generate these for you in dev mode. I did not have that set up. So, I was not seeing that new class have any effect. Once I ran the task to generate things, sure enough, it worked. And Tailwind works exactly how the docs say they do. But that was a couple of hours of my life that I'm not getting back. STEPHANIE: Yeah, that's rough. Sorry to hear. I've also definitely gone down that route of like, oh, it's not in the docs. The docs are wrong. Like, do they even know what they're talking about? I'm going to fix this for everyone. And similarly have been humbled by a facepalm solution when I'm like, oh, did I yarn [laughs]? No, I didn't [laughs]. JOËL: Uh-huh. I'm curious, for you, when you have sort of moments where it's like the library is not behaving the way you think it is, is your default to blame yourself, or is it to blame the library? STEPHANIE: [laughs]. Oh, good question. JOËL: And the follow-up to that is, are you generally correct? STEPHANIE: Yeah. Yep, yep, yep. Hmm, I will say I externalize the blame, but I will try to at least do, like, the basic troubleshooting steps of restarting my server [laughter], and then if...that's as far as I'll go. And then, I'll be like, oh, like, something must be wrong, you know, with this library, and I turn to Google. And if I'm not finding any fruitful results, again, you know, one path could be, oh, maybe I'm not Googling correctly, but the other path could be, maybe I've discovered something that no one else has before. But to your follow-up question, I'm almost, like, always wrong [laughter]. I'm still waiting for the day when I, like, discover something that is an actual real problem, and I can go and open an issue [chuckles] and, hopefully, be validated by the library author. JOËL: I think part of what I heard is that your debugging strategy is basic, but it's not as basic as Joël's because you remember to restart the server [chuckles]. STEPHANIE: We all have our days [laughter]. JOËL: Next time. So, Stephanie, what is new in your world? STEPHANIE: I'm very excited to share this with you. And I recognize that this is an audio medium, so I will also describe the thing I'm about to show you [laughs]. JOËL: Oh, this is an object. STEPHANIE: It is an object. I got a hat [laughs]. JOËL: Okay. STEPHANIE: I'm going to put it on now. It's a cap that says "Thinking" on it [laughs] in, like, you know, fun sans serif font with a little bit of edge because the thinking is kind of slanted. So, it is designy, if you will. It's my thinking cap. And I've been wearing it at work all week, and I love it. As a person who, in meetings and, you know, when I talk to people, I have to process before I respond a lot of the time, but that has been interpreted as, you know, maybe me not having anything to say or, you know, people aren't sure if I'm, you know, still thinking or if it's time to move on. And sometimes I [chuckles], you know, take a long time. My brain is just spinning. I think another funny hat design would be, like, the beach ball, macOS beach ball. JOËL: That would be hilarious. STEPHANIE: Yeah. Maybe I need to, like, stitch that on the back of this thinking cap. Anyway, I've been wearing it at work in meetings. And then, when I'm just silently processing, I'll just point to my hat and signal to everyone what's [laughs] going on. And it's also been really great for the end of my work day because then I take off the hat, and because I've taken it off, that's, like, my signal, you know, I have this physical totem that, like, now I'm done thinking about work, and that has been working. JOËL: Oh, I love that. STEPHANIE: Yeah, that's been working surprisingly well to kind of create a bit more of a boundary to separate work thoughts and life thoughts. JOËL: Because you are working from home and so that boundary between professional life and personal life can get a little bit blurry. STEPHANIE: Yeah. I will say I take it off and throw it on the floor kind of dramatically [laughter] at the end of my work day. So, that's what's new. It had a positive impact on my work-life balance. And yeah, if anyone else has the problem of people being confused about whether you're still thinking or not, recommend looking into a physical thinking cap. JOËL: So, you are speaking at RailsConf this spring in Detroit. Do you plan to bring the thinking cap to the conference? STEPHANIE: Oh yeah, absolutely. That's a great idea. If anyone else is going to RailsConf, find me in my thinking cap [laughs]. JOËL: So, this is how people can recognize Bikeshed co-host Stephanie Minn. See someone walking around with a thinking cap. STEPHANIE: Ooh. thinkingbot? JOËL: Ooh. STEPHANIE: Have I just designed new thoughtbot swag [laughter]? We'll see if this catches on. JOËL: So, we were talking recently, and you'd mentioned that you were facing some really interesting dilemmas when it came to writing tests and particularly how tests interact with your test database. STEPHANIE: Yeah. So, I recently, a few weeks ago, joined a new client project and, you know, one of the first things that I do is start to run those tests [laughs] in their codebase to get a sense of what's what. And I noticed that they were taking quite a long time to get set up before I even saw any progress in terms of successes or failures. So, I was kind of curious what was going on before the examples were even run. And when I tailed the logs for the tests, I noticed that every time that you were running the test suite, it would truncate all of the tables in the test database. And that was a surprise to me because that's not a thing that I had really seen before. And so, basically, what happens is all of the data in the test database gets deleted using this truncation strategy. And this is one way of ensuring a clean slate when you run your tests. JOËL: Was this happening once at the beginning of the test suite or before every test? STEPHANIE: It was good that it was only running once before the test suite, but since, you know, in my local development, I'm running, like, a file at a time or sometimes even just targeting a specific line, this would happen on every run in that situation and was just adding a little bit of extra time to that feedback loop in terms of just making sure your code was working if that's part of your workflow. JOËL: Do you know what version of Rails this project was in? Because I know this was popular in some older versions of Rails as a strategy. STEPHANIE: Yeah. So, it is Rails 7 now, recently upgraded to Rails 7. It was on Rails 6 for a little while. JOËL: Very nice. I want to say that truncation is generally not necessary as of Rails...I forget if it's 5 or 6. But back in the day, specifically for what are now called system tests, the sort of, like, Capybara UI-driven browser tests, you had, effectively, like, two threads that were trying to access the database. And so, you couldn't have your test data wrapped in a transaction the way you would for unit tests because then the UI thread would not have access to the data that had been created in a transaction just for the test thread. And so, people would use tools like Database Cleaner to use a truncation strategy to clear out everything between tests to allow a sort of clean slate for these UI-driven feature specs. And then, I want to say it's Rails 5, it may have been Rails 6 when system tests were added. And one of the big things there was that they now could, like, share data in a transaction instead of having to do two separate threads and one didn't have access to it. And all of a sudden, now you could go back to transactional fixtures the way that you could with unit tests and really take advantage of something that's really nice and built into Rails. STEPHANIE: That's cool. I didn't know that about system tests and that kind of shift happening. I do think that, in this case, it was one of those situations where, in the past, the database truncation, in this case, particular using the Database Cleaner gem was necessary, and that just never got reassessed as the years went by. JOËL: That's one of the classic things, right? When you upgrade a Rails app over multiple versions, and sometimes you sort of get a new feature that comes in for free with the new version, and you might not be aware of it. And some of the patterns in the app just kind of keep going. And you don't realize, hey, this part of the app could actually be modernized. STEPHANIE: So, another interesting thing about this testing situation is that I learned that, you know, if you ran these tests, you would experience this truncation strategy. But the engineering team had also kind of played around with having a different test setup that didn't clean the database at all unless you opted into it. JOËL: So, your test database would just...each test would just keep writing to the database, but they're not wrapped in transactions. Or they are wrapped in transactions, but you may or may not have some additional data. STEPHANIE: The latter. So, I think they were also using the transaction strategy there. But, you know, there are some reasons that you would still have some data persisted across test runs. I had actually learned that the use transactional fixtures config for RSpec doesn't roll back any data that might have been created in a before context hook. JOËL: Yep, or a before all. Yeah, the transaction wraps the actual example, but not anything that happens outside of it. STEPHANIE: Yeah, I thought that was an interesting little gotcha. So, you know, now we had these, like, two different ways to run tests. And I was chatting with a client developer about how that came to be. And we then got into an interesting conversation about, like, whether or not we each expect a clean database in the first place when we write our tests or when we run our tests, and that was an area that we disagreed. And that was cool because I had not really, like, thought about like, oh, how did I even arrive at this assumption that my database would always be clean? I think it was just, you know, from experience having only worked in Rails apps of a certain age that really got onto the Database [laughs] Cleaner train. But it was interesting because I think that is a really big assumption to make that shapes how you then approach writing tests. JOËL: And there's kind of a couple of variations on that. I think the sort of base camp approach of writing Rails with fixtures, you just sort of have, for the most part, an existing set of data that's there that you maybe layer on a few extra things on. But there's base level; you just expect a bunch of data to exist in your test database. So, it's almost going off the opposite assumption, where you can always assume that certain things are already there. Then there's the other extreme of, like, you always assume that it's empty. And it sounds like maybe there's a position in the middle of, like, you never know. There may be something. There may not be something, you know, spin the wheel. STEPHANIE: Yeah. I guess I was surprised that it, you know, that was just a question that I never really asked myself prior to this conversation, but it could feel like different testing philosophies. But yeah, I was very interested in this, you know, kind of opinion that was a little bit different from mine about if you assume that your database, your test database, is not clean, that kind of perhaps nudges you in the direction of writing tests that are less coupled to the database if they don't need to be. JOËL: What does coupling to the database mean in this situation? STEPHANIE: So, I'm thinking about Rails tests that might be asserting on a change in database behavior, so the change matcher in RSpec is one that I see maybe sometimes used when it doesn't need to be used. And we're expecting, like, account to have changed the count of the number of records on it for a model have changed after doing some work, right? JOËL: And the change matcher from RSpec is one that allows you to not care whether there are existing records or not. It sort of insulates you from that. STEPHANIE: That's true. Though I guess I was thinking almost like, what if there was some return value to assert on instead? And would that kind of help you separate some side effects from methods that might be doing too much? And kind of when I start to see tests that have both or are asserting on something being returned, and then also something happening, that's one way of, like, figuring out what kind of coupling is going on inside this test. JOËL: It's the classic command-query separation principle from object-oriented design. STEPHANIE: I think another one that came to mind, another example, especially when you're talking about system tests, is when you might be using Capybara and you end up...maybe you're going through a flow that creates a record. But from the user perspective, they don't actually know what's going on at the database level. But you could assert that something was created, right? But it might be more realistic at that level of abstraction to be asserting some kind of visual element that had happened as a result of the flow that you're testing. JOËL: Yeah. I would, in fact, go so far as to say that asserting on the state of your database in a system test is an anti-pattern. System tests are sort of, by design, meant to be all about user behavior trying to mimic the experience of a user. And a user of a website is not going to be able to...you hope they're not able to SSH into [chuckles] your database and check the records that have been created. If they can, you've got another problem. STEPHANIE: I wonder if you could take this idea to the extreme, though. And do you think there is a world where you don't really test database-level concerns at all if you kind of believe this idea that it doesn't really matter what the state of it should be? JOËL: I guess there's a few different things on, like, what it matters about the state of it because you are asserting on its state sort of indirectly in a sort of higher level integration test. You're asserting that you see certain things show up on the screen in a system test. And maybe you want to say, "I do certain tasks, and then I expect to see three items in an unordered list." Those three items probably come from the database, although, you know, you could have it where they come from an API or something like that. So, the database is an implementation level. But if you had random data in your database, you might, in some tests, have four items in the list, some tests have five. And that's just going to be a flaky test, and that's going to be incredibly painful. So, while you're not asserting on the database, having control over it during sort of test setup, I think, does impact the way you assert. STEPHANIE: Yeah, that makes sense. I was suddenly just thinking about, like, how that exercise can actually tell you perhaps, like, when it is important to, in your test setup, be persisting real records as opposed to how much you can get away with, like, not interacting with it because, like, you aren't testing at that integration level. JOËL: That brings up a good point because a lot of tests probably you might need models, but you might not need persisted models to interact with them, if you're testing a method on a model that just does things based off its internal state and not any of the ActiveRecord database queries, or if you have some other service or something that consumes a model that doesn't necessarily need to query. There's a classic blog post on the thoughtbot blog about when you should not reuse. There's a classic blog post on the thoughtbot blog about when not to use FactoryBot. And, you know, we are the makers of FactoryBot. It helps set up records in your database for testing. And people love to use it all the time. And we wrote an article about why, in many cases, you don't need to create something into the database. All you need is just something in memory, and that's going to be much faster than using FactoryBot because talking to the database is expensive. STEPHANIE: Yeah, and I think we can see that in the shift from even, like, fixtures to factories as well, where test data was only persisted as needed and as needed in individual tests, rather than seeding it and having all of those records your entire test run. And it's cool to see that continuing, you know, that idea further of like, okay, now we have this new, popular tool that reduce some of that. But also, in most cases, we still don't need...it's still too much. JOËL: And from a performance perspective, it's a bit of a see-saw in that fixtures are a lot faster because they get inserted once at the beginning of your test run. So, a SQL execution at the beginning of a test run and then every test after that is just doing its thing: maybe creating a record inside of a transaction, maybe not creating any records at all. And so, it can be a lot faster as opposed to using FactoryBot where you're creating records one at a time. Every create call in a test is a round trip to the database, and those are expensive. So, FactoryBot tests tend to be more expensive than those that rely on fixtures. But you have the advantage of more control over what data is present and sort of more locality because you can see what has been created at the test level. But then, if you decide, hey, this is a test where I can just create records in memory, that's probably the best of all worlds in that you don't need anything created ahead with fixtures. You also don't need anything to be inserted using FactoryBot because you don't even need the database for this test. STEPHANIE: I'm curious, is that the assumption that you start with, that you don't need a persisted object when you're writing a basic unit test? JOËL: I think I will as much as possible try not to need to persist and only if necessary use persist records. There are strategies with FactoryBot that will allow you to also, like, build stubbed or just build in memory. So, there's a few different variations that will, like, partially do things for you. But oftentimes, you can just new up an object, and that's what I will often start with. In many cases, I will already know what I'm trying to do. And so, I might not go through the steps of, oh, new up an object. Oh no, I'm getting a I can't do the thing I need to do. Now, I need to write to the database. So, if I'm testing, let's say, an ActiveRecord scope that's filtering down a series of records, I know that's a wrapper around a database query. I'm not going to start by newing up some records and then sort of accidentally discovering, oh yeah, it does write to the database because that was pretty clear to me from the beginning. STEPHANIE: Yeah. Like, you have your mental shortcuts that you do. I guess I asked that question because I wonder if that is a good heuristic to share with maybe developers who are trying to figure out, like, should they create persisted records or, you know, use just regular instance in memory or, I don't know, even [laughs] use, like, a double [laughs]? JOËL: Yeah, I've done that quite a bit as well. I would say maybe my heuristic is, is the method under test going to need to talk to the database? And, you know, I may or may not know that upfront because if I'm test driving, I'm writing the test first. So, sometimes, maybe I don't know, and I'll start with something in memory and then realize, oh, you know, I do need to talk to the database for this. And this is for unit tests, in particular. For something more like an integration test or a system test that might require data in the database, system tests almost always do. You're not interacting with instances in memory when you're writing a system test, right? You're saying, "Given the database state is this when I visit this URL and do these things, this page reacts in such and such a way." So, system tests always write to the database to start with. So, maybe that's my heuristic there. But for unit tests, maybe think a little bit about does your method actually need to talk to the database? And maybe even almost give yourself a challenge. Can I get away with not talking to the database here? STEPHANIE: Yeah, I like that because I've certainly seen a lot of unit tests that are integration tests in disguise [laughs]. JOËL: Isn't that the truth? So, we kind of opened up this conversation with the idea of there are different ways to manage your database in terms of, do you clean or not clean before a test run? Where did you end up on this particular project? STEPHANIE: So, I ended up with a currently open PR to remove the need to truncate the database on each run of the test suite and just stick with the transaction for each example strategy. And I do think that this will work for us as long as we decide we don't want to introduce something like fixtures, even though that is actually also a discussion that's still in the works. But I'm hoping with this change, like, right now, I can help people start running faster tests [chuckles]. And should we ever introduce fixtures down the line, then we can revisit that. But it's one of those things that I think we've been living with this for too long [laughs]. And no one ever questioned, like, "Oh, why are we doing this?" Or, you know, maybe that was a need, however many years ago, that just got overlooked. And as a person new to the project, I saw it, and now I'm doing something about it [laughs]. JOËL: I love that new person energy on a project and like, "Hey, we've got this config thing. Did you know that we didn't need this as of Rails 6?" And they're like, "Oh, I didn't even realize that." And then you add that, and it just moves you into the future a little bit. So, if I understand the proposed change, then you're removing the truncation strategy, but you're still going to be in a situation where you have a clean database before each test because you're wrapping tests in transactions, which I think is the default Rails behavior. STEPHANIE: Yeah, that's where we're at right now. So, yeah, I'm not sure, like, how things came to be this way, but it seemed obvious to me that we were kind of doing this whole extra step that wasn't really necessary, at least at this point in time. Because, at least to my knowledge [laughs], there's no data being seeded in any other place. JOËL: It's interesting, right? When you have a situation where this was sort of a very popular practice for a long time, a lot of guides mentioned that. And so, even though Rails has made changes that mean that this is no longer necessary, there's still a long tail of apps that will still have this that may be upgraded later, and then didn't drop this, or maybe even new apps that got created but didn't quite realize that the guide they were following was outdated, or that a best practice that was in their head was also outdated. And so, you have a lot of apps that will still have these sort of, like, relics of the past. And you're like, "Oh yeah, that's how we used to do things." STEPHANIE: So yeah, thanks, Joël, for going on this journey with me in terms of, you know, reassessing my assumptions about test databases. I'm wondering, like, if this is common, how other people, you know, approach what they expect from the test database, whether it be totally clean or have, you know, any required data for common flows and use cases of your system. But it does seem that little in between of, like, maybe it is using transactions to reset for each example, but then there's also some persistence that's happening somewhere else that could be a little tricky to manage. JOËL: On that note, shall we wrap up? STEPHANIE: Let's wrap up. Show notes for this episode can be found at bikeshed.fm. JOËL: This show has been produced and edited by Mandy Moore. STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show. JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter. STEPHANIE: Or reach both of us at hosts@bikeshed.fm via email. JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week. ALL: Byeeeeeeeeeeee!!!!!! AD: Did you know thoughtbot has a referral program? If you introduce us to someone looking for a design or development partner, we will compensate you if they decide to work with us. More info on our website at: tbot.io/referral. Or you can email us at: referrals@thoughtbot.com with any questions.Support The Bike Shed
Mar 25
28 min
419: What's New in Your World? (Extended Edition)
Stephanie introduces her ideal setup for enjoying coffee on a bike ride. Joël describes his afternoon tea ritual. Exciting news from the hosts: both have been accepted to speak at RailsConf! Stephanie's presentation, titled "So, Writing Tests Feels Painful. What now?" aims to tackle the issues developers encounter with testing while offering actionable advice to ease these pains. Joël's session will focus on utilizing Turbo to create a Dungeons & Dragons character sheet, combining his passion for gaming with technical expertise. Their conversation shifts to artificial intelligence and its potential in code refactoring and other applications, such as enhancing the code review process and solving complex software development problems. Joël shares his venture into combinatorics, illustrating how this mathematical approach helped him efficiently refactor a database query by systematically exploring and testing all potential combinations of query segments. Transcript:  JOËL: Hello and welcome to another episode of The Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Joël Quenneville. STEPHANIE: And I'm Stephanie Minn, and together, we're here to share a bit of what we've learned along the way. JOËL: So, Stephanie, what's new in your world? STEPHANIE: So, today I went out for a coffee on my bike, and I feel like I finally have my perfect, like, on-the-go coffee setup. We have this thoughtbot branded travel mug. So, it's one of the little bits of swag that we got from the company. It's, like, perfectly leak-proof. I'll link the brand in the show notes. But it's perfectly leak-proof, which is great. And on my bike, I have a little stem bag, so it's just, like, a tiny kind of, like, cylindrical bag that sits on the, like, vertical part of my handlebars that connects to the rest of my bag. And it's just, like, the perfect size for a 12-ounce coffee. And so, I put my little travel mug in there, and I just had a very refreshing morning. And I'd gone out on my bike for a little bit, stopping by for coffee and headed home to work. And I got to drink my coffee during my first meeting. So, it was a wonderful way to start the day. JOËL: Do you just show up at the coffee shop with your refillable mug and say, "Hey, can you pour some coffee in this?" STEPHANIE: Yeah. I think a lot of coffee places are really amenable to bringing your own travel mugs. So yeah, it's really nice because I get to use less plastic. And also, you know, when you get a to-go mug, it is not leak-proof, right? It could just slosh all over the place and spill, so not bike-friendly. But yeah, bring your own mug. It's very easy. JOËL: Excellent. STEPHANIE: So, Joël, what's new in your world? JOËL: Also, warm beverages. Who would have thought? It's almost like it's cold in North America or something. I've been really enjoying making myself tea in the afternoons recently. And I've been drinking this brand of tea that is a little bit extra. Every flavor of tea they have comes with a description of how the tea feels. STEPHANIE: Ooh. JOËL: I don't know who came up with these, but they're kind of funny. So, one that I particularly enjoy is described as feels like stargazing on an empty beach. STEPHANIE: Wow. That's very specific. JOËL: They also give you tasting notes. This one has tastes of candied violet, elderberry, blackberry, and incense. STEPHANIE: Ooh, that sounds lovely. Are you drinking, like, herbal tea in the afternoon, or do you drink caffeinated tea? JOËL: I'll do caffeinated tea. I limit myself to one pot of coffee that I brew in the morning, and then, whenever that's done, I switch to tea. Tea I allow myself anything: herbal, black tea; that's fine. STEPHANIE: Yeah, I can't have too much caffeine in the afternoon either. But I do love an extra tea. I wish I could remember, like, what even was in this tea or what brand it was, but once I had a tea that was a purplish color. But then, when you squeeze some lemon in it, or I guess maybe anything with a bit of acid, it would turn blue. JOËL: Oh, that's so cool. STEPHANIE: Yeah, I'll have to find what this tea was [laughs] and update the podcast for any tea lovers out there. But yeah, it was just, like, a little bit of extra whimsy to your regular routine. JOËL: I love adding a little whimsy to my day, even if it's just seeing a random animated GIF that a coworker has sent or Tuple has some of the, like, reactions you can send if you're pairing with someone. And I don't use those very often, so whenever one of those comes through, and it's like, ship it or yay, that makes me very happy. STEPHANIE: Agreed. JOËL: This week is really fun because as we were prepping for this episode, we both realized that there is a lot that's been new in our world recently. And Stephanie, in particular, you've got some pretty big news that recently happened to you. STEPHANIE: Yeah, it turns out we're making the what's new in your world segment the entire episode today [laughs]. But my news is that I am speaking at RailsConf this year, so that is May 7th through 9th in Detroit. And so, yeah, I haven't spoken at a RailsConf before, only a RubyConf. So, I'm looking forward to it. My talk is called: So, Writing Tests Feels Painful. What now? JOËL: Wait, is writing tests ever painful [laughs]? STEPHANIE: Maybe not for you, but for the rest of us [laughs]. JOËL: No, it absolutely is. I, right before this recording, came from a pairing session where we were scratching our heads on an, like, awkward-to-write test. It happens to all of us. STEPHANIE: Yeah. So, I was brainstorming topics, and I kind of realized, especially with a lot of our consulting experience, you know, we hear from developers or even maybe, like, engineering managers a lot of themes around like, "Oh, like, development is slowing down because our test suite is such a headache," or "It's really slow. It's really flaky. It's really complicated." And that is a pain point that a lot of tech leaders are also looking to address for their teams. But I was really questioning this idea that, like, it always had to be some effort to improve the test suite, like, that had to be worked on at some later point or get, like, an initiative together to fix all of these problems, and that it couldn't just be baked into your normal development process, like, on an individual level. I do think it is really easy to feel a lot of pain when trying to write tests and then just be like, ugh, like, I wish someone would fix this, right? Or, you know, just kind of ignore the signals of that pain because you don't know, like, how to manage it yourself. So, my talk is about when you do feel that pain, really trying to determine if there's anything you can do, even in just, like, the one test file that you're working in to make things a little bit easier for yourself, so it doesn't become this, like, chronic issue that just gets worse and worse. Is there something you could do to maybe reorganize the file as you're working in it to make some conditionals a little bit clearer? Is there any, like, extra test setup that you're like, "Oh, actually, I don't need this anymore, and I can just start to get rid of it, not just for this one example, but for the rest in this file"? And do yourself a favor a little bit. So yeah, I'm excited to talk about that because I think that's perhaps, like, a skill that we don't focus enough on. JOËL: Are you going to sort of focus in on the side of things where, like, a classic TDD mantra is that test pain reflects underlying code complexity? So, are you planning to focus on the idea of, oh, if you're feeling test pain, maybe take some time to refactor some of the code that's under test, maybe because there's some tight coupling? Or are you going to lean a little bit more into maybe, like, the Boy Scout rule, you know, 'Leave the campsite cleaner than you found it' for your test files? STEPHANIE: Ooh, I like that framing. Definitely more of the former. But one thing I've also noticed working with a lot of client teams is that it's not always clear, like, how to refactor. I think a lot of intermediate developers start to feel that pain but don't know what to do about it. They don't know, like, maybe the code smells, or the patterns, or refactoring strategies, and that can certainly be taught. It will probably pull from that. But even if you don't know those skills yet, I'm wondering if there's, like, an opportunity to teach, like, developers at that level to start to reflect on the code and be like, "Hmm, what could I do to make this a little more flexible?" And they might not know the names of the strategies to, like, extract a class, but just start to get them thinking about it. And then maybe when they come across that vocabulary later, it'll connect a lot easier because they'll have started to think about, you know, their experiences day to day with some of the more conceptual stuff. JOËL: I really like that because I feel we've probably all heard that idea that test pain, especially when you're test driving, is a sign of maybe some anti-patterns or some code smells in the underlying code that you're testing. But translating that into something actionable and being able to say, "Okay, so my tests are painful. They're telling me something needs to be refactored. I'm looking at this code, and I don't know what to refactor." It's a big jump. It's almost the classic draw two circles; draw the rest of the owl meme. And so, I think bridging that gap is something that is really valuable for our community. STEPHANIE: Yeah, that's exactly what I hope to do in my talk. So, Joël, you [chuckles] also didn't quite mention that you have big news as well. JOËL: So, I also got accepted to speak at RailsConf. I'm giving a talk on Building a Dungeons & Dragons Character Sheet Using Turbo. STEPHANIE: That's really awesome. I'm excited because I want to learn more about Turbo. I want someone else to tell me [laughs] what I can do with it. And as a person with a little bit of Dungeons & Dragons experience, I think a character sheet is kind of the perfect vehicle for that. JOËL: Building a D&D character sheet has been kind of my go-to project to experiment with a new front-end framework because it's something that's pretty dynamic. And for those who don't know, there's a bunch of fields that you fill in with stats for different attributes that your character has, but then those impact other stats that get rendered. And sometimes there can be a chain two or three long where different numbers kind of combine together. And so, you've got this almost dependency tree of, like, a particular number. Maybe your skill at acrobatics might depend on a number that you entered in the dexterity field, but it also depends on your proficiency bonus, and maybe also depends on the race that you picked and a few other things. And so, calculating those numbers all of a sudden becomes not quite so simple. And so, I find it's a really fun exercise to build when trying out a new interactive front-end technology. STEPHANIE: Have you done this with a different implementation or a framework? JOËL: I've done this, not completely, but I've attempted some parts of a D&D character sheet, I think, with Backbone.js with Ember. I may have done an Angular one at some point in original Angular, so Angular 1. I did this with Elm. Somehow, I skipped React. I don't think I did React to build a D&D character sheet. And now I'm kind of moving a little bit back to the backend. How much can we get done just with Turbo? Or do we need to pull in maybe Stimulus? These are all things that are going to be really fun to demonstrate. STEPHANIE: Yeah. Speaking of injecting some whimsy earlier, I think it's kind of like just a little more fun than a regular to-do app, you know, or a blog to show how you can build, you know, something that people kind of understand with a different technology. JOËL: Another really fun thing that I've been toying with this week has been using AI to help me refactor code. And this has been using just sort of a classic chat AI, not a tool like Copilot. And I was dealing with a query that was really slow, and I wanted to restructure it in a different way. And I described to the AI how I wanted it to refactor and explicitly said, "I want this to be the same before and after." And I asked it to do the refactor, and it gave me some pretty disappointing results where it did some, like, a couple of really obvious things that were not that useful. And I was talking to a colleague about how I was really disappointed. I was thinking, well, AI should be able to do something better than this. And this colleague suggested changing the way I was asking for things and specifically asking for a step-by-step and asking it to prove every step using relational algebra, which is the branch of math that deals with everything that underlies relational databases, so the transformations that you would do where you keep everything the same, but you're saying, "Hey, these equations are all equivalent." And it sure did. It gave me a, like, 10-step process with all these, like, symbols and things. My relational algebra is not that strong, and so I couldn't totally follow along. But then I asked it to give me a code example, like, show me the SQL at every step of this transformation and at the end. And, you know, it all kind of looked all right. I've not fully tested the final result it gave me to see if it does what it says on the tin. But I'm cautiously optimistic. I think it looks very similar to something that I came up with on my own. And so, I'm somewhat impressed, at least, like, much better than things were in the beginning with that first round. So, I'm really curious to see where I can take this. STEPHANIE: Yeah, I think that's cool that you were able to prompt it differently and get something more useful. One of the reasons why I personally have been a little bit hesitant to get into the large language models is because I would love to see the AI show its work, essentially, like, tell me a little bit more about how it got from question to answer. And I thought that framing of kind of step-by-step show me code was a really interesting way, even to just, like, get some different results that do the same thing. But you can kind of evaluate that a little bit more on your own rather than just using that first result that it gave you that was like, eh, like, I don't know if this really did anything for me. So, it would be cool, even if you don't end up using, like, the final one, right? If something along the way also is an improvement from what you started with that would be really interesting. JOËL: Honestly, I think you kind of want the same thing if you're chatting with an AI chatbot or having a conversation in Slack with a colleague. They're just like, "Hey, can you help me refactor this?" And then a sort of, like, totally different chunk of code. And it's just like, "Trust me, it works." STEPHANIE: [laughs]. JOËL: And maybe it does. Maybe you plug it into your codebase and run the tests against it, and the tests are still green. And so, you trust that it works, but you don't really understand where it came from. That doesn't always feel good, even when it comes from a human. So, what I've appreciated with colleagues has been when they've given me a step-by-step. Sometimes, they give me the final product. They just say, "Hey. Try this. Does this work?" Plug it in to the test. It does pass. It's green. Great. "Tell me what black magic you did to get to that." And then they give me the step-by-step and it's like, oh, that's so good because not only do I get a better understanding of what happens at every step, but now I'm equipped the next time I run into this problem to apply the same technique to figure it out on my own. STEPHANIE: Yeah. And I liked, also, that relational algebra pro tip, right? It kind of ensures that what you're getting makes sense or is equivalent along the way [laughs]. JOËL: We think, right? I don't know enough relational algebra to check its work. It is quite possible that it is making some subtle mistakes along the way, or, like, making inferences that it shouldn't be. I'm not going to say I trust that. But I think, specifically, when asking for SQL transformations, prompting it to do so using relational algebra in a step-by-step way seemed to be a way to get it to do something more reliably or at least give more interesting results. STEPHANIE: Cool. JOËL: I was interested in trying this out in part because I've been more curious about AI tools recently, and also because we're hoping to do a deeper dive into AI on a Bike Shed episode at some point later, so very much still in the gathering information phase. But this was a really cool experience. So, having an AI refactor a query for me using relational algebra, definitely something that's new in my world this week. STEPHANIE: Speaking of refactoring and this idea of making improvements to your code and trying to figure out how to get from what you currently have to something new, I have been thinking a lot about how to make code reviews more actionable. And that's because, on my current client project, our team is struggling a little bit with code reviews, especially when you kind of want to give feedback on more of a design change in the code or thinking about some different abstractions. I have found that that is really hard to communicate async and also in a, like, a GitHub code review format where you can really just comment, like, line by line. And I've found that, you know, when someone is leaving feedback, that's like, "I'm having a hard time reading this. And I'm imagining that we could organize the code a bit differently in these three different layers or abstractions," there's a lot of assumptions there, right [laughs]? That your message is being communicated to the author and that they are able to, like, visualize, or have a mental model for what you're explaining as well. And then kind of what I've been seeing in this dynamic is, like, not really knowing what to do with that and to kind of just, like, I don't know where to go from here. So, I guess the next step is just to, like, merge it. Is that something you've experienced before or encountered when it comes to feedback? JOËL: Broader changes are often challenging to explain, especially when they're...sometimes you get so abstract you can just write a quick paragraph. And sometimes it's like, hey, what if we, like, totally change our approach? I've definitely done the thing where I'll just ping someone and say, "Hey, can we talk about this synchronously? Can we get on a call and have a deeper conversation?" How do you tend to approach if you're not going to hop on a call with someone and, like, have a 20 or 30-minute conversation? How do you approach doing that asynchronously on a pull request? Are you the type of person to put, like, a ton of, like, code blocks, like, "Here's what I was thinking. We could instead have this class and this thing"? And, like, pretty soon, it's, like, a page and a half of text. Or do you have another approach that you like to use? STEPHANIE: Yeah. And I think that's where it can get really interesting. Because my process is, I'll usually just start commenting and maybe if I'm seeing some things that can be done differently. If it's not just, like, a really obvious change that I could just use English to describe, I'll add a little suggested change. But I also don't want to just rewrite this person's code [laughs] in a code review. JOËL: That's the challenge, right? STEPHANIE: Yeah. And I've definitely seen that be done before, too. Once I notice I'm at, like, four plus comments, and then they're not just, like, nitpicks about, like, syntax or something like that, that helps me clue into the idea that there is some kind of bigger change that I might be asking of the author. And I don't want to overwhelm them with, like, individual comments that really are trying to convey something more holistic. JOËL: Right. I wonder if having a, like, specialized yet more abstract language is useful for these sorts of things where a whole paragraph in English or, you know, a ton of code examples might be a bit much. If you're able to say something like, "Hey, how would you feel about using a strategy pattern approach here instead of, you know, maybe a template object or some custom thing that we've built here?" that allows us to say a lot in a fairly sort of terse way. And it's the thing that you can leave more generically on the PR instead of, like, individually commenting in a bunch of places. And that can start a broader conversation at more of an architecture level. STEPHANIE: Yes, I really like that. That's a great idea. I would follow that up with, like, I think at the end of the day, there are some conversations that do need to be had synchronously. And so, I like the idea of leaving a comment like that and just kind of giving them resources to learn what a strategy pattern is and then offering support because that's also a way to shorten that feedback loop of trying to communicate an idea. And I like that it's kind of guiding them, but also you're there to add some scaffolding if it ends up being, like, kind of a big ask for them to figure out what to do. JOËL: There's also oftentimes, I think, a tone thing to manage where, especially if there's a difference in seniority or experience between the two people, it can be very easy for something to come across as an ask or a demand rather than a like, "Hey, let's think about some alternatives here." Or, like, "I have some concerns with your implementation. Let's sort of broadly explore some possible alternatives. Maybe a strategy pattern works." But the person reading that who wrote the original code might be, like, receiving that as "Your code is bad. You should have done a strategy pattern instead." And that's not the conversation I want to have, right? I want to have a back-and-forth about, "Hey, what are the trade-offs involved? Do you have a third architecture you'd like to suggest?" And so, that can be a really tricky thing to avoid. STEPHANIE: Yeah, I like that what you're saying also kind of suggested that it's okay if you don't have an idea yet for exactly how it should look like. Maybe you just are like, oh, like, I'm having a hard time understanding this, but I don't think just leaving it at that gives the author a lot to go on. I think there's something to it about maybe the action part of actionable is just like, "Can you talk about it with me?" Or "Could you explain what you're trying to do here?" Or, you know, leave a comment about what this method is doing. There's a lot of ways, I think, that you can reach some amount of improvement, even if it doesn't end up being, like, the ideal code that you would write. JOËL: Yes. There's also maybe a distinction in making it actionable by giving someone some code and saying, "Hey, you should copy-paste this code and make that..." or, you know, use a GitHub suggested code or something, which works on the small. And in the big, you can give some maybe examples and say, "Hey, what if you refactored in this way?" But sometimes, you could even step back and let them do that work and say, "Hey, I have some concerns with the current architecture. It's not flexible in the ways that we need to be flexible. Here's my understanding of the requirements. And here's sort of how I see maybe this architecture not working with that. Let's think of some different ways we could approach this problem." And oftentimes, it's nice to give at least one or two different ideas to help start that. But it can be okay to just ask the person, "Hey, can you come up with some alternate implementations that would fulfill these sets of requirements?" STEPHANIE: Yeah, I like that. And I can even see, like, maybe you do that work, and you don't end up pursuing it completely in addressing that feedback. But even asking someone to do the exercise itself, I think, can then spark new ideas and maybe other improvements. In general, I like to think about...I'm a little hesitant to use this metaphor because I'm not actually giving code, like, letter grades when I review them, but the idea that, like, not all code has to get, like, an A [chuckles], but maybe getting it, like, from one letter grade up to, like, half a letter grade, like, higher, that is valuable, even if it's not always practical to go through multiple rounds of code review. And I think just making it actionable enough to be a little bit better, like, that is, in my opinion, the sweet spot. JOËL: That's true. The sort of over-giving feedback to someone to try to get code perfect, rather than just saying, "Hey, can we make it slightly better?" And, you know, there are probably some minimum standards you need to hit. But at some point, it's a trade-off of like, how much time do we need to put polishing this versus shipping something? STEPHANIE: Yeah, and I think that it is cumulative over time, right? That's how people learn. Yeah, it's like one of the biggest opportunities for developers to level up is from that feedback. And that's why I think it's important that it's actionable because, you know, and you put the time into, like, giving that review, and it's not just to make sure the code works, but it's also, like, one of the touch points for collaboration. JOËL: So, if you had to summarize what makes code review comments actionable, do you have, like, top three tips that make a comment really actionable as opposed to something that's not helpful? Or maybe that's more of the journey that you're on, and you've not distilled it down to three pithy tips that you can put in a listicle. STEPHANIE: Honestly, I think it does kind of just distill down to one, which is for every comment, you should have an idea of what you would like the author to do about it. And it's okay if it's nothing, but then tell them that it's nothing. You could just be expressing, "I thought this was kind of weird, [laughs]" or "This is not my favorite thing, but it's okay." JOËL: And it can be okay for the thing you want the author to do. It doesn't have to be code. It could be a conversation. STEPHANIE: Yeah, exactly. It could be a conversation. It could be asking for information, too, right? Like, "Did you consider alternatives, and could you share them with me?" But that request portion, I think is really important because, yeah, I think there's so much miscommunication that can happen along the way. So, definitely still trying to figure out how to best support that kind of code review culture on my team. JOËL: This week's episode has been really fun because it's just been a combination of a lot of things that are new in our world, things that we've been trying, things that we've been learning. And kind of in an almost, like, a meta sense, one of the things I've been digging into is combinatorics, the branch of math that looks at how things combine and particularly how it works with combining a bunch of ActiveRecord query fragments where there's potential branching, so things like doing a union of two sort of sub queries or doing an or where you're combining two different where queries and trying to figure out what are the different paths through that. STEPHANIE: Wow, what a great way to combine what we were talking about, Joël [laughs]. Did you apply combinatorics to this podcast episode [laughs]? JOËL: Somehow, topics multiply with each other, something, something. STEPHANIE: Yeah, that makes sense to me [laughs]. Okay. Will you tell me more about what you've been using it for in your queries? JOËL: So, one thing I'm trying to do is because I've got these different branching paths through a query, I want to see sort of all the different ways because these are defined as ActiveRecord scopes, and I'm chaining them together. And it looks linear because I'm calling scope1 dot scope2 dot scope3. But each of those have branches inside of them. And so, there's all these different ways that data could get used or not. And one way that I figured out, like, what are the different paths here, was actually drawing out a matrix, just putting together a table. In this case, I had two scopes, each of which had a two-way branch inside, and so I made a two by two matrix. And that gave me all of the combinations of, oh, if you go down one branch in one scope and down another branch in the other scope. And what I went through is then I went in in each square and filled in how many records I would expect to get back from the query from some basic set that I was working on in each of these combinations. And one thing that was really interesting is that some of those combinations were sort of mutually exclusive, where a scope further down the line was filtering on the same field as an earlier one and would overwrite it or not overwrite it, but the two would then sort of you can't have both of those things be true at the same time. So, I'm looking for something that has a particular manager ID, and then I'm looking for something that has a particular different manager ID. And the way Rails combines these, if you just change scopes with where, is to and them together. There are no records that have both manager ID 1 and manager ID 2. You can only have one manager ID. And so, as I'm filling out my matrix, there's some sections I can just zero out and be like, wait, this will always return zero record. And then I can start focusing on the parts that are not zeroed out. So, I've got two or three squares. What's special about those? And that helped me really understand what the combination of these multiple query fragments together were actually trying to do as a holistic whole. STEPHANIE: Wow, yeah, that is really interesting because I hear you when you say it looks linear. And it would be really surprising to me for there to be branching paths. Like, that's not really what I think about when I think about SQL. But that makes a lot of sense that it could get so complicated that it's just impossible to get a certain kind of result. Like, what's going to be the outcome of applying combinatorics to this? Is there a refactoring opportunity, or is it really just to even understand what's going on? JOËL: So, this was a refactoring that I was trying to do, but I didn't really understand the underlying behavior of the chain of scopes. I just knew that they were doing some complex things that were inefficient from a SQL perspective. And so, I was looking at ways to refactor, but I also wanted to get a sense of what is this actually trying to do other than just chaining a bunch of random bits of code together? So, the matrix really helped for that. The other way that I used it was to write some tests because this query I was trying to refactor, this chain of scopes, was untested. And I wanted to write tests that were very thorough because I wanted to make sure that my refactor didn't break any edge cases. And I'm, you know, writing a few tests. Okay, well, here's a record that I definitely want to get returned by this query, and maybe here are a couple of records I don't want to get returned. And the more I was, like, going into this and trying to write test cases, the more I was finding more edge cases that I didn't want to and, oh, but what about this? And what about the combination of these things? And it got to the point where it was just messing with my mind. I was, like, confusing myself and really struggling to write tests that would do anything useful. STEPHANIE: Wow. Yeah. Honestly, I have already started to become a little bit suspicious of complex scopes, and this further pushes me in that direction [laughs] because yeah, once you start to...like, the benefit of them is that you can chain them, but it really hides a lot of the underlying behavior. So, you can easily just turn yourself around or, like, go, you know, kind of end up [laughs] in a little bit of a bind. JOËL: Definitely, especially once it grows a little bit harder to hold in your head. And I don't know exactly where that level is for me. But in this particular situation, I identified, I think, five different dimensions that would impact the results of this query. And then each dimension had maybe three or four different values that we might care about. And, eventually, I just took the time to write this out. So, I created five arrays and then just said, "Hey, here are the different managers that we care about. Here are the different project types we care about. Here are the different..." and we had, like, five of these, and each array had three or four elements in it. And then, in a series of nested loops, I iterated through all of these arrays and at the innermost loop, created the data that I wanted that matched that particular set of values. Now, we're often told you should not be doing things in nested loops because you end up sort of multiplying all of these together, but, in this case, this is actually what I wanted to do. You know, it turns out that I had a hundred-ish records I had to create to sort of create a data set that would be all the possible edge cases I might want to filter on. And creating them all by hand with all of the different variations was going to be too much. And so, I ended up doing this with arrays and nested loops. And it got me the data that I needed. And it gave me then the confidence to know that my refactor did indeed work the way I was expecting. STEPHANIE: Wow. That's truly hero's work [chuckles]. I'm, like, very excited because it sounds like that's a huge opportunity for some performance improvements as well. JOËL: For the underlying code, yes. The test might be a little bit slow because I'm creating a hundred records in the database. And you might say, "Oh, do you really need to do that? Can you maybe collapse some of these cases?" In this particular case, I really wanted to have high confidence that the refactor was not changing anything. And so, I was okay creating a hundred records over a series of nested iterations. That was a price I was willing to pay. The refactored query, it turns out, I was able to write it in a way that was significantly faster. STEPHANIE: Yeah, that's what I suspected. JOËL: So, I had to rewrite it in a way that didn't take advantage of all the change scopes. I had to just sort of write something custom from scratch, which is often the case, right? Performance and reusability sometimes fight against each other, and it's a trade-off. So, I'm not reusing the scopes. I had to write something from scratch, but it's multiple hundreds of times faster. STEPHANIE: Wow. Yeah. That seems worth it for a slow test [laughs] for the user experience to be a lot better, especially when you just reach that level of complexity. And it's a really awesome strategy that you applied to figure that out. I think it's a very unique one [laughs]. That's for sure. JOËL: I've had an interest in sort of analytical tools to help me understand domain models, to help understand problems, to help understand code that I'm working with for a while now, and I think an understanding of combinatorics fits into that. And then, particular tools within that, such as drawing things out in a table, in a two by two matrix, or an end-by-end matrix to get something visual, that's a great tool for debugging or understanding a problem. Thinking of problems as data that exists in multiple dimensions and then asking about the cardinality of that set it's the kind of analysis I did a lot when I was modeling using algebraic data types in Elm. But now I've sort of taken some of the tools and analysis I use from that world into thinking about things like SQL records, things like dealing with data in Ruby. And I'm able to bring those tools and that way of thinking to help me solve some problems that I might struggle to solve otherwise. For any of our listeners who this, like, kind of piques their interest, combinatorics falls under a broader umbrella of mathematics called discrete math. And within that, there's a lot that I think is really useful, a lot of tools and techniques that we can apply to our day-to-day programming. We have a Bike Shed episode where we talked about is discrete math relevant to day-to-day programmers and what are the ways it's so? We'll link that in the show notes. I also gave a talk at RailsConf last year diving into that titled: The Math Every Programmer Needs. So, if you're looking for something that's accessible to someone who's not done a math degree, those are two great jumping-off points. STEPHANIE: Yeah. And then, maybe you'll start drawing out arrays and applying combinatorics to figure out your performance problems. JOËL: On that note, shall we wrap up? STEPHANIE: Let's wrap up. Show notes for this episode can be found at bikeshed.fm. JOËL: This show has been produced and edited by Mandy Moore. STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show. JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter. STEPHANIE: Or reach both of us at hosts@bikeshed.fm via email. JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week. ALL: Byeeeeeeeeee!!!!!!!! AD: Did you know thoughtbot has a referral program? If you introduce us to someone looking for a design or development partner, we will compensate you if they decide to work with us. More info on our website at: tbot.io/referral. Or you can email us at: referrals@thoughtbot.com with any questions.Support The Bike Shed
Mar 18
37 min
418: Mental Models For Reduce Functions
Joël talks about his difficulties optimizing queries in ActiveRecord, especially with complex scopes and unions, resulting in slow queries. He emphasizes the importance of optimizing subqueries in unions to boost performance despite challenges such as query duplication and difficulty reusing scopes. Stephanie discusses upgrading a client's app to Rails 7, highlighting the importance of patience, detailed attention, and the benefits of collaborative work with a fellow developer. The conversation shifts to Ruby's reduce method (inject), exploring its complexity and various mental models to understand it. Joël and Stephanie discuss when it's preferable to use reduce over other methods like each, map, or loops and the importance of understanding the underlying operation you wish to apply to two elements before scaling up with reduce. The episode also touches on monoids and how they relate to reduce, suggesting that a deep understanding of functional programming concepts can help simplify reduce expressions. Rails 7 EXPLAIN ANALYZE Blocks, symbol to proc, and symbols arguments for reduce Ruby tally Performance considerations for reduce in JavaScript Persistant data structures Avoid passing a block to map and reduce Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire monoids iteration anti-patterns Joël’s talk on “constructor replacement” Transcript:  STEPHANIE: Hello and welcome to another episode of The Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Stephanie Minn. JOËL: And I'm Joël Quenneville. And together, we're here to share a bit of what we've learned along the way. STEPHANIE: So, Joël, what's new in your world? JOËL: I've been doing a bunch of fiddling with query optimization this week, and I've sort of run across an interesting...but maybe it's more of an interesting realization because it's interesting in the sort of annoying way. And that is that, using ActiveRecord scopes with certain more complex query pieces, particularly unions, can lead to queries that are really slow, and you have to rewrite them differently in a way that's not reusable in order to make them fast. In particular, if you have sort of two other scopes that involve joins and then you combine them using a union, you're unioning two sort of joins. Later on, you want to change some other scope that does some wares or something like that. That can end up being really expensive, particularly if some of the underlying tables being joined are huge. Because your database, in my case, Postgres, will pull a lot of this data into the giant sort of in-memory table as it's, like, building all these things together and to filter them out. And it doesn't have the ability to optimize the way it would on a more traditional relation. A solution to this is to make sure that the sort of subqueries that are getting unioned are optimized individually. And that can mean moving conditions that are outside the union inside. So, if I'm chaining, I don't know, where active is true on the outer query; on the union itself, I might need to move that inside each of the subqueries. So, now, in the two or three subqueries that I'm unioning, each of them needs to have a 'where active true' chained on it. STEPHANIE: Interesting. I have heard this about using ActiveRecord scopes before, that if the scopes are quite complex, chaining them might not lead to the most performant query. That is interesting. By optimizing the subqueries, did you kind of change the meaning of them? Was that something that ended up happening? JOËL: So, the annoying thing is that I have a scope that has the union in it, and it does some things sort of on its own. And it's used in some places. There are also other places that will try to take that scope that has the union on it, chain some other scopes that do other joins and some more filters, and that is horribly inefficient. So, I need to sort of rewrite the sort of subqueries that get union to include all these new conditions that only happen in this one use case and not in the, like, three or four others that rely on that union. So, now I end up with some, like, awkward query duplication in different call sites that I'm not super comfortable about, but, unfortunately, I've not found a good way to make this sort of nicely reusable. Because when you want to chain sort of more things onto the union, you need to shove them in, and there's no clean way of doing that. STEPHANIE: Yeah. I think another way I've seen this resolved is just writing it in SQL if it's really complex and it becoming just a bespoke query. We're no longer trying to use the scope that could be reusable. JOËL: Right. Right. In this case, I guess, I'm, like, halfway in between in that I'm using the ActiveRecord DSL, but I am not reusing scopes and things. So, I sort of have the, I don't know, naive union implementation that can be fine in all of the simpler use cases that are using it. And then the query that tries to combine the union with some other fancy stuff it just gets its own separate implementation different than the others that it has optimized. So, there are sort of two separate paths, two separate implementations. I did not drop down to writing raw SQL because I could use the ActiveRecord DSL. So, that's what I've been working with. What's new in your world this week? STEPHANIE: So, a couple of weeks ago, I think, I mentioned that I was working on a Rails 7 upgrade, and we have gotten it out the door. So, now the client application I'm working on is on Rails 7, which is exciting for the team. But in an effort to make the upgrade as incremental as possible, we did, like, back out of a few of the new application config changes that would have led us down a path of more work. And now we're kind of following up a little bit to try to turn some of those configs on to enable them. And it was very exciting to kind of, like, officially be on Rails 7. But I do feel like we tried to go for, like, the minimal amount of work possible in that initial big change. And now we're having to kind of backfill a little bit on some of the work that was a little bit more like, oh, I'm not really sure, like, how big this will end up being. And it's been really interesting work, I think, because it requires, like, two different mindsets. Like, one of them is being really patient and focused on tedious work. Like, okay, what happens when we enable this config option? Like, what changes? What errors do we see? And then having to turn it back off and then go in and fix them. But then another, I think, like, headspace that we have to be in is making decisions about what to do when we come to a crossroads around, like, okay, now that we are starting to see all the changes that are coming about from enabling this config, is this even what we want to do? And it can be really hard to switch between those two modes of thinking. JOËL: Yeah. How do you try to balance between the two? STEPHANIE: So, I luckily have been pairing with another dev, and I've actually found that to be really effective because he has, I guess, just, like, a little bit more of that patience to do the more tedious, mundane [laughs] aspects of, like, driving the code changes. And I have been riding along. But then I can sense, like, once he gets to the point of like, "Oh, I'm not sure if we should keep going down this road," I can step in a little bit more and be like, "Okay, like, you know, I've seen us do this, like, five times now, and maybe we don't want to do that." Or maybe being like, "Okay, we don't have a really clear answer, but, like, who can we talk to to find out a little bit more or get their input?" And that's been working really well for me because I've not had a lot of energy to do more of that, like, more manual or tedious labor [chuckles] that comes with working on that low level of stuff. So yeah, I've just been pleasantly surprised by how well we are aligning our superpowers. JOËL: To use some classic business speech, how does it feel to be in the future on Rails 7? STEPHANIE: Well, we're not quite up, you know, up to modern days yet, but it does feel like we're getting close. And, like, I think now we're starting to entertain the idea of, like, hmm, like, could we be even on main? I don't think it's really going to happen, but it feels a little bit more possible. And, in general, like, the team thinks that that could be, like, really exciting. Or it's easier, I think, once you're a little bit more on top of it. Like, the worst is when you get quite behind, and you end up just feeling like you're constantly playing catch up. It just feels a little bit more manageable now, which is good. JOËL: I learned this week a fun fact about Rails 7.1, in particular, which is that the analyze method on ActiveRecord queries, which allowed you to sort of get SQL EXPLAIN statements, now has the ability to pass in a couple of extra parameters. So, there are symbols, and you can pass in things like analyze or verbose, which allows you to get sort of more data out of your EXPLAIN query, which can be quite nice when you're debugging for performance. So, if you're in the future and you're on Rails 7.1 and you want sort of the in-depth query plans, you don't need to copy the SQL into a Postgres console to get access to the sort of fully developed EXPLAIN plan. You can now do it by passing arguments to EXPLAIN, which I'm very happy for. STEPHANIE: That's really nice. JOËL: So, we've mentioned before that we have a developers' channel on Slack here at thoughtbot, and there's all sorts of fun conversations that happen there. And there was one recently that really got me interested, where people were talking about Ruby's reduce method, also known as inject. And it's one of those methods that's kind of complicated, or it can be really confusing. And there was a whole thread where people were talking about different mental models that they had around the reduce method and how they sort of understand the way it works. And I'd be curious to sort of dig into each other's mental models of that today. To kick us off, like, how comfortable do you feel with Ruby's reduce method? And do you have any mental models to kind of hold it in your head? STEPHANIE: Yeah, I think reduce is so hard to wrap your head around, or it might be one of the most difficult, I guess, like, functions a new developer encounters, you know, in trying to understand the tools available to them. I always have to look up the order of the arguments [laughs] for reduce. JOËL: Every time. STEPHANIE: Yep. But I feel like I finally have a more intuitive sense of when to use it. And my mental model for it is collapsing a collection into one value, and, actually, that's why I prefer calling it reduce rather than the inject alias because reduce kind of signals to me this idea of going from many things to one canonical thing, I suppose. JOËL: Yeah, that's a very common use case for reducing, and I guess the name itself, reducing, kind of has almost that connotation. You're taking many things, and you're going to reduce that down to a single thing. STEPHANIE: What was really interesting to me about that conversation was that some people kind of had the opposite mental model where it made a bit more sense for them to think about injecting and, specifically, like, the idea of the accumulator being injected with values, I suppose. And I kind of realized that, in some ways, they're kind of antonyms [chuckles] a little bit because if you're focused on the accumulator, you're kind of thinking about something getting bigger. And that kind of blew my mind a little bit when I realized that, in some ways, they can be considered opposites. JOËL: That's really fascinating. It is really interesting, I think, the way that we can take the name of a method and then almost, like, tell ourselves a story about what it does that then becomes our way of remembering how this method works. And the story we tell for the same method name, or in this case, maybe there's a few different method names that are aliases, can be different from person to person. I know I tend to think of inject less in terms of injecting things into the accumulator and more in terms of injecting some kind of operator between every item in the collection. So, if we have an array of numbers and we're injecting plus, in my mind, I'm like, oh yeah, in between each of the numbers in the collection, just inject a little plus sign, and then do the math. We're summing all the items in the collection. STEPHANIE: Does that still hold up when the operator becomes a little more complex than just, you know, like, a mathematical operator, like, say, a function? JOËL: Well, when you start passing a block and doing custom logic, no, that mental model kind of falls apart. In order for it to work, it also has to be something that you can visualize as some form of infix operator, something that goes between two values rather than, like, a method name, which is typically in prefix position. I do want to get at this idea, though: the difference between sort of the block version versus passing. There are ways where you can just do a symbol, and that will call a method on each of the items. Because I have a bit of a hot take when it comes to writing reduce blocks or inject blocks that are more accessible, easier to understand. And that is, generally, that you shouldn't, or more specifically, you should not have a big block body. In general, you should be either using the symbol version or just calling a method within the block, and it's a one-liner. Which means that if you have some complex behavior, you need to find a way to move that out of this sort of collection operation and into instance methods on the objects being iterated. STEPHANIE: Hmm, interesting. By one-liner do you mean passing the name of the method as a proc or actually, like, having your block that then calls the method? Because I can see it becoming even simpler if you have already extracted a method. JOËL: Yeah, if you can do symbol to proc, that's amazing, or even if you can use just the straight-up symbol way of invoking reduce or inject. That typically means you have to start thinking about the types of objects that you are working with and what methods can be moved onto them. And sometimes, if you're working with hashes or something like that that don't have domain methods for what you want, that gets really awkward. And so, then maybe that becomes maybe a hint that you've got some primitive obsession happening and that this hash that sort of wants a domain object or some kind of domain method probably should be extracted to its own object. STEPHANIE: I'll do you with another kind of spicy take. I think, in that case, maybe you don't want a reduce at all. If you're starting to find that...well, okay, I think it maybe could depend because there could be some very, like, domain-specific logic. But I have seen reduce end up being used to transform the structure of the initial collection when either a different higher-order function can be used or, I don't know, maybe you're just better off writing it with a regular loop [laughs]. It could be clearer that way. JOËL: Well, that's really interesting because...so, you mentioned the idea that we could use a different higher-order function, and, you know, higher-order function is that fancy term, just a method that accepts another method as an argument. In Ruby, that just means your method accepts a block. Reduce can be used to implement pretty much the entirety of enumerable. Under the hood, enumerable is built in terms of each. You could implement it in terms of reduce. So, sometimes it's easy to re-implement one of the enumerable methods yourself, accidentally, using reduce. So, you've written this, like, complex reduce block, and then somebody in review comes and looks at it and is like, "Hey, you realize that's just map. You've just recreated map. What if we used map here?" STEPHANIE: Yeah. Another one I've seen a lot in JavaScript land where there are, you know, fewer utility functions is what we now have in Ruby, tally. I feel like that was a common one I would see a lot when you're trying to count instances of something, and I've seen it done with reduce. I've seen it done with a for each. And, you know, I'm sure there are libraries that actually provide a tally-like function for you in JS. But I guess that actually makes me feel even more strongly about this idea that reduce is best used for collapsing something as opposed to just, like, transforming a data structure into something else. JOËL: There's an interesting other mental model for reduce that I think is hiding under what we're talking about here, and that is the idea that it is a sort of mid-level abstraction for dealing with collections, as opposed to something like map or select or some of those other enumerable helpers because those can all be implemented in terms of reduce. And so, in many cases, you don't need to write the reduce because the library maintainer has already used reduce or something equivalent to build these higher-level helpers for you. STEPHANIE: Yeah, it's kind of in that weird point between, like, very powerful [chuckles] so that people can start to do some funky things with it, but also sometimes just necessary because it can feel a little bit more concise that way. JOËL: I've done a fair amount of functional programming in languages like Elm. And there, if you're building a custom data structure, the sort of lowest-level way you have of looping is doing a recursion, and recursions are messy. And so, what you can do instead as a library developer is say, "You know what, I don't want to be writing recursions for all of these." I don't know; maybe I'm building a tree library. I don't want to write a recursion for every different function that goes over trees if I want to map or filter or whatever. I'm going to write reduce using recursion, and then everything else can be written in terms of reduce. And then, if people want to do custom things, they don't need to recurse over my tree. They can use this reduce function, which allows them to do most of the traversals they want on the tree without needing to touch manual recursion. So, there's almost, like, a low-level, mid-level, high-level in the library design, where, like, lowest level is recursion. Ideally, nobody touches that. Mid-level, you've got reducing that's built out on top of recursion. And then, on top of that, you've got all sorts of other helpers, like mapping, like filtering, things like that. STEPHANIE: Hmm. I'm wondering, do you know of any performance considerations when it comes to using reduce built off a recursion? JOËL: So, one of the things that can be really nice is that writing a recursion yourself is dangerous. It's so easy to, like, accidentally introduce Stack Overflow. You could also write a really inefficient one. So, ideally, what you do is that you write a reduce that is safe and that is fast. And then, everybody else can just use that to not have to worry about the sort of mechanics of traversing the collection. And then, just use this. It already has all of the safety and speed features built in. You do have to be careful, though, because reduce, by nature, traverses the entire collection. And if you want to break out early of something expensive, then reduce might not be the tool for you. STEPHANIE: I was also reading a little bit about how, in JavaScript, a lot of developers like to stick to that idea of a pure function and try to basically copy the entire accumulator for every iteration and creating a new object for that. And that has led to some memory issues as well. As opposed to just mutating the accumulator, having, especially when you, you know, are going through a collection, like, really large, making that copy every single time and creating, yeah [chuckles], just a lot of issues that way. So, that's kind of what prompted that question. JOËL: Yeah, that can vary a lot by language and by data structure. In more functional languages that try to not mutate, they often have this idea of what they call persistent data structures, where you can sort of create copies that have small modifications that don't force you to copy the whole object under the hood. They're just, like, pointers. So, like, hey, we, like, are the same as this other object, but with this extra element added, or something like that. So, if you're growing an array or something like that, you don't end up with 10,000 copies of the array with, like, a new element every time. STEPHANIE: Yeah, that is interesting. And I feel like trying to adopt different paradigms for different tools, you know, is not always as straightforward as some wish it were [laughs]. JOËL: I do want to give a shout-out to an academic paper that is...it is infamously dense. The title of it is Functional Programming with Bananas, Lenses, and Barbed Wire. STEPHANIE: It doesn't sound dense; it sounds fun. Well, I don't about barbed wire. JOËL: It sounds fun, right? STEPHANIE: Yeah, but certainly quirky [laughs]. JOËL: It is incredibly dense. And they've, like, created this custom math notation and all this stuff. But the idea that they pioneered there is really cool, this idea that kind of like I was talking about sort of building libraries in different levels. Their idea is that recursion is generally something that's unsafe and that library and language designers should take care of all of the recursion and instead provide some of these sort of mid-level helper methods to do things. Reducing is one of them, but their proposal is that it's not the only one. There's a whole sort of family of similar methods that are there that would be useful in different use cases. So, reduce allows you to sort of traverse the whole thing. It does not allow you to break out early. It does not allow you to keep sort of track of a sort of extra context element if you want to, like, be traversing a collection but have a sort of look forward, look back, something like that. So, there are other variations that could handle those. There are variations that are the opposite of reduce, where you're, like, inflating, starting from a few parameters and building a collection out of them. So, this whole concept is called recursion schemes, and you can get, like, really deep into some theory there. You'll hear fancy words like catamorphisms and anamorphisms. There's a whole world to explore in that area. But at its core, it's this idea that you can sort of slice up things into this sort of low-level recursion, mid-level helpers, and then, like, kind of userland helpers built on top of that. STEPHANIE: Wow. That is very intense; it sounds like [chuckles]. I'm happy not to ever have to write a recursion ever again, probably [laughs]. Have you ever, as just a web developer in your day-to-day programming, found a really good use case for dropping down to that level? Or are you kind of convinced that, like, you won't really ever need to? JOËL: I think it depends on the paradigm of the language you're working in. In Ruby, I've very rarely needed to write a recursion. In something like Elm, I've had to do that, eh, not infrequently. Again, it depends, like, if I'm doing more library-esque code versus more application code. If I'm writing application code and I'm using an existing, let's say, tree library, then I typically don't need to write a recursion because they've already written traversals for me. If I'm making my own and I have made my own tree libraries, then yes, I'm writing recursions myself and building those traversals so that other people don't have to. STEPHANIE: Yeah, that makes sense. I'd much rather someone who has read that paper [laughs] write some traversal methods for me. JOËL: And, you know, for those who are curious about it, we will put a link to this paper in the description. So, we've talked about a sort of very academic mental model way of thinking about reducing. I want to shift gears and talk about one that I have found is incredibly practical, and that is the idea that reduce is a way to scale an operation that works on two objects to an operation that works on sort of an unlimited number of objects. To make it more concrete, take something like addition. I can add two numbers. The plus operator allows me to take one number, add another, get a sum. But what if I want to not just add two numbers? I want to add an arbitrary number of numbers together. Reduce allows me to take that plus operator and then just scale it up to as many numbers as I want. I can just plug that into, you know, I have an array of numbers, and I just call dot reduce plus operator, and, boom, it can now scale to as many numbers as I want, and I can sum the whole thing. STEPHANIE: That dovetails quite nicely with your take earlier about how you shouldn't pass a block to reduce. You should extract that into a method. Don't you think? JOËL: I think it does, yes. And then maybe it's, like, sort of two sides of a coin because I think what this leads to is an approach that I really like for reducing because sometimes, you know, here, I'm starting with addition. I'm like, oh, I have addition. Now, I want to scale it up. How do I do that? I can use reduce. Oftentimes, I'm faced with sort of the opposite problem. I'm like, oh, I need to add all these numbers together. How do I do that? I'm like, probably with a reduce. But then I start writing the block, and, like, I get way too into my head about the accumulator and what's going to happen. So, my strategy for writing reduce expressions is to, instead of trying to figure out how to, like, do the whole thing together, first ask myself, how do I want to combine any two elements that are in the array? So, I've got an array of numbers, and I want to sum them all. What is the thing I need to do to combine just two of those? Forget the array. Figure that out. And then, once I have that figured out, maybe it's an existing method like plus. Maybe it's a method I need to define on it if it's a custom object. Maybe it's a method that I write somewhere. Then, once I have that, I can say, okay, I can do it for two items. Now, I'm going to scale it up to work for the whole array, and I can plug it into reduce. And, at that point, the work is already basically done, so I don't end up with a really complex block. I don't end up, like, almost ending in, like, a recursive infinite loop in my head because I do that. STEPHANIE: [laughs]. JOËL: So, that approach of saying, start by figuring out what is the operation you want to do to combine two elements, and then use reduce as a way to scale that to your whole array is a way that I've used to keep things simple in my mind. STEPHANIE: Yeah, I like that a lot as a supplement to the model I shared earlier because, for me, when I think about reducing as, like, collapsing into a value, you kind of are just like, well, okay, I start with the collection, and then somehow I get to my single value. But the challenge is figuring out how that happens [laughs], like, the magic that happens in between that. And I think another alias that we haven't mentioned yet for reduce that is used in a lot of other languages is fold. And I actually like that one a lot, and I think it relates to your mental model. Because when I think about folding, I'm picturing folding up a paper like an accordion. And you have to figure out, like, what is the first fold that I can make? And just repeating that over and over to get to your little stack of accordion paper [laughs]. And if you can figure out just that first step, then you pretty much, like, have the recipe for getting from your initial input to, like, your desired output. JOËL: Yeah. I think fold is interesting in that some languages will make a distinction between fold and reduce. They will have both. And typically, fold will require you to pass an initial value, like a starting accumulator, to start it off. Whereas reduce will sort of assume that your array can use the first element of the array as the first accumulator. STEPHANIE: Oh, I just came up with another visual metaphor for this, which is, like, folding butter into croissant pastry when the butter is your initial value [laughs]. JOËL: And then the crust is, I guess, the elements in the array. STEPHANIE: Yeah. Yeah. And then you get a croissant out of it [laughs]. Don't ask me how it gets to a perfectly baked, flaky, beautiful croissant, but somehow that happens [laughs]. JOËL: So, there's an interesting sort of subtlety here that I think happens because there are sort of two slightly different ways that you can interact with a reduce. Sometimes, your accumulator is of the same type as the elements in your array. So, you're summing an array of numbers, and your accumulator is the sum, but each of the elements in the array are also numbers. So, it's numbers all the way through. And sometimes, your accumulator has a different type than the items in the array. So, maybe you have an array of words, and you want to get the sum of all of the characters and all the words. And so, now your accumulator is a number, but each of the items in the array are strings. STEPHANIE: Yeah, that's an interesting distinction because I think that's where you start to see the complex blocks being passed and reduced. JOËL: The complex blocks, definitely; I think they tend to show up when your accumulator has a different type than the individual items. So, maybe that's, like, a slightly more complicated use case. Oftentimes, too, the accumulator ends up being some, like, more complex, like, hash or something that maybe would really benefit from being a custom object. STEPHANIE: I've never done that before, but I can see why that would be really useful. Do you have an example of when you used a custom object as the accumulator? JOËL: So, I've done it for situations where I'm working with objects that are doing tally-like operations, but I'm not doing just a generic tally. There's some domain-specific stuff happening. So, it's some sort of aggregate counter on multiple dimensions that you can use, and that can get really ugly. And you can either do it with a reduce or you can have some sort of, like, initial version of the hash outside and do an each and mutate the hash and stuff like that. All of these tend to be a little bit ugly. So, in those situations, I've often created some sort of custom object that has some instance methods that allow to sort of easily add new elements to it. STEPHANIE: That's really interesting because now I'm starting to think, what if the elements in the collection were also a custom object? [chuckles] And then things could, I feel like, could be really powerful [laughs]. JOËL: There's often a lot of value, right? Because if the items in the collection are also a custom object, you can then have methods on them. And then, again, the sort of complexity of the reduce can sort of, like, fade away because it doesn't own any of the logic. All it does is saying, hey, there's a thing you can do to combine two items. Let's scale it up to work on a collection of items. And now you've sort of, like, really simplified what logic is actually owned inside the reduce. I do want to shout out for those listeners who are theory nerds and want to dig into this. When you have a reduce, and you've got an operation where all the values are of the same type, including the accumulator, typically, what you've got here is some form of monoid. It may be a semigroup. So, if you want to dig into some theory, those are the words to Google and to go a deep dive on. The main thing about monoids, in particular, is that monoids are any objects that have both a sort of a base case, a sort of empty version of themselves, and they have some sort of combining method that allows you to combine two values of that type. If your object has these things and follows...there's a few rules that have to be true. You have a monoid. And they can then be sort of guaranteed to be folded nicely because you can plug in their base case as your initial accumulator. And you can plug in their combining method as just the value of the block, and everything else just falls into place. A classic here is addition for numbers. So, if you want to add two numbers, your combining operator is a plus. And your sort of empty value is a zero. So, you would say, reduce initial value is zero, array of numbers. And your block is just plus, and it won't sum all of the numbers. You could do something similar with strings, where you can combine strings together with plus, and, you know, your empty string is your base case. So, now you're doing sort of string concatenation over arbitrary number of strings. Turns out there's a lot of operations that fall into that, and you can even define some of those on your custom object. So, you're like, oh, I've got a custom object. Maybe I want some way of, like, combining two of them together. You might be heading in the direction of doing something that is monoidal, and if so, that's a really good hint to know that it can sort of, like, just drop into place with a fold or a reduce and that that is a tool that you have available to you. STEPHANIE: Yeah, well, I think my eyes, like, widened a little bit when you first dropped the term monoid [laughs]. I do want to spend the last bit of our time talking about when not to use reduce, and, you know, we did talk a lot about recursion. But when do you think a regular old loop will just be enough? JOËL: So, you're suggesting when would you want to use something like an each rather than a reduce? STEPHANIE: Yeah. In my mind, you know, you did offer, like, a lot of ways to make reduce simpler, a lot of strategies to end up with some really nice-looking syntax [chuckles], I think. But, oftentimes, I think it can be equally as clear storing your accumulator outside of the iteration and that, like, is enough for me to understand. And reduce takes a little bit of extra overhead to figure out what I'm looking at. Do you have any thoughts about when you would prefer to do that? Or do you think that you would usually reach for something else? JOËL: Personally, I generally don't like the pattern of using each to iterate over a collection and then mutate some external accumulator. That, to me, is a bit of a code smell. It's a sign that each is not quite powerful enough to do the thing that I want to do and that I'm probably needing some sort of more specialized form of iteration. Sometimes, that's reduce. Oftentimes, because each can suffer from the same problem you mentioned from reduce, where it's like, oh, you're doing this thing where you mutate an external accumulator. Turns out what you're really doing is just map. So, use map or use select or, you know, some of the other built-in iterators from the enumerable library. There's a blog post on the thoughtbot blog that I continually link to people. And when I see the pattern of, like, mutating an external variable with each, yeah, I tend to see that as a bit of a code smell. I don't know that I would never do it, but whenever I see that, it's a sign to me to, like, pause and be like, wait a minute, is there a better way to do this? STEPHANIE: Yeah, that's fair. I like the idea that, like, if there's already a method available to you that is more specific to go with that. But I also think that sometimes I'd rather, like, come across that pattern of mutating a variable outside of the iteration over, like, someone trying to do something clever with the reduce. JOËL: Yeah, I guess reduce, especially if it's got, like, a giant block and you've got then, like, things in there that break or call next to skip iterations and things like that, that gets really mind-bending really quickly. I think a case where I might consider using an each over a reduce, and that's maybe generally when I tend to use each, is when I'm doing side effects. If I'm using a reduce, it's because I care about the accumulated value at the end. If I'm using each, it's typically because I am trying to do some amount of side effects. STEPHANIE: Yeah, that's a really good call out. I had that written down in my notes, and I'm glad you brought it up because I've seen them get conflated a little bit, and perhaps maybe that's the source of the pain that I'm talking about. But I really like that heuristic of reduce as, you know, you're caring about the output, as opposed to what's going on inside. Like, you don't want any unexpected behavior. JOËL: And I think that applies to something like map as well. My sort of heuristic is, if I'm doing side effects, I want each. If I want transformed values that are sort of one-to-one in the collection, I want map. If I want a single sort of aggregate value, then I want reduce. STEPHANIE: I think that's the cool thing about mixing paradigms sometimes, where all the strategies you talked about in terms of, you know, using custom, like, objects for your accumulator, or the elements in your collection, like, that's something that we get because, you know, we're using an object-oriented language like Ruby. But then, like, you also are kind of bringing the functional programming lens to, like, when you would use reduce in the first place. And yeah, I am just really excited now [chuckles] to start looking for some places I can use reduce after this conversation and see what comes out of it. JOËL: I think I went on a bit of an interesting journey where, as a newer programmer, reduce was just, like, really intense. And I struggled to understand it. And I was like, ban it from code. I don't want to ever see it. And then, I got into functional programming. I was like, I'm going to do reduce everywhere. And, honestly, it was kind of messy. And then I, like, went really deep on a lot of functional theory, and I think understood some things that then I was able to take back to my code and actually write reduce expressions that are much simpler so that now my heuristic is like, I love reduce; I want to use it, but I want as little as possible in the reduce itself. And because I understand some of these other concepts, I have the ability to know what things can be extracted in a way that will feel very natural, in a way that myself from five years ago would have just been like, oh, I don't know. I've got this, you know, 30-line reduce expression that I know is complicated, but I don't know how to improve. And so, a little bit of the underlying theory, I don't think it's necessary to understand these simplified reduces, but as an author who's writing them, I think it helps me write reduces that are simpler. So, that's been my journey using reduce. STEPHANIE: Yeah. Well, thanks for sharing. And I'm really excited. I hope our listeners have learned some new things about reduce and can look at it from a different light. JOËL: There are so many different perspectives. And I think we keep discovering new mental models as we talk to different people. It's like, oh, this particular perspective. And there's one that we didn't really dig into but that I think makes more sense in a functional world that's around sort of deconstructing a structure and then rebuilding it with different components. The shorthand name of this mental model, which is a fairly common one, is constructor replacement. For anyone who's interested in digging into that, we'll link it in the show notes. I gave a talk at an Elm meetup where I sort of dug into some of that theory, which is really interesting and kind of mind-blowing. Not as relevant, I think, for Rubyists, but if you're in a language that particularly allows you to build custom structures out of recursive types or what are sometimes called algebraic data types, or tagged unions, or discriminated unions, this thing goes by a bajillion names, that is a really interesting other mental model to look at. And, again, I don't think the list that we've covered today is exhaustive. You know, I would love it for any of our listeners; if you have your own mental models for how to think about folding, injecting, reducing, send them in: hosts@bikeshed.fm. We'd love to hear them. STEPHANIE: And on that note, shall we wrap up? JOËL: Let's wrap up. STEPHANIE: Show notes for this episode can be found at bikeshed.fm. JOËL: This show has been produced and edited by Mandy Moore. STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show. JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter. STEPHANIE: Or reach both of us at hosts@bikeshed.fm via email. JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week. ALL: Byeeeeeeee!!!!!!! AD: Did you know thoughtbot has a referral program? If you introduce us to someone looking for a design or development partner, we will compensate you if they decide to work with us. More info on our website at: tbot.io/referral. Or you can email us at referrals@thoughtbot.com with any questions.Support The Bike Shed
Mar 11
42 min
417: Module Docs
Stephanie shares about her vacation at Disney World, particularly emphasizing the technological advancements in the park's mobile app that made her visit remarkably frictionless. Joël had a conversation about a topic he loves: units of measure, and he got to go deep into the idea of dimensional analysis with someone this week. Together, Joël and Stephanie talk about module documentation within software development. Joël shares his recent experience writing module docs for a Ruby project using the YARD documentation system. He highlights the time-consuming nature of crafting good documentation for each public method in a class, emphasizing that while it's a demanding task, it significantly benefits those who will use the code in the future. They explore the attributes of good documentation, including providing code examples, explaining expected usage, suggesting alternatives, discussing edge cases, linking to external resources, and detailing inputs, outputs, and potential side effects. Multidimensional numbers episode YARD docs New factory_bot documentation Dash Solargraph Transcript:  JOËL: Hello and welcome to another episode of The Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Joël Quenneville. STEPHANIE: And I'm Stephanie Minn, and together, we're here to share a bit of what we've learned along the way. JOËL: So, Stephanie, what's new in your world? STEPHANIE: So, I recently was on vacation, and I'm excited [chuckles] to tell our listeners all about it. I went to Disney World [laughs]. And honestly, I was especially struck by the tech that they used there. As a person who works in tech, I always kind of have a little bit of a different experience knowing a bit more about software, I suppose, than just your regular person [laughs], citizen. And so, at Disney World, I was really impressed by how seamlessly the like, quote, unquote, "real life experience" integrated with their use of their branded app to pair with, like, your time at the theme park. JOËL: This is, like, an app that runs on your mobile device? STEPHANIE: Yeah, it's a mobile app. I haven't been to Disney in a really long time. I think the last time I went was just as a kid, like, this was, you know, pre-mobile phones. So, I recall when you get into the line at a ride, you can skip the line by getting what's called a fast pass. And so, you kind of take a ticket, and it tells you a designated time to come back so that you could get into the fast line, and you don't have to wait as long. And now all this stuff is on your mobile app, and I basically did not wait in [laughs] a single line for more than, like, five minutes to go on any of the rides I wanted. It just made a lot of sense that all these things that previously had more, like, physical touchstones, were made a bit more convenient. And I hesitate to use the word frictionless, but I would say that accurately describes the experience. JOËL: That's kind of amazing; the idea that you can use tech to make a place that's incredibly busy also feel seamless and where you don't have to wait in line. STEPHANIE: Yeah and, actually, I think the coolest part was it blended both your, like, physical experience really well with your digital one. I think that's kind of a gripe I have as a technologist [laughs] when I'm just kind of too immersed in my screen as opposed to the world around me. But I was really impressed by the way that they managed to make it, like, a really good supplement to your experience being there. JOËL: So, you're not hyped for a future world where you can visit Disney in VR? STEPHANIE: I mean, I just don't think it's the same. I rode a ride [laughs] where it was kind of like a mini roller coaster. It was called Expedition Everest. And there's a moment, this is, like, mostly indoors, but there's a moment where the roller coaster is going down outside, and you're getting that freefall, like, drop feeling in your stomach. And it also happened to be, like, drizzling that day that we were out there, and I could feel it, you know, like, pelting my head [laughs]. And until VR can replicate that experience [chuckles], I still think that going to Disney is pretty fun. JOËL: Amazing. STEPHANIE: So, Joël, what's new in your world? JOËL: I'm really excited because I had a conversation about a topic that I like to talk about: units of measure. And I got to go deep into the idea of dimensional analysis with someone this week. This is a technique where you can look at a calculation or a function and sort of spot-check whether it's correct by looking at whether the unit for the measure that would come out match what you would expect. So, you do math on the units and ignore the numbers coming into your formula. And, you know, let's say you're calculating the speed of something, and you get a distance and the amount of time it took you to take to go that distance. And let's say your method implements this as distance times time. Forget about doing the actual math with the numbers here; just look at the units and say, okay, we've got our meters, and we've got our seconds, and we're multiplying them together. The unit that comes out of this method is meters times seconds. You happen to know that speeds are not measured in meters times seconds. They're measured in meters divided by seconds or meters per second. So, immediately, you get a sense of, like, wait a minute, something's wrong here. I must have a bug in my function. STEPHANIE: Interesting. I'm curious how you're representing that data to, like, know if there's a bug or not. In my head, when you were talking about that, I'm like, oh yeah, I definitely recall doing, like, math problems for homework [laughs] where I had, you know, my meters per second. You have your little fractions written out, and then when you multiply or divide, you know how to, like, deal with the units on your piece of paper where you're showing your work. But I'm having a hard time imagining what that looks like as a programmer dealing with that problem. JOËL: You could do it just all in your head based off of maybe some comments that you might have or the name of the variable or something. So, you're like, okay, well, I have a distance in meters and a time in seconds, and I'm multiplying the two. Therefore, what should be coming out is a value that is in meters times seconds. If you want to get fancier, you can do things with value objects of different types. So, you say, okay, I have a distance, and I have a time. And so, now I have sort of a multiplication of a distance and a time, and sort of what is that coming out as? That can sometimes help you prevent from having some of these mistakes because you might have some kind of error that gets raised at runtime where it's like, hey, you're trying to multiply two units that shouldn't be multiplied, or whatever it is. You can also, in some languages, do this sort of thing automatically at the type level. So, instead of looking at it yourself and sort of inferring it all on your own based off of the written code, languages like F# have built-in unit-of-measure systems where once you sort of tag numbers as just being of a particular unit of measure, any time you do math with those numbers, it will then tag the result with whatever compound unit comes from that operation. So, you have meters, and you have seconds. You divide one by the other, and now the result gets tagged as meters per second. And then, if you have another calculation that takes the output of the first one and it comes in, you can tell the compiler via type signature, hey, the input for this method needs to be in meters per second. And if the other calculation sort of automatically builds something that's of a different unit, you'll get a compilation error. So, it's really cool what it can do. STEPHANIE: Yeah, that is really neat. I like all of those built-in guardrails, I suppose, to help you, you know, make sure that your answer is correct. Definitely could have used that [chuckles]. Turns out I just needed a calculator to take my math test with [laughs]. JOËL: I think what I find valuable more than sort of the very rigorous approach is the mindset. So, anytime you're dealing with numbers, thinking in your mind, what is the unit of this number? When I do math with it with a different number, is it the same unit? Is it a different unit? What is the unit of the thing that's coming out? Does this operation make sense in the domain of my application? Because it's easy to sometimes think you're doing a math operation that makes sense, and then when you look at the unit, you're like, wait a minute, this does not make sense. And I would go so far as to say that, you know, you might think, oh, I'm not doing a physics app. I don't care about units of measure. Most numbers in your app that are actually numbers are going to have some kind of unit of measure associated to them. Occasionally, you might have something where it's just, like, a straight-up, like, quantity or something like that. It's a dimensionless number. But most things will have some sort of unit. Maybe it's a number of dollars. Maybe it is an amount of time, a duration. It could be a distance. It could be all sorts of things. Typically, there is some sort of unit that should attach to it. STEPHANIE: Yeah. That makes sense that you would want to be careful about making sure that your mathematical operations that you're doing when you're doing objects make sense. And we did talk about this in the last episode about multidimensional numbers a little bit. And I suppose I appreciate you saying that because I think I have mostly benefited from other people having thought in that mindset before and encoding, like I mentioned, those guardrails. So, I can recall an app where I was working with, you know, some kind of currency or money object, and that error was raised when I would try to divide by zero because rather than kind of having to find out later with some, not a number or infinite [laughs] amount of money bug, it just didn't let me do that. And that wasn't something that I had really thought about, you know, I just hadn't considered that zero value edge case when I was working on whatever feature I was building. JOËL: Yeah, or even just generally the idea of dividing money. What does that even mean? Are you taking an amount of money and splitting it into two equivalent piles to split among multiple people? That kind of makes sense. Are you dividing money by another money value? That's now asking a very different kind of question. You're asking, like, what is the ratio between these two, I guess, piles of money if we want to make it, you know, in the physical world? Is that a thing that makes sense in your application? But also, realize that that ratio that you get back is not itself an amount of money. And so, there are some subtle bugs that can happen around that when you don't keep track of what your quantities are. So, this past week, I've been working on a project where I ended up having to write module docs for the code in question. This is a Ruby project, so I'm writing docs using the YARD documentation system, where you effectively just write code comments at the sort of high level covering the entire class and then, also, individual documentation comments on each of the methods. And that's been really interesting because I have done this in other languages, but I'd never done it in Ruby before. And this is a piece of code that was kind of gnarly and had been tricky for me to figure out. And I figured that a couple of these classes could really benefit from some more in-depth documentation. And I'm curious, in your experience, Stephanie, as someone who's writing code, using code from other people, and who I assume occasionally reads documentation, what are the things that you like to see in good sort of method-level docs? STEPHANIE: Personally, I'm really only reading method-level docs when, you know, at this point, I'm, like, reaching for a method. I want to figure out how to use it in my use case right now [laughs]. So, I'm going to search API documentation for it. And I really am just scanning for inputs, especially, I think, and maybe looking at, you know, some potential various, like, options or, like, variations of how to use the method. But I'm kind of just searching for that at a glance and then moving on [laughs] with my day. That is kind of my main interaction with module docs like that, and especially ones for Ruby and Rails methods. JOËL: And for clarity's sake, I think when we're talking about module docs here, I'm generally thinking of, like, any sort of documentation that sort of comments in code meant to document. It could be the whole modular class. It could be on a per-method level, things like RDoc or YARD docs on Ruby classes. You used the word API docs here. I think that's a pretty similar idea. STEPHANIE: I really haven't given the idea of writing this kind of documentation a lot of thought because I've never had to do too much of it before, but I know, recently, you have been diving deep into it because, you know, like you said, you found these classes that you were working with a bit ambiguous, I suppose, or just confusing. And I'm wondering what kind of came out of that journey. What are some of the most interesting aspects of doing this exercise? JOËL: And one of the big ones, and it's not a fun one, but it is time-consuming. Writing good docs per method for a couple of classes takes a lot of time, and I understand why people don't do it all the time. STEPHANIE: What kinds of things were you finding warranted that time? Like, you know, you had to, at some point, decide, like, whether or not you're going to document any particular method. And what were some of the things you were looking out for as good reasons to do it? JOËL: I was making the decisions to document or not document on a class level, and then every public method gets documentation. If there's a big public API, that means every single one of those methods is getting some documentation comments, explaining what they do, how they're meant to be used, things like that. I think my kind of conclusion, having worked with this, is that the sort of sweet spot for this sort of documentation is for anything that is library-like, so a lot of things that maybe would go into a Rails lib directory might make sense. Anything you're turning into a gem that probably makes sense. And sometimes you have things in your Rails codebase that are effectively kind of library-like, and that was the case for the code that I was dealing with. It was almost like a mini ORM style kind of ActiveRecord-inspired series of base classes that had a bunch of metaprogramming to allow you to write models that were backed by not a database but a headless CMS, a content management system. And so, these classes are not extracted to the lib directory or, like, made into a gem, but they feel very library-esque in that way. STEPHANIE: Library-like; I like that descriptor a lot because it immediately made me think of another example of a time when I've used or at least, like, consumed this type of documentation in a, like, SaaS repo. Rather, you know, I'm not really seeing that level of documentation around domain objects, but I noticed that they really did a lot of extending of the application record class because they just had some performance needs that they needed to write some, like, custom code to handle. And so, they ended up kind of writing a lot of their own ORM-like methods for just some, like, custom callbacks on persisting and some just, like, bulk insertion functionality. And those came with a lot of different ways to use them. And I really appreciated that they were heavily documented, kind of like you would expect those ActiveRecord methods to be as well. JOËL: So, I've been having some conversations with other members at thoughtbot about when they like to use the style of module doc. What are some of the alternatives? And one that kept coming up for different people that they would contrast with this is what they would call the big README approach, and this could be for a whole gem, or it could be maybe some directory with a few classes in your application that's got a README in the root of the directory. And instead of documenting each method, you just write a giant README trying to answer sort of all of the questions that you anticipate people will ask. Is that something that you've seen, and how do you feel about that as a tool when you're looking for help? STEPHANIE: Yes. I actually really like that style of documentation. I find that I just want examples to get me started, especially; I guess this is especially true for libraries that I'm not super familiar with but need to just get a working knowledge about kind of immediately. So, I like to see examples, the getting started, the just, like, here's what you need to know. And as I start to use them, that will get me rolling. But then, if I find I need more details, then I will try to seek out more specific information that might come in the form of class method documentation. But I'm actually thinking about how FactoryBot has one of the best big README-esque [laughs] style of documentation, and I think they did a really big refresh of the docs not too long ago. It has all that high-level stuff, and then it has more specific information on how to use, you know, the most common methods to construct your factories. But those are very detailed, and yet they do sit, like, separately from inline, like, code documentation in the style of module docs that we're talking about. So, it is kind of an interesting mix of both that I think is helpful for me personally when I want both the “what do I need to know now?” And the, “like, okay, I know where to look for if I need something a little more detailed.” JOËL: Yeah. The two don't need to be mutually exclusive. I thought it was interesting that you mentioned how much examples are valuable to you because...I don't know if this is controversial, but an opinion that I have about sort of per-method documentation is that you should always default to having a code example for every method. I don't care how simple it is or how obvious it is what it does. Show me a code example because, as a developer, examples are really, really helpful. And so, seeing that makes documentation a lot more valuable than just a couple of lines that explain something that was maybe already obvious from the title of the method. I want to see it in action. STEPHANIE: Interesting. Do you want to see it where the method definition is? JOËL: Yes. Because sometimes the method definition, like, the implementation, might be sort of complex. And so, just seeing a couple of examples, like, oh, you call with this input, you get that. Call with this other input; you get this other thing. And we see this in, you know, some of the core docs for things like the enumerable methods where having an example there to be like, oh, so that's how map works. It returns this thing under these circumstances. That sort of thing is really helpful. And then, I'll try to do it at a sort of a bigger level for that class itself. You have a whole paragraph about here's the purpose of the class. Here's how you should use it. And then, here's an example of how you might use it. Particularly, if this is some sort of, like, base class you're meant to inherit from, here's the circumstances you would want to subclass this, and then here's the methods you would likely want to override. And maybe here are the DSLs you might want to have and to kind of package that in, like, a little example of, in this case, if you wanted a model that read from the headless CMS, here's what an example of such a little model might look like. So, it's kind of that putting it all together, which I think is nice in the module docs. It could probably also live in the big README at some level. STEPHANIE: Yeah. As you are saying that, I also thought about how I usually go search for tests to find examples of usage, but I tend to get really overwhelmed when I see inline, like, that much inline documentation. I have to, like, either actively ignore it, choose to ignore it, or be like, okay, I'm reading this now [laughs]. Because it just takes up so much visual space, honestly. And I know you put a lot of work into it, a lot of time, but maybe it's because of the color of my editor theme where comments are just that, like, light gray [laughs]. I find them quite easy to just ignore. But I'm sure there will be some time where I'm like, okay, like, if I need them, I know they're there. JOËL: Yeah, that is, I think, a downside, right? It makes it harder to browse the code sometimes because maybe your entire screen is almost taken up by documentation, and then, you know, you have one method up, and you've got to, like, scroll through another page of documentation before you hit the next method, and that makes it harder to browse. And maybe that's something that plays into the idea of that separation between library-esque code versus application code. When you browse library-esque code, when you're actually browsing the source, you're probably doing it for different reasons than you would for code in your application because, at that point, you're effectively source diving, sometimes being like, oh, I know this class probably has a method that will do the thing I want. Where is it? Or you're like, there's an edge case I don't understand on this method. I wonder what it does. Let me look at the implementation. Or even some existing code in the app is using this library method. I don't know what it does, but they call this method, and I can't figure out why they're using it. Let me look at the source of the library and see what it does under the hood. STEPHANIE: Yeah. I like the distinction of it is kind of a different mindset that you're reading the code at, where, like, sometimes my brain is already ready to just read code and try to figure out inputs and outputs that way. And other times, I'm like, oh, like, I actually can't parse this right now [chuckles]. Like, I want to read just English, like, telling me what to expect or, like, what to look out for, especially when, like you said, I'm not really, like, trying to figure out some strange bug that would lead me to diving deep in the source code. It's I'm at the level where I'm just reaching for a method and wanting to use it. We're writing these YARD docs. I think I also heard you mention that you gave some, like, tips or maybe some gotchas about how to use certain methods. I'm curious why that couldn't have been captured in a more, like, self-documenting way. Or was there a way that you could have written the code for that not to have been needed as a comment or documented as that? And was there a way that method names could have been clear to signal, like, the intention that you were trying to convey through your documentation? JOËL: I'm a big fan of using method names as a form of documentation, but they're frequently not good enough. And I think comments, whether they're just regular inline comments or more official documentation, can be really good to help avoid sort of common pitfalls. And one that I was working with was, there were two methods, and one would find by a UID, so it would search up a document by UID. And another one would search by ID. And when I was attempting to use these before I even started documenting, I used the wrong one, and it took me a while to realize, oh wait, these things have both UIDs and IDs, and they're slightly different, and sometimes you want to use one or the other. The method names, you know, said like, "Find by ID" or "Find by UID." I didn't realize there were both at the time because I wasn't browsing the source. I was just seeing a place where someone had used it. And then, when I did find it in the source, I'm like, well, what is the difference? And so, something that I did when I wrote the docs was sort of call out on both of those methods; by the way, there is also find by UID. If you're searching by UID, consider using the other one. If you don't know what the difference is, here's a sentence summarizing the difference. And then, here's a link to external documentation if you want to dive into the nitty gritty of why there are two and what the differences are. And I think that's something you can't capture in just a method name. STEPHANIE: Yeah, that's true. I like that a lot. Another use case you can think of is when method names are aliased, and it's like, I don't know how I would have possibly known that until I, you know, go through the journey of realizing [laughs] that these two methods do the same thing or, like, stumbling upon where the aliasing happens. But if that were captured in, like, a little note when I'm in, like, a documentation viewer or something, it's just kind of, like, a little tidbit of knowledge [laughs] that I get to gain along the way that ends up, you know, being useful later because I will have just kind of...I will likely remember having seen something like that. And I can at least start my search with a little bit more context than when you don't know what you don't know. JOËL: I put a lot of those sorts of notes on different methods. A lot of them are probably based on a personal story where I made a mistaken assumption about this method, and then it burned me. But I'm like, okay, nobody else is going to make that mistake. By the way, if you think this is what the method does, it does something slightly different and, you know, here's why you need to know that. STEPHANIE: Yeah, you're just looking out for other devs. JOËL: And, you know, trying to, like, take my maybe negative experience and saying like, "How can I get value out of that?" Maybe it doesn't feel great that I lost an hour to something weird about a method. But now that I have spent that hour, can I get value out of it? Is the sort of perspective I try to have on that. So, you mentioned kind of offhand earlier the idea of a documentation viewer, which would be separate than just reading these, I guess, code comments directly in your code editor. What sort of documentation viewers do you like to use? STEPHANIE: I mostly search in my browser, you know, just the official documentation websites for Rails, at least. And then I know that there are also various options for Ruby as well. And I think I had mentioned it before but using DuckDuckGo as my search engine. I have nice bang commands that will just take me straight to the search for those websites, which is really nice. Though, I have paired with people before who used various, like, macOS applications to do something similar. I think Alfred might have some built-in workflows for that. And then, a former co-worker used to use one called Dash, that I have seen before, too. So, it's another one of those just handy just, like, search productivity extensions. JOËL: You mentioned the Rails documentation, and this is separate from the guides. But the actual Rails docs are generated from comments like this inline in code. So, all the different ActiveRecord methods, when you search on the Rails documentation you're like, oh yeah, how does find_by work? And they've got a whole, like, paragraph explaining how it works with a couple of examples. That's this kind of documentation. If you open up that particular file in the source code, you'll find the comments. And it makes sense for Rails because Rails is more of, you know, library-esque code. And you and I search these docs pretty frequently, although we don't tend to do it, like, by opening the Rails gem and, like, grepping through the source to find the code comment. We do it through either a documentation site that's been compiled from that source or that documentation that's been extracted into an offline tool, like you'd mentioned, Dash. STEPHANIE: Yeah, I realized how conflicting, I suppose, it is for me to say that I find inline documentation really overwhelming or visually distracting, whereas I recognize that the only reason I can have that nice, you know, viewing experience is because documentation viewers use the code comments in that format to be generated. JOËL: I wonder if there's like a sort of...I don't know what this pattern is called, but a bit of a, like, middle-quality trap where if you're going to source dive, like, you'd rather just look at the code and not have too much clutter from sort of mediocre comments. But if the documentation is really good and you have the tooling to read it, then you don't even need to source dive at all. You can just read the documentation, and that's sufficient. So, both extremes are good, but that sort of middle kind of one foot in each camp is sort of the worst of both worlds experience. Because I assume when you look for Rails documentation, you never open up the actual codebase to search. The documentation is good enough that you don't even need to look at the files with the comments and the code. STEPHANIE: Yeah, and I'm just recalling now there's, like, a UI feature to view the source from the documentation viewer page. JOËL: Yes. STEPHANIE: I use that actually quite a bit if the comments are a little bit sparse and I need just the code to supplement my understanding, and that is really nice. But you're right, like, I very rarely would be source diving, unless it's a last resort [laughs], let's be honest. JOËL: So, we've talked about documentation viewers and how that can make things nice, and you're able to read documentation for things. But a lot of other tooling can benefit from this sort of model documentation as well, and I'm thinking, in particular, Solargraph, which is Ruby's language server protocol. And it has plugins for VS Code, for Vim, for a few different editors, takes advantage of that to provide all sorts of things. So, you can get smart expansion of code and good suggestions. You can get documentation for what's under your cursor. Maybe you're reading somebody else's code that they've written, and you're like, why are they calling this parameterized method here? What does that even do? Like, in VS Code, you could just hover over it, and it will pop up and show you documentation, including the, like, inputs and return types, and things like that. That's pretty nifty. STEPHANIE: Yeah, that is cool. I use VS Code, but I've not seen that too much yet because I don't think I've worked in enough codebases with really comprehensive [laughs] YARD docs. I'm actually wondering, tooling-wise, did you use any helpful tools when you were writing them or were you hand-documenting each? JOËL: I was hand-documenting everything. STEPHANIE: Class. Okay. JOËL: The thing that I did use is the YARD gem, which you don't need to have the gem to write YARD-style documentation. But if you have the gem, you can run a local server and then preview a documentation site that is generated from your comments that has everything in there. And that was incredibly helpful for me as I was trying to sort of see an overview of, okay, what would someone who's looking at the docs generated from this see when they're trying to look for what the documentation of a particular method does? STEPHANIE: Yeah, and that's really nice. JOËL: Something that I am curious about that I've not really had a lot of experience with is whether or not having extra documentation like that can help AI tools give us better suggestions. STEPHANIE: Yeah, I don't know the answer to that either, but I would be really curious to know if that is already something that happens with something like Copilot. JOËL: Do better docs help machines, or are they for humans only? STEPHANIE: Whoa, that's a very [laughs] philosophical question, I think. It would make sense, though, that if we already have ways to parse and compile this kind of documentation, then I can see that incorporating them into the types of, like, generative problems that AI quote, unquote "solves" [chuckles] would be really interesting to find out. But anyone listening who kind of knows the answer to that or has experience working with AI tools and various types of code comment documentation would be really curious to know what your experience is like and if it improves your development workflow. So, for people who might be interested in getting better at documenting their code in the style of module docs, what would you say are some really great attributes of good documentation in this form? JOËL: I think, first of all, you have to write from the motivation of, like, if you were confused and wanting to better understand what a method does, what would you like to see? And I think coming from that perspective, and that was, in my case, I had been that person, and then I was like, okay, now that I've figured it out, I'm going to write it so that the next person is not confused. I have five or six things that I think were really valuable to add to the docs, a few of which we've already mentioned. But rapid fire, first of all, code example. I love code examples. I want a code example on every method. An explanation of expected usage. Here's what the method does. Here's how we expect you to use this method in any extra context about sort of intended use. Callouts for suggested alternatives. If there are methods that are similar, or there's maybe a sort of common mistake that you would reach for this method, put some sort of call out to say, "Hey, you probably came here trying to do X. If that's what you were actually trying to do, you should use method Y." Beyond that, a discussion of edge cases, so any sort of weird ways the method behaves. You know, when you pass nil to it, does it behave differently? If you call it in a different context, does it behave differently? I want to know that so that I'm not totally surprised. Links to external resources–really great if I want to, like, dig deeper. Is this method built on some sort of, like, algorithm that's documented elsewhere? Please link to that algorithm. Is this method integrating with some, like, third-party API? You know, they have some documentation that we could link to to go deeper into, like, what these search options do. Link to that. External links are great. I could probably find it by Googling myself, but you are going to make me very happy as a developer if you already give me the link. You'd mentioned capturing inputs and outputs. That's a great thing to scan for. Inputs and outputs, though, are more sometimes than just the arguments and return values. Although if we're talking about arguments, any sort of options hash, please document the keys that go in that because that's often not obvious from the code. And I've spent a lot of time source diving and jumping between methods trying to figure out like, what are the options I can pass to this hash? Beyond the explicit inputs and outputs, though, anything that is global state that you rely on. So, do you need to read something from an environment variable or even a global variable or something like that that might make this method behave differently in different situations? Please document that. Any situations where you might raise an error that I might not expect or that I might want to rescue from, let me know what are the potential errors that might get raised. And then, finally, any sorts of side effects. Does this method make a network call? Are you writing to the file system? I'd like to know that, and I'd have to, like, figure it out by trial and error. And sometimes, it will be obvious in just the description of the method, right? Oh, this method pulls data from a third-party API. That's pretty clear. But maybe it does some sort of, like, caching in the background or something to a file that's not really important. But maybe I'm trying to do a unit test that involves this, and now, all of a sudden, I have to do some weird stubbing. I'd like to know that upfront. So, those are kind of all the things I would love to have in my sort of ideal documentation comment that would make my life easier as a developer when trying to use some code. STEPHANIE: Wow. What a passionate plea [laughs]. I was very into listening to you list all of that. You got very animated. And it makes a lot of sense because I feel like these are kind of just the day-to-day developer issues we run into in our work and would be so awesome if, especially as the, you know, author where you have figured all of this stuff out, the author of a, you know, a method or a class, to just kind of tell us these things so we don't have to figure it out ourselves. I guess I also have to respond to that by saying, on one hand, I totally get, like, you want to be saved [chuckles] from those common pitfalls. But I think that part of our work is just going through that and playing around and exploring with the code in front of us, and we learn all of that along the way. And, ultimately, even if that is all provided to you, there is something about, like, going through it yourself that gives you a different perspective on it. And, I don't know, maybe it's just my bias against [laughs] all the inline text, but I've also seen a lot of that type of information captured at different levels of documentation. So, maybe it is a Confluence doc or in a wiki talking about, you know, common gotchas for this particular problem that they were trying to solve. And I think what's really cool is that, you know, everyone can kind of be served and that people have different needs that different styles of documentation can meet. So, for anyone diving deep in the source code, they can see all of those examples inline. But, for me, as a big Googler [laughs], I want to see just a nice, little web app to get me the information that I need to find. I'm happy having that a little bit more, like, extracted from my source code. JOËL: Right. You don't want to have to read the source code with all the comments in it. I think that's a fair criticism and, yeah, probably a downside of this. And I'm wondering, there might be some editor tooling that allows you to just collapse all comments and hide them if you wanted to focus on just the code. STEPHANIE: Yeah, someone, please build that for me. That's my passionate plea [laughs]. And on that note, shall we wrap up? JOËL: Let's wrap up. STEPHANIE: Show notes for this episode can be found at bikeshed.fm. JOËL: This show has been produced and edited by Mandy Moore. STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show. JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter. STEPHANIE: Or reach both of us at hosts@bikeshed.fm via email. JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week. ALL: Bye. AD: Did you know thoughtbot has a referral program? If you introduce us to someone looking for a design or development partner, we will compensate you if they decide to work with us. More info on our website at: tbot.io/referral. Or you can email us at referrals@thoughtbot.com with any questions.Support The Bike Shed
Mar 4
39 min
416: Multi-Dimensional Numbers
Joël discusses the challenges he encountered while optimizing slow SQL queries in a non-Rails application. Stephanie shares her experience with canary deploys in a Rails upgrade. Together, Stephanie and Joël address a listener's question about replacing the wkhtml2pdf tool, which is no longer maintained. The episode's main topic revolves around the concept of multidimensional numbers and their applications in software development. Joël introduces the idea of treating objects containing multiple numbers as single entities, using the example of 2D points in space to illustrate how custom classes can define mathematical operations like addition and subtraction for complex data types. They explore how this approach can simplify operations on data structures, such as inventories of T-shirt sizes, by treating them as mathematical objects. EXPLAIN ANALYZE visualizer Canary in a coal mine Episode 413: Developer Tales of Package Management Docs for media-specific CSS Episode 386: Value Objects Revisited: The Tally Edition Money gem Transcript: STEPHANIE: Hello and welcome to another episode of The Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Stephanie Minn. JOËL: And I'm Joël Quenneville. And together, we're here to share a bit of what we've learned along the way. STEPHANIE: So, Joël, what's new in your world? JOËL: I've recently been trying to do some performance enhancements to some very slow queries. This isn't a Rails app, so we're sort of combining together a bunch of different scopes. And the way they're composing together is turning out to be really slow. And I've reached for a tool that is just really fun. It's a visualizer for SQL query plans. You can put the SQL keywords in front of a query: 'EXPLAIN ANALYZE,' and it will then output a query plan, sort of how it's going to attempt to do the work. And that might be like, oh, we're going to use this index on this table to join on this other thing, and then we're going to...maybe this is a table that we think we're going to do a sequential scan through and, you know, it builds out a whole thing. It's a big block of text, and it's kind of intimidating to look at. So, there are a few websites out there that will do this. You just paste a query plan in, and they will build you a nice, little visualization, almost like a tree of, like, tasks to be done. Oftentimes, they'll also annotate it with metadata that they pulled from the query plan. So, oh, this particular node is the really expensive one because we're doing a sequential scan of this table that has 15 million rows in it. And so, it's really useful to then sort of pinpoint what are the areas that you could optimize. STEPHANIE: Nice. I have known that you could do that EXPLAIN ANALYZE on a SQL query, but I've never had to do it before. Is this your first time, or is it just your first time using the visualizer? JOËL: I've played around with EXPLAIN ANALYZE a little bit before. Pro tip: In Rails, if you've got a scope, you can just chain dot explain on the end, and instead of running the query, it will run the EXPLAIN version of it and return the query plan. So, you don't need to, like, turn into SQL then manually run it in your database system to get the EXPLAIN. You can just tack a dot explain on there to get the query plan. It's still kind of intimidating, especially if you've got a really complex query that's...this thing might be 50 lines long of EXPLAIN with all this indentation and other stuff. So, putting it into a sort of online visualizer was really helpful for the work that I was doing. So, it was my first time using an online visualizer. There are a few out there. I'll link to the one that I used in the show notes. But I would do that again, would recommend. STEPHANIE: Nice. JOËL: So, Stephanie, what's new in your world? STEPHANIE: So, I actually just stepped away from being in the middle of doing a Rails upgrade [chuckles] and releasing it to production just a few minutes before getting on to record with you on this podcast. And the reason I was able to do that, you know, without feeling like I had to just monitor to see how it was going is because I'm on a project where the client is using canary deploys. And I was so pleasantly surprised by how easy it made this experience where we had decided to send the canary release earlier this morning. And the way that they have it set up is that the canary goes to 10% of traffic. 10% of the users were on Rails 7 for their sessions. And we saw a couple of errors in our error monitoring service. And we are like, "Okay, like, let's take a look at this, see what's going on." And it turns out it was not too big of a deal because it had to do with, like, a specific page. And, for the most part, if a user did encounter this error, they probably wouldn't again after refreshing because they had, like, a 90% chance [chuckles] of being directed to the previous version where everything is working. And we were kind of making that trade-off of like, oh, we could hotfix this right now on the canary release. But then, as we were starting to debug a little bit, it was a bit hairier than we expected originally. And so, you know, I said, "I have to hop on to go record The Bike Shed. So, why don't we just take this canary down just for the time being to take that time pressure off? And it's Friday, so we're heading into the weekend. And maybe we can revisit the issue with some fresh eyes." So, I'm feeling really good, actually. And I'm glad that we were able to do something that seems scary, but there were guardrails in place to make it a lot more chill. JOËL: Yay for the ability to roll back. You used the term canary release. That's not one that I'm familiar with. Can you explain what a canary release is? STEPHANIE: Oh yeah. Have you heard of the phrase 'Canary in the coal mine'? JOËL: I have. STEPHANIE: Okay. So, I believe it's the same idea where you are, in this case, releasing a potentially risky change, but you don't want to immediately make it available to, like, all of your users. And so, you send this change to, like, a small reach, I suppose, and give it a little bit of a test and see [chuckles] what comes back. And that can help inform you of any issues or risks that might happen before kind of committing to deploying a potentially risky change with a bigger impact. JOËL: Is this handled with something like a feature flag framework? Or is this, like, at an infrastructure level where you're just like, "Hey, we've got the canary image in, like, one container on one server, and then we'll redirect 10% of traffic to that to be served by that one and the other 90% to be served by the old container or something like that"? STEPHANIE: Yeah, in this case, it was at the infrastructure level. And I have also seen something similar at a feature flag level, too, where you're able to have some more granularity around what percent of users are seeing a feature. But I think with something like a Rails upgrade, it was nice to be able to have that at that infrastructure level. It's not necessarily, like, a particular page or feature to show or not show. JOËL: Yeah, I think you would probably want that at a higher level when you're changing over the entire app. Is this something that you had to custom-build yourself or something that just sort of came out of the box with some of the infrastructure tools you're using? STEPHANIE: It came out of the box, actually. I just joined this client project this week and was very delighted to see just some really great deployment infrastructure and getting to meet the DevOps engineers, too, who built it. And they're really proud of it. They kind of walked us through our first release earlier this week. And he was telling me, the DevOps engineer, that this was actually his favorite part of the job, is walking people through their first release and being their buddy while they do it. Because I think he gets to also see users interact with the tool that he built, and he had a lot of pride in that, so it was a very delightful experience. JOËL: That's so wonderful. I've been on so many projects where the sort of infrastructure side of things is not the team's strong point, and releasing can be really scary. And it's great to hear the opposite of that. We recently received a question for Stephanie based on an earlier episode. So, the question asks, "In episode 413, Stephanie discussed a recent issue she encountered with wkhtml2pdf. The episode turned into a deeper discussion about package management, but I don't think it ever cycled back to the conclusion. I'm curious: how did Stephanie solve this dilemma? We're facing the same issue on a project that my team maintains. It's an old codebase, and there are bits of old code that use wkhtml2pdf to generate print views of our data in our application. The situation is fairly dire. wkhtml2pdf is no longer maintained. In fact, it won't even be available to install from our operating system's package repositories in June. We're on FreeBSD, but I assume the same will be eventually true for other operating systems. And so, unless you want to maintain some build step to check out and compile the source code for an application that will no longer receive security updates, just living with it isn't really an option. There are three options we're considering. One, eliminate the dependency entirely. Based on user feedback, it sounds like our old developers were using this library to generate PDFs when what users really wanted was an easy way to print. So, instead of downloading a PDF, just ensure the screen has a good print style sheet and register an onload handler to call window dot print. We're thinking we could implement this as an A/B test to the feature to test this theory. Or two, replace wkhtml2pdf with a call to Headless Chrome and use that to generate the PDF. Or, three, replace wkhtml2pdf with a language-level package. For us, that might be the dompdf library available via Composer because we're a PHP shop." Yeah, a lot to unpack here. Any high-level thoughts, Stephanie? STEPHANIE: My first thought while I was listening to you read that question is that wkhtml2pdf is such a mouthful [laughs]. And I was impressed how you managed to say it at least, like, five times. JOËL: So, I try to say that five times fast. STEPHANIE: And then, my second high-level thought was, I'm so sorry to Brian, our listener who wrote in, because I did not really solve this dilemma [chuckles] for my project and team. I kind of kicked the can down the road, and that's because this was during a support and maintenance rotation that I've talked a little bit about before on the show. I was only working on this project for about a week. And what we thought was a small bug to figure out why PDFs were a little bit broken turned out, as you mentioned, to be this kind of big, dire dilemma where I did not feel like I had enough information to make a good call about what to do. So, I kind of just shared my findings that, like, hey, there is kind of a risk and hoping that someone else [laughs] would be able to make a better determination. But I really was struck by the options that you were considering because it was actually a bit of a similar situation to the bug I was sharing where the PDF that was being generated that was slightly broken. I don't think it was, like, super valuable to our users that it be in the form of a PDF. It really was just a way for them to print something to have on handy as a reference from, you know, some data that was generated from the app. So, yeah, based on what you're sharing, I feel really excited about the first one. Joël, I'm sure you have some opinions about this as well. JOËL: I love sort of the bigger picture thinking that Brian is doing here, sort of stepping back and being like, wait, why do we even need PDF here, and how are our customers using it? I think those are the really good questions to ask before sinking a ton of time into coming up with something that might be, like, a bit of a technical wonder. Like, hey, we managed to, like, do this PDF generation thing that we had to, like, cobble together so many other things. And it's so cool technically, but does it actually solve the underlying problem? So, shout out to Brian for thinking about it in those terms. I love that. Second cool thing that I wanted to shout out, because I think this is a feature of browsers that not many people are aware of; you can have multiple style sheets for your page, and you can tag them to be for different media. So, you can have a style sheet that only gets applied when you print versus when you display on screen. And there are a couple of others. I don't remember exactly what they are. I'll link to the docs in the show notes. But taking advantage of this, like, this is old technology but making that available and saying, "Yeah, we'll make it so that it's nice when you print, and we'll maybe even, you know, a link or a button with JavaScript so that you could just Command-P or Control-P to print. But we'll have a button in there as well that will allow you to print to PDF," and that solves your problem right there. STEPHANIE: Yeah, that's really cool. I didn't know that about being able to tag style sheets for different media types. That's really fascinating. And I like that, yeah, we're just eliminating this dependency on something, like, potentially really complex with a, hopefully, kind of elegant and modern solution, maybe. JOËL: And your browser is already able to do so many of these things. Why do we sort of try to recreate it? Printing is a thing browsers have been able to do for a long time. Printing to PDF is a thing that you can do for a long time. I will sometimes use that on sites where I need to, let's say I'm purchasing something, and I need some sort of receipt to expense, but they won't give me a download, a PDF download that I can send to the accounting team, so I will print to PDF the, like, HTML view. And that works just fine. It's kind of a workaround hack. Sometimes, it doesn't work well because the HTML page is just not well set up to, like, show up on a PDF page. You get some, like, weird, like, pagination issues or things like that. But, you know, just a little bit of thought for a print style sheet, especially for something you know that people are likely going to want to print or to save to PDF, that's a nice touch. STEPHANIE: Yeah. So, good luck, Brian, and let us know how this goes and any outcomes you find successful. So, for today's longer topic, I was excited because I saw, Joël, you dropped something in our topic backlog: Multidimensional Numbers. I'm curious what prompted this idea and what you wanted to say about it. JOËL: We did an episode a while back where we talked about value objects, wrapping numbers, wrapping collections. This is Episode 386, and we were talking about tallying, specifically working with collections of T-shirt sizes and doing math on these sort of objects that might contain multiple numbers. And a sort of sidebar from that that we didn't really get into is the idea that objects that contain sort of multiple numbers can be treated as a number themselves. And I think a great example of this is something like a point in two-dimensional space. It's got an x coordinate, a y coordinate. It's two numbers, but you can treat sort of the combination of the two of them together as a single number. There's a whole set of coordinate math that you can do to do things like add coordinates together, subtract them, find the distance between them. There's a whole field of vector math that we can do on those. And I think learning to recognize that numbers are not just instances of the integer or the float class but that there could be these more complex things that are also numbers is maybe an important realization and something that, as developers, if we think of these sort of more complex values as numbers, or at least mathematical objects, then that will help us write better code. STEPHANIE: Cool. Yeah. When you were first talking about 2D points, I was thinking about if I have experience working with that before or, like, having to build something really heavily based off of, like, a canvas or, you know, a coordinate system. And I couldn't think of any really good examples until I thought about, like, geographic locations. JOËL: Oh yeah, like a latitude, longitude. STEPHANIE: Yeah, exactly. Like, that is a lot more common, I think, for various types of just, like, production applications than 2D points if you're not working on, like, a video game or something like that, I think. JOËL: Right, right. I think you're much more likely to be working with 2D points on some more sort of front-end-heavy application. I was talking with someone this week about managing a seat map for concerts and events like that and sort of creating a seat map and have it be really interactive, and you can, like, click on seats and things like that. And depending on the level of libraries you're using to build that, you may have to do a lot of 2D math to make it all come together. STEPHANIE: Yeah. So, I would love to get into, you know, maybe we've realized, okay, we have some kind of compound number. What are some good reasons for using them differently than you would a primitive? JOËL: So, you mentioned primitives, and I think this is where maybe I'm developing a reputation about, like, always wanting value objects for everything. But it would be really easy, let's say, for an xy point to be just an array of two numbers or maybe even a hash with an x key and a y key. What's tricky about that is that then you don't have the ability to do math on them. Arrays do define the plus operator, but they don't do what you want them to do with points. It's the set union. So, adding two points would not at all do what you want, or subtracting two points. So, instead, if you have a custom 2D point class and you can define plus and minus on there to do the right thing, now they're not pairs of numbers, two values; they're a single value, and you can treat them as if they are just a single number. STEPHANIE: You mentioned that arrays don't do the right thing when you try to add them up. What is the right thing that you're thinking of then? JOËL: It probably depends a little bit on the type of object you're working with. So, with 2D points, you're probably trying to do vector addition where you're effectively saying almost, like, "Shift this point in 2D space by the amount of this other point." Or if you're doing a subtraction, you might even be asking, like, "What is the distance between these two points?" Euclidean distance, I think, is the technical term for this. There's also a couple of different ways you can multiply values. You can multiply a 2D point by just a sort of, not by another point, but by just an integer. That's called scaling. So, you're just like, oh, take this point in 2D space, but make it bigger, make it five times bigger or five times further from the origin. Or you can do some stuff with other points. But what you don't want to do is turn this into, if you're starting with arrays, you don't want to turn this into an array of four points. When you add two points in 2D space, you're not trying to create a point in 4D space. STEPHANIE: Whoa, I mean [laughs], maybe you're not. JOËL: You could but -- [laughter] STEPHANIE: Yeah. While you were saying that, I guess that is what is really cool about wrapping, encapsulating them in objects is that you get to decide what that means for you and your application, and -- JOËL: Yeah. Well, plus can mean different things, right? STEPHANIE: Yeah. JOËL: On arrays, plus means combining two arrays together. On integers, it means you do integer math. And on points, it might be vector addition. STEPHANIE: Are there any other arithmetic operators you can think of that would be useful to implement if you were trying to create some functionality on a point? JOËL: That's a good question because I think realizing the inverse of that is also a really powerful thing. Just because you create a sort of new mathematical object, a point in 2D space, doesn't mean that necessarily every arithmetic operator makes sense on it. Does it make sense to divide a point by another point? Maybe not. And so, instead of going with the mindset of, oh, a point is a mathematical object, I now need to implement all of arithmetic on this, instead, think in terms of your domain. What are the operations that make sense? What are the operations you need for this point? And, you know, maybe the answer is look up what are the common sort of vector math operations and implement those on your 2D point. Some of them will map to arithmetic operators like plus and minus, and then some of them might just be some sort of custom method where maybe you say, "Oh, I want the Euclidean distance between these two points." That's just a thing. Maybe it's just a named instance method on there. But yeah, don't feel like you need to implement all of the math operators because that's a mistake that I have made and then have ended up, like, implementing nonsensical things. STEPHANIE: [laughs] Creating your own math. JOËL: Yes, creating my own math. I've done this even on where I've done value objects to wrap single values. I was doing a class to represent currency, and I was like, well, clearly, you need, like, methods to, like, add or subtract your currency, and that's another thing. If you have, let's say, a plus method, now you can plug it into, let's say, reduce plus. And you can just sum a list of these currency objects and get back a new currency. It's not even going to give you back an integer. You just get a sort of new currency object that is the sum of all the other ones, and that's really nice. STEPHANIE: Yeah, that's really cool. It reminds me of all the magic of enumerable that you had talked about in a previous conference talk, where, you know, you just get so much out of implementing those basic operators that, like, kind of scales in handiness. JOËL: Yes. Turns out Ruby is actually a pretty nice system. If you have objects that respond to some common methods and you plug them into enumerable, and it just all kind of works. STEPHANIE: So, one thing you had said earlier that I've felt kind of excited about and wanted to highlight was you mentioned all the different ways that you could represent a 2D point with more primitive data stores, so, you know, an array of two integers, a hash with xy keys. It got me thinking about how, yeah, like, maybe if your system has to talk to another system and you're importing data or exporting data, it might eventually need to take those forms. But what is cool about having an encapsulated object in your application is you can kind of control those boundaries a little bit and have more confidence in terms of the data types that you're using within your system by having various ways to construct that, like, domain object, even if the data coming in is in a different shape. JOËL: And I think that you're hitting on one of the real beauties of object-oriented programming, where the sort of users of your object don't need to know about the internal representation. Maybe you store an array internally. Maybe it's two separate instance variables. Maybe it's something else entirely. But all that the users of your, let's say, 2D point object really need to care about is, hey, the constructor wants values in this shape, and then I can call these domain methods on it, and then the rest just sort of happens. It's an implementation detail. It doesn't matter. And you alluded, I think, to the idea that you can sort of create multiple constructors. You called them constructors. I tend to call them that as well. But they're really just class methods that will kind of, like, add some sugar on top of the constructor. So, you might have, like, a from array pair or from hash or something like that that allows you to maybe do a little bit of massaging of the data before you pass it into your constructor that might want some underlying form. And I think that's a pattern that's really nice. STEPHANIE: Yeah, I agree. JOËL: Something that can be interesting there, too, is that mathematically, there are multiple ways you can think of a 2D point. An xy coordinate pair is a common one, but another sort of system for representing a point in 2D space is called the polar coordinate system. So, you have some sort of, like, origin point. You're 0,0. And then, instead of saying so many to the left and so many up from that origin point, you give an angle and a distance, and that's where your point is. So, an angle and distance point, I think, you know, theta and magnitude are the fancy terms for this. You could, instead of creating a separate, like, oh, I have a polar coordinate point and a Cartesian coordinate point, and those are separate things, you can say, no, I just have a point in 2D space. They can be constructed from either an xy coordinate pair or a magnitude angle pair. Internally, maybe you convert one to the other for internal representation because it makes the math easier or whatever. Your users never need to know that. They just pass in the values that they want, use the constructor that is most convenient for them, and it might be both. Maybe some parts of the app require polar coordinates; some require Cartesian coordinates. You could even construct one of each, and now you can do math with each other because they're just instances of the same class. STEPHANIE: Whoa. Yeah, I was trying to think about transforming between the two types as well. It's all possible [laughs]. JOËL: Yes. Because you could have reader-type methods on your object that say, oh, for this point, give me its x coordinate; give me its y coordinate. Give me its distance from the origin. Give me its angle from the origin. And those are all questions you can ask that object, and it can calculate them. And you don't need to care what its internal representation is to be able to get all four of those. So, we've been talking about a lot of these sort of composite numbers, not composite numbers, that's a separate mathematical thing, but numbers that are composed of sort of multiple sub-numbers. And what about situations where you have two things, and one of them is not a number? I'm thinking of all sorts of units of measure. So, I don't just have three. I have three, maybe...and we were talking about currency earlier, so maybe three U.S. dollars. Or I don't just have five; I have five, you know, let's say, meters of distance. Would you consider something like that to be one of these compound number things? STEPHANIE: Right. I think I was–when we were originally talking about this, conflating the two. But I realized that, you know, just because we're adding context to a number and potentially packaging it as a value object, it's still different from what we're talking about today where, you know, there's multiple components to the number that are integral or required for it to mean what we intended to mean, if that makes sense. JOËL: Yeah. STEPHANIE: So yeah, I guess we did want to kind of make a distinction between value objects that while the additional context is important and you can implement a lot of different functionality based on what it represents, at the end of the day, it only kind of has one magnitude or, like, one integer to kind of encapsulate it represented as a number. Does that sound right? JOËL: Yeah. You did throw out the words encapsulation and value object. So, in a situation maybe where I have three US dollars, would you create some kind of custom object to wrap that? Or is that a situation where you'd be more comfortable using some kind of primitive? Like, I don't know, maybe an array pair of three and the symbol USD or something like that. STEPHANIE: Oh, I would definitely not do that [laughter]. Yeah. Like I, you know, for the most part, I think I've seen that as a currency object, and that expands the world of what we can do with it, converting into a lot of different other currencies. And yeah, just making sure those things don't get divorced from each other because that context is what gives it meaning. But when it comes to our compound numbers, it's like, without all of the components, it doesn't make sense, or it doesn't even represent the same, like, numerical value that we were trying to convey. JOËL: Right. You need both, or, you know, it could be more than two. It could be three, four, or five numbers together to mean something. You mentioned conversions, which I think is something that's also interesting because a lot of units of measure have sort of multiple ways of measuring, and you often want to convert between them. And maybe that's another case where encapsulation is really nice where, you know, maybe you have a distance object. And you have five meters, and you put that into your distance object, but then somebody wants it in feet somewhere else or in centimeters, or something like that. And it can just do all the conversion math safely inside that object, and the user doesn't have to worry about it. STEPHANIE: Right. This is maybe a bit of a tangent, but as a Canadian living in the U.S., I don't know [laughs] if you have any opinions about converting meters and feet. JOËL: The one I actually do the most often is converting Celsius to Fahrenheit and vice versa. You know, I've been here, what, 11 years now? I don't have a great intuition for Fahrenheit temperatures. So, I'm converting in my head just [laughs] on a daily basis. STEPHANIE: Yeah, that makes sense. Conversions: they're important. They help out our friends who [laughs] are on different systems of measurement. JOËL: There's a classic story that I love about unit conversions. I think it's one of the NASA Mars missions. STEPHANIE: Oh yeah. JOËL: You've heard of this one. It was trying to land on Mars, and it burned up in the atmosphere because two different teams had been building different components and used different unit systems, both according to spec for their own module. But then, when the modules try to talk to each other, they're sending over numbers in meters instead of feet or something like that. And it just caused [laughs] this, like, multi-year, multi-billion dollar project to just burn up. STEPHANIE: That's right. So, lesson of the day is don't do that. I can think of another example where there might be a little bit of misconceptions in terms of how to represent it. And I'm thinking about time and when that has been represented in multiple parts, such as in hours and, minutes and seconds. Do you have any initial impressions about a piece of data like that? JOËL: So, that's really interesting, right? Because, at first glance, it looks like, oh, it's, like, a triplet of hour, minute, seconds. It's sort of another one of these sort of compound numbers, and I guess you could implement it that way. But in reality, you're tracking a single quantity, the amount of time elapsed, and that can be represented with a single number. So, if you're representing, let's say, time of day, what would show up on your clock? That could be, depending on the resolution, number of, let's say, seconds since midnight, and that's a single counter. And then, you can do some math on it to get hours, minutes, seconds for a particular moment. But really, it's a single quantity, and we can do that with time. We can't do that with a 2D point. Like, it has to have two components. STEPHANIE: So, do you have a recommendation for what unit of time time would best be stored? I'm just thinking of all the times that I've had to do that millisecond, you know, that conversion of, you know, however many thousands of milliseconds in my head into something that actually means [laughs] something to me as a human being who measures time in hours and minutes. JOËL: My recommendation is absolutely go for a single number that you store in your, let's say, time of day object. It makes the math so much easier. You don't have to worry about, like, overflowing from one number into another when you're doing math or anything like that. And then the number that you count should be at the whatever the smallest resolution you care at. So, is there ever any time where you want to distinguish between two different milliseconds in time? Or maybe you're like, you know what? These are, like, we're tracking time of day for appointments. We don't care about the difference between two milliseconds. We don't need to track them independently. We don't even care about seconds. The most granular we ever care about things is by the minute. And so, maybe then your internal number that you track is a counter of minutes since midnight. But if you need more precision, you can go down to seconds or milliseconds or nanoseconds. But yeah, find what is the sort of the least resolution you want to get away with and then make that the unit of measure for a single counter in your object. And then encapsulate that so that nobody else needs to care that, internally, your time of day object is doing milliseconds because nobody wants to do that math. Just give me a nice, like, hours and minutes method on your object, and I will use that. I don't need to know internally what it's using. Please don't just pass around integers; wrap it in an object, especially because integers, there's enough times where you're doing seconds versus milliseconds. And when I just have an integer, I never know if the person storing this integer means seconds or milliseconds. So, I'm just like, oh, I'm going to pass to this, like, user object, a, like, time integer. And unless there's a comment or a constant, you know, that's named something duration in milliseconds or something like that, you know, or sometimes even, like, one year in milliseconds, or there's no way of knowing. STEPHANIE: Yeah. That makes a lot of sense. When you kind of choose a standard of a standard unit, it's, like, possible to make it easier [laughs]. JOËL: So, circling back to sort of the initial thing that sparked this conversation, the previous episode about T-shirt inventories, there we were dealing with what started off as, like, a hash of different T-shirt sizes and quantities of T-shirts that we had in that size, so small (five), medium (three), large (four). And then, we eventually turned that into a value object that represented...I think we called it a tally, but maybe we called it inventory. And this may be wrong, so tell me if I'm wrong here, I think we can kind of treat that as a number, as, like, one of these compound numbers. It's a sort of multidimensional number where you say, well, we have sort of three dimensions where we can have numbers that sort of increase and decrease independently. We can do math on these because we can take inventories or tallies and add and subtract them. And that's what we ended up having to do. We created a value object. We implemented plus and minus on it. There are rules for how the math works. I think this is a multidimensional number with the definition we're working on this show. Am I wrong here? STEPHANIE: I wouldn't say that you're wrong. I think I would have to think a little [laughs] more to say definitively that you're right. But I know that this example came from, you know, an application I was actually working on. And one of the main things that we had to do with these representations [laughs], I'm hesitant to call them a number, especially, but we had to compare these representations frequently because an inventory, for example, in a warehouse, wanting to make sure that it is equal to or there's enough of the inventory if someone was placing an order, which would also contain, like, a representation of T-shirt size inventory. And that was kind of where some of that math happened because, you know, maybe we don't want to let someone place an order if the inventory at the warehouse is smaller than their order, right? So, there is something really compelling about the comparison operations that we were doing that kind of is leaning me in the direction of, like, yeah, like, it makes sense to me to use this in a way that I would compare, like, quantities or numbers of something. JOËL: I think one thing that was really compelling to me, and that kind of blew my mind, was that we were trying to, like, figure out some things like, oh, we've got so many people with these size preferences, and we've got so many T-shirts across different warehouses. And we're summing them up and we're trying to say like, "How many do we need to purchase if there is a deficit?" And we can come up with effectively a formula for this. We're like, sum these numbers, when we're talking about just before we introduce sizes when it's just like, oh, people have T-shirts. They all get the count of people and a count of T-shirts in our warehouse, and we find, you know, the difference between that. And there's a few extra math operations we do. Then you introduce size, and you break it down by, oh, we've got so many of each. And now the whole thing gets really kind of messy and complicated. And you're doing these reduces and everything. When we start treating the tally of T-shirts as an object, and now it's a number that responds to plus and minus, all of a sudden, you can just plug those back into the original formula, and it all just works. The original formula doesn't care whether the numbers you're doing this formula on are simple integers or these sort of multidimensional numbers. And that blew my mind, and it was so cool. STEPHANIE: Yeah, that is really neat. And you get a lot of added benefits, too. I think the other important piece in the T-shirt size example was kind of tracking the state change, and that's so much easier when you have an object. There's just a lot more you can do with it. And even if, you know, you're not persisting every single version of the representation, you know, because sometimes you don't want to, sometimes you're really just kind of only holding it in memory to figure out if you need to, you know, do something else. But other times, you do want to persist it. And it just plugs in really well with, like, the rest of object-oriented programming [laughs] in terms of interacting with the rest of your business needs, I think, in your app. JOËL: Yeah, turns out objects, they're kind of nice. And you can do math with them. Who knew? Math is not just about integers. STEPHANIE: And on that note, shall we wrap up? JOËL: Let's wrap up. STEPHANIE: Show notes for this episode can be found at bikeshed.fm. JOËL: This show has been produced and edited by Mandy Moore. STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show. JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter. STEPHANIE: Or reach both of us at hosts@bikeshed.fm via email. JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week. ALL: Byeeeeeee!!!!!! AD: Did you know thoughtbot has a referral program? If you introduce us to someone looking for a design or development partner, we will compensate you if they decide to work with us. More info on our website at: tbot.io/referral. Or you can email us at referrals@thoughtbot.com with any questions.Support The Bike Shed
Feb 26
39 min
415: Codebase Calibration
Stephanie has a delightful and cute Ruby thing to share: Honeybadger, the error monitoring service, has created exceptionalcreatures.com, where they've illustrated and characterized various common Ruby errors into little monsters, and they're adorable. Meanwhile, Joël encourages folks to submit proposals for RailsConf. Together, Stephanie and Joël delve into the nuances of adapting to and working within new codebases, akin to aligning with a shared mental model or vision. They ponder several vital questions that every developer faces when encountering a new project: the balance between exploring a codebase to understand its structure and diving straight into tasks, the decision-making process behind adopting new patterns versus adhering to established ones, and the strategies teams can employ to assist developers who are familiarizing themselves with a new environment. Honeybadger's Exceptional Creatures RailsConf CFP coaching sessions HTTP Cats Support and Maintenance Episode Transcript:  JOËL: Hello and welcome to another episode of The Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Joël Quenneville. STEPHANIE: And I'm Stephanie Minn. And together, we're here to share a bit of what we've learned along the way. JOËL: So, Stephanie, what's new in your world? STEPHANIE: I have a delightful and cute Ruby thing to share I'd seen just in our internal company Slack. Honeybadger, the error monitoring service, has created a cute little webpage called exceptionalcreatures.com, where they've basically illustrated and characterized various common Ruby errors into little monsters [laughs], and I find them adorable. I think their goal is also to make it a really helpful resource for people encountering these kinds of errors, learning about them for the first time, and figuring how to triage or debug them. And I just think it's a really cool way of, like, making it super approachable, debugging and, you know, when you first encounter a scary error message, can be really overwhelming, and then Googling about it can also be equally [chuckles] overwhelming. So, I just really liked the whimsy that they kind of injected into something that could be really hard to learn about. Like, there are so many different error messages in Ruby and in Rails and whatever other libraries you're using. And so, that's kind of a...I think they've created a one-stop shop for, you know, figuring out how to move forward with common errors. And I also like that it's a bit of a collective effort. They're calling it, like, a bestiary for all the little creatures [laughs] that they've discovered. And I think you can, like, submit your own favorite Ruby error and any guidance you might have for someone trying to debug it. JOËL: That's adorable. It reminds me a little bit of HTTP status codes as cat memes site. It has that same energy. One thing that I think is really interesting is that because it's Honeybadger, they have stats on, like, frequency of these errors, and a lot of these ones are tied to...I think they're picking some of the most commonly surfaced errors. STEPHANIE: Yeah, there's little, like, ratings, too, for how frequently they occur, kind of just like, I don't know, Pokémon [laughs] [inaudible 02:31]. I think it's really neat that they're using something like a learning from their business or maybe even some, like, proprietary information and sharing it with the world so that we can learn from it. JOËL: I think one thing that's worth specifying as well is that these are specific exception classes that get raised. So, they're not just, like, random error strings that you see in the wild. They don't often have a whole lot of documentation around them, so it's nice to see a dedicated page for each and a little bit of maybe how this is used in the real world versus maybe how they were designed to be used. Maybe there's a line or two in the docs about, you know, core Ruby when a NoMethodError should be raised. How does NoMethodError actually get used, you know, in real life, and the exceptions that Honeybadger is capturing. That's really interesting to see. STEPHANIE: Yeah, I like how each page for the exception class, and I'm glad you made that distinction, is kind of, like, crowdsourced guidance and information from the community, so I think you could even, you know, contribute to it if you wanted. But yeah, just a fun, little website to bring you some delight when you're on your next head-smacking, debugging adventure [laughs]. JOËL: And I love that it brings some joy to the topic, but, honestly, I think it's a pretty good reference. I could see myself linking to this anytime I want to have a deeper discussion on exceptions. So, maybe there's a code review, and maybe I want to suggest that we raise a different error than the one that we're doing. I could see myself in that GitHub comment being like, "Oh, instead of, you know, raising an exception here, why don't we instead raise a NoMethodError or something like that?" And then link to the bestiary page. STEPHANIE: So, Joël, what's new in your world? JOËL: So, just recently, RailsConf announced their call for proposals. It's a fairly short period this year, only about three-ish weeks long. So, I've been really encouraging colleagues to submit and trying to be a resource for people who are interested in speaking at conferences. We did a Q&A session with a fellow thoughtboter, Aji Slater, who's also a former RailsConf speaker, about what makes for a good talk, what is it like to submit to a call for proposals, you know, kind of everything from the process from having an idea all the way to stage presence and delivering. And there's a lot of great questions that got asked and some good discussion that happened there. STEPHANIE: Nice. Yeah, I think I have noticed that you are doing a lot more to help, especially first-time speakers give their first conference talk this year. And I'm wondering if there's anything you've learned or any hopes and dreams you have for kind of the amount of time you're investing into supporting others. JOËL: What I'd like to see is a lot of people submitting proposals; that's always a great thing. And, a proposal, even if it doesn't get accepted, is a thing that you can resubmit. And so, having gone through the effort of building a proposal and especially getting it maybe peer-reviewed by some colleagues to polish your idea, I think is already just a really great exercise, and it's one that you can shop around. It's one that you can maybe convert into a blog post if you need to. You can convert that into some kind of podcast appearance. So, I think it's a great way to take an idea you're excited about and focus it, even if you can't get into RailsConf. STEPHANIE: I really like that metric for success. It reminds me of a writer friend I have who actually was a guest on the show, Nicole Zhu. She submits a lot of short stories to magazines and applications to writing fellowships, and she celebrates every rejection. I think at the end of the year, she, like, celebrates herself for having received, you know, like, 15 rejections or something that year because that meant that she just went for it and, you know, did the hard part of doing the work, putting yourself out there. And that is just as important, you know, if not more than whatever achievement or goal or the idea of having something accepted. JOËL: Yeah, I have to admit; rejection hurts. It's not a fun thing to go through. But I think even if you sort of make it to that final stage of having written a proposal and it gets rejected, you get a lot of value out of that journey sort of regardless of whether you get accepted or not. So, I encourage more people to do that. To any of our listeners who are interested, the RailsConf call for proposals goes through February 13th, 2024. So, if you are listening before then and are inspired, I recommend submitting. If you're unsure of what makes for a good CFP, RailsConf is currently offering coaching sessions to help craft better proposals. They have one on February 5th, one on February 6th, and one on February 7th, so those are also options to look into if this is maybe your first time and you're not sure. There's a signup form. We'll link to it in the show notes. STEPHANIE: So, another update I have that I'm excited to get into for the rest of the episode is my recent work on our support and maintenance team, which I've talked about on the show before. But for any listeners who don't know, it's a kind of sub-team at thoughtbot that is focused on helping maintain multiple client projects at a time. But, at this point, you know, there's not as much active feature development, but the work is focused on keeping the codebase up to date, making any dependency upgrades, fixing any bugs that come up, and general support. So, clients have a team to kind of address those things as they come up. And when I had last talked about it on the podcast, I was really excited because it was a bit of a different way of working. I felt like it was very novel to be, you know, have a lot of different projects and domains to be getting into. And knowing that I was working on this team, like, short-term and, you know, it may not be me in the future continuing what I might have started during my rotation, I thought it was really interesting to be optimizing towards, like, completion of a task. And that had kind of changed my workflow a bit and my process. JOËL: So, now that you've been doing work on the support and maintenance team for a while and you've kind of maybe gotten more comfortable with it, how are you generally feeling about this idea of sort of jumping into new codebases all the time? STEPHANIE: It is both fun and more challenging than I thought it would be. I tend to actually really enjoy that period of joining a new team or a project and exploring, you know, a codebase and getting up to speed, and that's something that we do a lot as consultants. But I think I started to realize that it's a bit of a tricky balance to figure out how much time should I be spending understanding what this codebase is doing? Like, how much of the application do I need to be understanding, and how much poking around should I be doing before just trying to get started on my first task, the first starter ticket that I'm given? There's a bit of a balance there because, on one hand, you could just immediately start on the task and kind of just, you know, have your blinders [chuckles] on and not really care too much about what the rest of the code is doing outside of the change that you're trying to make. But that also means that you don't have that context of why certain things are the way they are. Maybe, like, the way that you want to be building something actually won't work because of some unexpected complexity with the app. So, I think there, you know, needs to be time spent digging around a little bit, but then you could also be digging around for a long time [chuckles] before you feel like, okay, I finally have enough understanding of this new codebase to, like, build a feature exactly how a seasoned developer on the team might. JOËL: I imagine that probably varies a little bit based on the task that you're doing. So, something like, oh, we want to upgrade this codebase to Ruby 3.3, probably requires you to have a very different understanding of the codebase than there's a bug where submitting a comment double posts it, and you have to dig into that. Both of those require you to understand the application on very different levels and kind of understand different mental models of what the app is doing. STEPHANIE: Yeah, absolutely. That's a really good point that it can depend on what you are first asked to work on. And, in fact, I actually think that is a good guidepost for where you should be looking because you could develop a mental model that is just completely unrelated [chuckles] to what you're asked to do. And so, I suppose that is, you know, usually a good place to start, at least is like, okay, I have this first task, and there's some understanding and acceptance that, like, the more you work on this codebase, the more you'll explore and discover other parts of it, and that can be on a need to know kind of basis. JOËL: So, I'm thinking that if you are doing something like a Ruby upgrade or even a Rails upgrade, a lot of what you care about the app is going to be on a more mechanical level. So, you want to know what gems you're using. You want to know what different patterns are being used, maybe how callbacks are happening, any particular features that are version-specific that are being used, things like that. Whereas if you're, you know, say, fixing a bug, you might care a lot more about some of the product-level concerns. What are we actually trying to do here? What is the expected user experience? How does this deviate from that? What were the underlying mental models of the developers? So, there's almost, like, two lenses you can look at the code. Now, I almost want to make this a two-dimensional thing, where you can look at it either from, like, a very kind of mechanical lens or a product lens in one axis. And then, on the other axis, you could look at it from a very high-level 10,000-foot view and maybe zoom in a little bit where you need, versus a very localized view; here's where the bug is happening on this page, and then sort of zoom out as necessary. And I could see different sorts of tasks falling in different quadrants there of, do I need a more mechanical view? Do I need a more product-focused view? And do I need to be looking locally versus globally? STEPHANIE: Wow. I can't believe you just created a Cartesian graph [laughs] for this problem on the fly. But I love it because I do think that actually lines up with different strategies I've taken before. It's like, how much do you even look at the code before deciding that you can't really get a good picture of it, of what the product is, without just poking around from the app itself? I actually think that I tend to start from the code. Like, maybe I'll see a screenshot that someone has shared of the app, you know, like a bug or something that they want me to fix, and then looking for that text in the code first, and then trying to kind of follow that path, whereas it's also, you know, perfectly viable to try to see the app being used in production, or staging, or something first to get a better understanding of some of the business problems it's trying to solve. JOËL: When you jump into a new codebase, do you sort of consciously take the time to plan your approach or sort of think about, like, how much knowledge of this new codebase do I need before I can, like, actually look at the problem at hand? STEPHANIE: Ooh, that's kind of a hard question to answer because I think my experience has told me enough times that it's never what I think it's going to [laughs] be, not never, but it frequently surprises me. It has surprised me enough times that it's kind of hard to know off the bat because it's not...as much as we work in frameworks that have opinions and conventions, a lot of the work that happens is understanding how this particular codebase and team does things and then having to maybe shift or adjust from there. So, I think I don't do a lot of planning. I don't really have an idea about how much time it'll take me because I can't really know until I dive in a little bit. So, that is usually my first instinct, even if someone is wanting to, like, talk to me about an approach or be, like, "Hey, like, how long do you think this might take based on your experience as a consultant?" This is my first task. Oftentimes, I really can't say until I've had a little bit of downtime to, in some ways, like, acquire the knowledge [chuckles] to figure that out or answer that question. JOËL: How much knowledge do you like to get upfront about an app before you dive into actually doing the task at hand? Are there any things, like, when you get access to a new codebase, that you'll always want to look at to get a sense of the project before you look at any tickets? STEPHANIE: I actually start at the model level. Usually, I am curious about what kinds of objects we're working with. In fact, I think that is really helpful for me. They're like building blocks, in order for me to, like, conceptually understand this world that's being represented by a codebase. And I kind of either go outwards or inwards from there. Usually, if there's a model that is, like, calling to me as like, oh, I'll probably need to interact with, then I'll go and seek out, like, where that model is created, maybe through controllers, maybe through background jobs, or something like that, and start to piece together entry points into the application. I find that helpful because a lot of the times, it can be hard to know whether certain pages or routes are even used at all anymore. They could just be dead code and could be a bit misleading. I've certainly been misled [chuckles] more than once. And so, I think if I'm able to pull out the main domain objects that I notice in a ticket or just hear people talk about on the team, that's usually where I gravitate towards first. What about you? Do you have a place you like to start when it comes to exploring a new codebase like that? JOËL: The routes file is always a good sort of overview of, like, what is going on in the app. Scanning the models directory is also a great start in a Rails app to get a sense of what is this app about? What are the core nouns in our vocabulary? Another thing that's good to look for in a codebase is what are the big types of patterns that they tend to use? The Rails ecosystem goes through fads, and, over time, different patterns will be more popular than others. And so, it's often useful to see, oh, is this an app where everything happens in service objects, or is this an app that likes to rely on view components to render their views? Things like that. Once you get a sense of that, you get a little bit of a better sense of how things are architected beyond just the basic MVC. STEPHANIE: I like that you mentioned fads because I think I can definitely tell, you know, how modern an app is or kind of where it might be stuck in time [chuckles] a little bit based on those patterns and libraries that it's heavily utilizing, which I actually find to be an interesting and kind of challenging position to be in because how do you approach making changes to a codebase that is using a lot of patterns or styles from back in the day? Would you continue following those same patterns, or do you feel motivated to introduce something new or kind of what might be trendy now? JOËL: This is the boring answer, but it's almost never worth it to, like, rewrite the codebase just to use a new pattern. Just introducing the new pattern in some of the new things means there are now two patterns. That's also not a great outcome for the team. So, without some other compelling reason, I default to using the established patterns. STEPHANIE: Even if it's something you don't like? JOËL: Yes. I'm not a huge fan of service objects, but I work in plenty of codebases that have them, and so where it makes sense, I will use service objects there. Service objects are not mutually exclusive with other things, and so sometimes it might make sense to say, look, I don't feel like I can justify a service object here. I'll do this logic in a view, or maybe I'll pull this out into some other object that's not a service object and that can live alongside nicely. But I'm not necessarily introducing a new pattern. I'm just deciding that this particular extraction might not necessarily need a service object. STEPHANIE: That's an interesting way to describe it, not as a pattern, but as kind of, like, choosing not to use the existing [chuckles] pattern. But that doesn't mean, like, totally shifting the architecture or even how you're asking other people to understand the codebase. And I think I'm in agreement. I'm actually a bit of a follower, too, [laughs], where I want to, I don't know, just make things match a little bit with what's already been created, follow that style. That becomes pretty important to me when integrating with a team in a codebase. But I actually think that, you know, when you are calibrating to a codebase, you're in a position where you don't have all that baggage and history about how things need to be. And maybe you might be empowered to have a little bit more freedom to question existing patterns or bring some new ideas to the team to, hopefully, like, help the code evolve. I think that's something that I struggle with sometimes is feeling compelled to follow what came before me and also wanting to introduce some new things just to see what the team might think about them. JOËL: A lot of that can vary depending on what is the pattern you want to introduce and sort of what your role is going to be on that team. But that is something that's nice about someone new coming onto a project. They haven't just sort of accepted that things are the way they are, especially for things that the team already doesn't like but doesn't feel like they have the energy to do anything better about it. So, maybe you're in a codebase where there's a ton of Ruby code in your ERB templates, and it's not really a pattern that you're following. It's just a thing that's there. It's been sort of the path of least resistance for a long time, and it's easier to add more lines in there, but nobody likes it. New person joins the team, and their naive exuberance is just like, "We can fix it. We can make it better." And maybe that's, you know, going back and rewriting all of your views. That's probably not the best use of their time. But it could be maybe the first time they have to touch one of these views, cleaning up that one and starting a conversation among the team. "Hey, here are some patterns that we might like to clean up some of these views instead," or "Here are maybe some guidelines for anything new that we write that we want to do to keep our views clean," and sort of start moving the needle in a positive direction. STEPHANIE: I like the idea of moving the needle. Even though I tend to not want to stir the pot with any big changes, one thing that I do find myself doing is in a couple of places in the specs, just trying to refactor a bit away from using lets. There were some kind of forward-thinking decisions made before when RSpec was basically going to deprecate using the describe block without prepending it with their module, so just kind of throwing that in there whenever I would touch a spec and asking other people to do the same. And then, recently, one kind of, like, small syntax thing that I hadn't seen before, and maybe this is just because of the age of the codebases in which I'm working, the argument forwarding syntax in Ruby that has been new, I mean, it's like not totally new anymore [laughs], but throwing that in there a little every now and then to just kind of shift away from this, you know, dated version of the code kind of towards things that other people are seeing and in newer projects. JOËL: I love harnessing that energy of being new on a project and wanting to make things better. How do you avoid just being, you know, that developer, though, that's new, comes in, and just wants to change everything for the sake of change or for your own personal opinions and just kind of moves things around, stirs the pot, but doesn't really contribute anything net positive to the team? Because I've definitely seen that as well, and that's not a good first contribution or, you know, contribution in general as a newer team member. How do we avoid being that person while still capitalizing on that energy of being someone new and wanting to make a positive impact? STEPHANIE: Yeah, that's a great point, and I kind of alluded to this earlier when I asked, like, oh, like, even if you don't like an existing syntax or pattern you'll still follow it? And I think liking something a different way is not a good enough reason [chuckles]. But if you are able to have a good reason, like I mentioned with the RSpec prepending, you know, it didn't need to happen now, but if we would hope to upgrade that gem eventually, then yeah, that was a good reason to make that change as opposed to just purely aesthetic [laughs]. JOËL: That's one where there is pretty much a single right answer to. If you plan to keep staying up to date with versions of RSpec, you will eventually need to do all these code changes because, you know, they're deprecating the old way. Getting ahead of that gradually as we touch spec files, there's kind of no downside to it. STEPHANIE: That's true, though maybe there is a person who exists out there who's like, "I love this old version of RSpec, and I will die on this hill that we have to stay on [laughs] it." But I also think that I have preferences, but I'm not so attached to them. Ideally, you know, what I would love to receive is just, like, curiosity about like, "Oh, like, why did you make this change?" And just kind of share my reasoning. And sometimes in that process, I realize, you know, I don't have a great reason, and I'll just say, "I don't have a great reason. This is just the way I like it. But if it doesn't work for you, like, tell me, and I'll consider changing it back. [chuckles]" JOËL: Maybe that's where there's a lot of benefit is the sort of curiosity on the part of the existing team and sort of openness to both learn about existing practices but also share about different practices from the new teammate. And maybe that's you're coming in, and you have a different style where you like to write tests, maybe without using RSpec's let syntax; the team is using it. Maybe you can have a conversation with the team. It's almost certainly not worth it for you to go and rewrite the entire test suite to not use let and be like, "Hey, first PR. I made your test better." STEPHANIE: Hundreds of files changed, thousands [laughs] of lines of code. I think that's actually a good segue into the question of how can a team support a new hire or a new developer who is still calibrating to a codebase? I think I'm curious about this being different from onboarding because, you know, there are a lot of things that we already kind of expect to give some extra time and leeway for someone who's new coming in. But what might be some ways to support a new developer that are less well known? JOËL: One that I really like is getting them involved as early as possible in code review because then they get to see the patterns that are coming in, and they can be involved in conversations on those. The first PR you're reviewing, and you see a bunch of tests leaning heavily on let, and maybe you ask a question, "Is this a pattern that we're following in this codebase? Did we have a particular motivation for why we chose this?" And, you know, and you don't want to do it in a sort of, like, passive-aggressive way because you're trying to push something else. It has to come from a place of genuine curiosity, but you're allowing the new teammate to both see a lot of the existing patterns kind of in very quick succession because you see a pretty good cross-section of those when you review code. And also, to have conversations about them, to ask anything like, "Oh, that's unusual. I didn't know we were doing that." Or, "Hey, is this a pattern that we're doing kind of just local to this subsystem, or is this something that's happening all the way? Is this a pattern that we're using and liking? Is this a thing that we were doing five years ago that we're phasing out, but there's still a few of them left?" Those are all, I think, great questions to ask when you're getting started. STEPHANIE: That makes a lot of sense. It's different from saying, "This is how we do things here," and expecting them to adapt or, you know, change to fit into that style or culture, and being open to letting it evolve based on the new team, the new people on the team and what they might be bringing to the table. I like to ask the question, "What do you need to know?" Or "What do you need to be successful?" as opposed to telling them what I think they need [laughs]. I think that is something that I actually kind of recently, not regret exactly, but I was kind of helping out some folks who were going to be joining the team and just trying to, like, shove all this information down their throats and be like, "Oh, and watch out for these gotchas. And this app uses a lot of callbacks, and they're really complex." And I think I was maybe coloring their [chuckles] experience a little bit and expecting them to be able to drink from the fire hose, as opposed to trusting that they can see for themselves, you know, like, what is going on, and form opinions about it, and ask questions that will support them in whatever they are looking to do. When we talked earlier about the four different quadrants, like, the kind of information they need to know will differ based off of their task, based off of their experience. So, that's one way that I am thinking about to, like, make space for a new developer to help shape that culture, rather than insisting that things are the way they are. JOËL: It can be a fine balance where you want to be open to change while also you have to remain kind of ruthlessly pragmatic about the fact that change can be expensive. And so, a lot of changes you need to be justified, and you don't want to just be rewriting your patterns for every new employee or, you know, just to follow the latest trends because we've seen a lot of trends come and go in the Rails ecosystem, and getting on all of them is just not worth our time. STEPHANIE: And that's the hard truth of there's always trade-offs [laughs] in software development, isn't that right? JOËL: It sure is. You can't always chase the newest shiny, as fun as that is. STEPHANIE: On that note, shall we wrap up? JOËL: Let's wrap up. STEPHANIE: Show notes for this episode can be found at bikeshed.fm. JOËL: This show has been produced and edited by Mandy Moore. STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show. JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter. STEPHANIE: Or reach both of us at hosts@bikeshed.fm via email. JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week. ALL: Byeeeeeeee!!!!!!! AD: Did you know thoughtbot has a referral program? If you introduce us to someone looking for a design or development partner, we will compensate you if they decide to work with us. More info on our website at: tbot.io/referral. Or you can email us at referrals@thoughtbot.com with any questions.Support The Bike Shed
Feb 5
30 min
414: Spike Tasks
Joël shares his recent experience with Turbo, a JavaScript framework that simplifies adding interactivity to websites without extensive JavaScript coding. Stephanie gives an update on her quest to work from her office more, and the birds have arrived—most notably, chickadees. Stephanie and Joël address a listener question from Edward about the concept of a "spike" in software development. They discuss the nature of spikes, emphasizing that they are typically throwaway work aimed at learning and de-risking rather than producing final code, and explore how spikes can lead to better decision-making and prioritization in software development, especially in complex codebases. Transcript: STEPHANIE:  Hello and welcome to another episode of The Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Stephanie Minn. JOËL: And I'm Joël Quenneville. And together, we're here to share a bit of what we've learned along the way. STEPHANIE: So, Joël, what's new in your world? JOËL: I'm pretty excited because this week, I actually got to use a little bit of Turbo for the first time. Turbo is Rails'...I guess it's not technically just for Rails. It's a sort of unobtrusive JavaScript framework that allows you to build a lot of interactive functionality without actually having to write a lot of JavaScript yourself, just by writing some HTML in a certain way. And you can add a lot of functionality and interactivity to your site without having to drop to custom writing some JavaScript. STEPHANIE: Cool. Yeah, that is exciting. I personally have not gotten to use too much of it in a production/client setting; only played around with it a little bit on my own to keep up with what's new and just kind of reading about how other people are excited to use it. So, what are your first impressions so far? JOËL: It's pretty nice. It, you know, works as advertised. My situation, I was rendering a calendar view of a lot of events, and this is completely server-rendered. And I realized, wait a minute, there are some days where I've got, like, 20 events, and I really, like, I want my calendar squares to say sort of equally sized. So, I wanted to limit myself to only showing four or five events per calendar day. And so, I added a little link at the bottom of the calendar day that says, you know, "See more." And when you click that link, it does some Turbo stuff, and it pulls in other events so that you can now sort of expand it to get the whole day. So, it's just a little bit of interactivity that you kind of get for free with Turbo just by wrapping a particular HTML tag around it and having the Turbo library loaded. STEPHANIE: That's cool. I'm excited to try it out next time I'm working on a Rails project that just needs a little bit of that interactivity, you know, just to make that experience a little bit richer. And it seems like a really good, like, low-effort way to add some of those enhancements. Based on what you described, it sounds really easy. JOËL: Yeah, I was impressed with just how low effort it all was, which is what you want, right? It works out of the box. So, for anyone who's kind of curious about it, Turbo Frames is the little bit that I used, and it worked really well. Oh, something I'm actually excited about it as well; it plays nicely with clients that have disabled JavaScript. So, this link that I click to pull in the rest of the events, if somebody has JavaScript disabled, or if they command-click or control-click to open in a new tab, it doesn't just do nothing like it would often do in many sort of front-end framework-y places that have hijacked the URL click handler. Instead, it actually opens up the full list of items in a new page, just as if you'd clicked a normal link. So, it really gives you that progressive enhancement feel where I can click a link, and it goes to another page with a list of all the 30 events if I don't have JavaScript. But if I do, maybe I get a slightly better experience where, instead of taking me to a new page, it just expands the list, and I get to see the full list. So, it plays nicely on both sides. STEPHANIE: That's really cool. As someone who's just starting to dabble in some alternative browsers outside of the main popular ones [chuckles], I have noticed how many websites do not work for me anymore [laughs]. And that sounds, like, nice from a user perspective. JOËL: So, other than dabbling with the new browsers, what's new in your world? STEPHANIE: A few weeks ago, I talked about [laughs] sitting more at my desk and, you know, various incentives that I gave myself to do that. And I'd like to say that I've been doing a pretty good job [laughs]. So, what's new in my world is that I've followed up on my commitment to sit at my desk more, feel a little bit more organized in my workday. And that's especially true because the birds have finally discovered my bird feeder [laughs]. JOËL: Oh, that's really cool. STEPHANIE: There were a few weeks where I was not really getting any visitors, and, you know, I was just like, when are they going to come and eat this delicious birdseed that I've [laughs] put out for them? And it seems like a flock of chickadees that normally like to hang out on the apple tree in my backyard have figured out this new source of food, and they'll sometimes, five of them at a time, will come, and sometimes they even fight [laughs] to get on the ledge to hang out at the bird feeder. And yeah, it turns out that the six pounds of bird feed that I bought, I'll start to turn through [laughs] that a little bit quicker now, so I'm excited about that and just to also see other birds and species come and go as time goes on. So, that's been an exciting new development. JOËL: So, the six pounds of birdseed might not last you through the winter. STEPHANIE: I was debating between six pounds and, like, a 20-pound bag [laughs], which that would have been a lot. And so far, I think the six pounds has been serving me well. We'll see how long it lasts, but yeah, it's finally starting. I might have to refill it soon, so, you know, I was hopefully not going to have to store all that bird feed [laughs] just, like, in my house for a long time. JOËL: Any birds that have shown up that have been particularly fun to watch or that are maybe your favorites? STEPHANIE: I mentioned the chickadees because they seem to come as a group, and I really like watching them interact with each other. It's just kind of like bird TV, you know, it's not just a single bird. It's just watching these animals that are a collective do their thing. And I've been enjoying that a lot. JOËL: Now I'm just imagining a reality TV but the Chickadee edition. STEPHANIE: Oh yeah, definitely. I know some people put, like, cameras at their bird feeders to either live stream, which is funny because most of the time, there's nothing happening [laughs]. Usually, the birds are really in and out. Or they'll have, like, a really fancy camera to take, like, really beautiful up-close photos. There's a blog that I discovered recently where someone posts about the birds that visit them at their place in Michigan. I'll link to it in the show notes, but it's really cool to see these, like, up-close and personal photos of basically the bird's mouth. Sometimes, they're open [laughs], so you can see right in them. I don't know; maybe there's a time where I'll get so into it that I'll create my own bird feeder blog. JOËL: Well, if you do, you should definitely share it with the listeners on the podcast. Speaking of listeners on the podcast, we've recently had a listener question from Edward that I thought was a really interesting topic, and I wanted to take a whole episode to dig into. And Edward asks about the concept of a spike. Sometimes, we're asked to investigate a complex new feature, and you might want to do some evaluation on the feasibility and complexity and build out just enough of it to make a well-informed opinion. And ideally, you're doing that in a way that reduces risk of spending too much time with unproven impact. The problem is that in any reasonably complex codebase, that investigation work can be most of the work needed to build the feature. And Edward gives an example: if you're adding a system admin role, the core of the work is adding a new role with all of the abilities, but the real work is ensuring that it interacts with the entire system in the appropriate way. So, how do you manage making sure that you're doing spikes well? And Edward asks if this is something that we've experienced a sort of feeling that we're doing 90% of the work in the spike. He also asks, does this say something about the codebase that you're working on? If it's hard to spike in it, does that say something about the underlying codebase, or are we just all doing spikes wrong? So yeah, I'm curious, Stephanie: do you occasionally spike things out in code on your projects? STEPHANIE: Yeah, I do. I think one piece that was left a little bit unsaid is that I think spiking usually comes up when the team can't really estimate how long a task will take, you know, assuming that you use estimates on your team [chuckles]. That calls for a spike ticket, right? And someone will spend some time. And I think on some teams, this is usually time-boxed as well to maybe do a proof of concept or, yeah, do some of that initial exploration. JOËL: Before we go too deep, I think it's probably useful to define spike in that I think it's a little bit easy and probably varies from team to team and even from a developer to developer. I think, for me, when I think of a spike, it's throwaway work. The code that I write will not get shipped, and this is not code that will just get improved later. It is entirely throwaway work. And the purpose of it is to learn something about the project that's being done. Typically, it's in a sort of de-risking fashion, so to say, look, we've got a feature that's got a lot of unknowns in it. And if we commit to it right now or we start investing time into it, it could become a bit of a time pit. Let's try to answer some questions about it. Let's try to resolve some of those unknowns so that we can better make decisions around maybe estimation, but maybe even just prioritization. If this seems like something that would be really challenging to do, maybe we don't want to prioritize it this quarter. Is that similar to how you think of spikes, or do you have a different sort of definition of it? STEPHANIE: Yeah, I am glad you mentioned that it's throwaway work. I think I was a little hesitant to commit to that definition with conviction because even based off of what Edward was saying, there's kind of, like, maybe different ideas about that or different expectations. But I sometimes think that, depending, spiking doesn't even necessarily need to lead to code. Like, it could just be answering questions. And so, at the same time, I think it is, I like what you said, work that helps you learn more about the system, whether or not there's some code written as, like, a potential path at the end of it. JOËL: Interesting. So, you would put some things that don't involve code at all in the spike bucket. STEPHANIE: I think there have been times where I've done a spike, and I've not coded out anything, but I've answered some questions, and I've left comments about unearthing some of the uncertainty that led us to want to explore the idea in the first place. Then, again, I also have gone down the path of, like, trying out a solution and maybe even multiple and then evaluating afterwards which ones I think were more suitable. So, it could mean both. I think that is actually something that's within the power of whoever is assigned this work to determine whatever is valuable to them in order to get enough information to figure out how you want to move forward. JOËL: Another element of spikes that I think is often implied is that because this is throwaway work, you're not necessarily putting in all the work to make everything sort of clean, or well-structured, or reusable, or anything like that. So, it's quite possible that you would not even test this. You might not break this out into objects in the way that you would if this had to be reused. You might have duplication all over, and that's okay because the purpose of this code is not to be sort of production-grade; it's to answer some questions, and then you're going to throw it away and, using those answers, build something correctly. STEPHANIE: Yeah, I think that's true. And it's kind of an interesting distinction from, you know, what you might consider your regular work in which the expectation is that it will be shipped [chuckles]. And there's also some amount of conflating the two, I think because if, you know, you and I are saying like, yeah, like, this exploration should be standalone, and it is not going to be used to be built on top of necessarily, there is some amount of revisiting. And you're not starting from scratch because you have an idea, but you are starting fresh if you will. And so, you know, when you are doing that spiking, I think it allows you to move a little bit faster, but that doesn't mean that the work is, like, any X percent [laughs] done at the end of it. JOËL: The work is still kind of, I guess, 0% done, again, because this is throwaway code, in our definition of a spike anyway. Would you distinguish between the terms spike and prototype? STEPHANIE: Oh, interesting. My initial reaction is that a prototype would then be user-tested [laughs] in some way. Like, the point is to then show someone and then get them interacting with it, any initial reactions from that. Whereas a spike is really for the developer and maybe the team to discuss. JOËL: I like that distinction. I definitely think that a spike, for me, is purely technical. We're not spiking out a feature by putting a thing live in production behind a feature flag, showing it to 10% of users, and seeing how they respond to that. That's not a spike. So, I think something a little bit more like that, or where you're showing things maybe to users, or you're wanting to do maybe some user testing with something. And it can be throwaway code still. I think now you're starting to get something more that you would call a prototype. So, I like that distinction of, is this sort of internal or external? But in the way they're used, they can often be similar, and that oftentimes both will sort of...they're built to be as cheap as possible to answer the questions you're trying to get answered, whether that's from a user or just technical reasons. And so, the whole thing can be a little bit of smoke and mirrors, a little bit of duct tape and toothpicks, as long as you only have...like, the only solid parts you need are the parts that are going to help you answer your question. And so, any hack or cheat you can get to to bypass everything else is time you've saved, and that's a good thing. STEPHANIE: Oh, I'm very curious about this idea of time saved because I think sometimes an underappreciated outcome of a spike is what not to do or is choosing not to do something. And it can feel not great to have spent hours or even days exploring a path just to realize that it's not worth it. I'm curious, like, when you know to stop and also, how you get other people kind of onboard that even just figuring out an initial idea was not a viable solution, how that could be a valuable insight to the rest of the team. JOËL: Something that I think can be really useful is before you even start spiking out something, write a list of questions that you're trying to answer with this code, and then don't let yourself get distracted. Write the minimum amount of code that will allow you to answer those questions. So, maybe that is a question around, is it possible to connect this external API to our systems? There are some questions around, like, how credentials and things will work or how complex that will be. It might be a question around, like, maybe there's even, like, a performance thing. We want to talk to an external system and, you know, the responses back need to be within a certain amount of time. Otherwise, this whole approach where we're going to try to fetch data live is not feasible. So, the answer we need there is, can we do it live, or do we need to consider some sort of background fetching, or caching approach, or something like that? So, write the minimum amount of code that it would take to do that. And maybe the minimum amount of code, like you said, is not even really code. Maybe it's a script or even just trying out some curl commands and timing them at the command line. It could be a lot of things. But I think having a list of questions up front really helps you focus on the purpose of the spike. And I think it helps me a little bit as well with emotional attachment in that success is not necessarily coming to a yes on all of those questions. It is having an answer, going from question mark to some answer. So, if I can answer that question, if I can find even a clever way to answer that question faster, that is success. I have done a good job with my spike. STEPHANIE: I like that a lot. I think some people might struggle with spikes because they're so ambiguous. And if it's just, like, explore this potential feature, or, like, maybe not even that, but even saying, like, we want to build this admin role, to use Edward's example. And to constrain it to how should we do that, it already kind of guides the spike in a certain direction that may or may not be exactly what you're looking for. And so, there's some value in figuring out what questions to ask with the product team, even to get alignment on what the purpose of this task is. And, you know, this is true of regular feature work, too. When those decisions have kind of already been made about what we're working on without a lot of input from developers who will be working on it, it can be really hard to, like, go back and be like, "Oh, actually, that's not really possible." But if the questions are like, "Is this possible?" or like, what it costs to do this, I think it prevents some of that friction and misalignment that might be had when the outcome of a spike turns out to be maybe not what someone wants to hear. JOËL: And I think the questions you ask don't necessarily have to be yes or no questions. They could be some sort of list, right? It could be, look, we're looking at two different implementations or two general approaches, families of solutions for our super admin role. What are the trade-offs of each? And so, a spike might be exploring. Can we come up with a list of pros and cons for each approach? And maybe some of them we just know from experience at developing, but maybe some of them might involve actually doing a little bit of work to play out the pros and cons. Maybe that's in our app. Maybe that's even spinning up a little app on the side, right? If we're comparing maybe two gems or something like that to see how we feel about throwing a few different scenarios and exploring edge cases. So, the questions don't need to be straight-up yes or no. So, you mentioned earlier the idea that sometimes one developer might do the spike, and then another one might do the actual work, maybe inspired by the answers that were on that spike. And I think that can lead to some really interesting dynamics, especially if the developer who did the first spike has done kind of, like, what Edward describes, what feels like 90% of the feature. It may be not so great code quality. And then this is a branch on GitHub, and they're like, "Okay, do the rest. Make it good. I've already explored the possibilities here," and then you're the developer who has to pick that up. Have you ever experienced that? And if so, how do you feel picking up a ticket like that? STEPHANIE: Yeah, I have experienced it, and I think there is always something lost when that happens when you are not the person who did the research. And then having to just go from whatever was left in the notes or from the code and, you know, I don't know how feasible it is for whoever spiked to always be doing the implementing, but I certainly end up having a lot of questions, I think. Like, you can't document or even code out, like, every single thing you learned in that process, right? There's always from big to small decisions or alternatives considered that won't make it into however that communication or expression or knowledge transfer happens. And I think the two choices that I have as a developer picking that up is either to just trust [laughs] that the work the other person did is taking me down a good path or to spend more time rebuilding some of that context and making some of my own evaluations along the way and deciding for myself whether I'm like, oh yeah, this is a good idea, or maybe, like, I might change something here. So, I think that there is some time lost, too. And I think that's a really good thing to point out when someone might think like, oh, this is mostly done. That's kind of my first reaction in terms of the context loss in an exchange like that. JOËL: Do you feel like this is a situation where you would want to have the same developer do both the spike and the final implementation? Or is this maybe a situation where spikes aren't being done correctly, and maybe a branch with some code that's kind of half-written is maybe the wrong artifact to hand off from one developer to the other? STEPHANIE: Oh, that's really interesting about if that's the wrong artifact to hand off because it could be misleading. Maybe it's not always, and maybe there's some really great code that comes out of it if someone builds on top of a work-in-progress branch or a spike branch. Honestly, I think, and I haven't even really gotten to experience this all too much because maybe there is some perception that it's backtracking or, you know, it's more work or more time, but it would be really cool for whoever had spiked it to then bring someone along to pair on it and start fresh, like we mentioned, where they're kind of coming to each decision to be made with an idea, but it's not necessarily set in stone, right? There could be that discussion. It could be, like, a generative experience to either refine that code that had initially been spiked out or discover new things along the way. It's not like the outcome has already been decided because of the spike. It is information, and that's that. JOËL: And we on this podcast are very pro-discovering new things along the way. I think sometimes as a developer, if I get sort of a, you know, maybe a 90% branch done that's get passed on to me from somebody else who did a spike, it feels a little bit like the finish the rest of the owl meme, except that now I'm not even, like, just trying to follow a tutorial. Just somebody did the first couple of circles and then is like, "Oh yeah, you finish the rest of the owl. I did the hard work. You just need to polish it up." On the one hand, it's like, dude, if you're, like, doing 90%, you may as well finish it. I don't want to just be polishing somebody else's work. And, you know, oftentimes, it might feel like it's done 90% of the time, but it's actually, like, there's a lot of edge cases and nuance that have not been handled. And, you know, a spike is meant to be throwaway work to start with. So, I felt like those sorts of handoffs often, I don't know, they don't sit with me well. STEPHANIE: Yeah. You could also come in and be like, this doesn't even look like an owl at all [laughs]. JOËL: I feel like maybe in my ideal world, a branch with partly written code is, I guess, an intermediate artifact that might be useful to show. But what I really want from a spike is answers to questions that will allow me, when I build the thing from scratch to make intelligent decisions. So, probably what I want out of a spike is something that's closer to documentation, a list of questions that we were asking, and then the answers we came to by doing the spike work. And that might be maybe a list of trade-offs, or maybe we didn't really know the correct endpoints from this undocumented API, and we tried some stuff, and we, like, figured out what endpoints we needed, or what the shape of the JSON payload needed to be, things like that. Maybe we tried a couple of different implementations, or we did some exploration around, like, what gem we'd like to use, and we have a recommendation for a gem. Those are all, I think, very concrete outcomes from a spike that I can then use when I'm building it from scratch. And I'm not just, like, branching off your branch or having it open in another browser and copy-pasting snippets while trying to, like, add some testing and maybe modularizing it a little bit. I think that leads to probably a better outcome for the person who's doing the spike because they have a tighter scope and also a better outcome for me, who's then trying to build that feature correctly from the ground up. I think that would be my sort of ideal workflow. STEPHANIE: While you were saying that, I thought about how a lot of those points sounded like requirements for a feature. And that, I think, is also a good outcome when a spike then leads to more concrete requirements because those are all decisions that were thought through, right? And even better is if that also documents things that were tried and the trade-offs that came with them or, like, the reasons why they were less viable or not ideal for that added context because that is also work that happened [laughs] and should be captured so someone can know that that might not be time they need to spend on that. I am really interested in one piece that we haven't quite touched on is the complexity of the app and what it means for spiking to be a challenge because of the complexity of the app. JOËL: Yeah. And I think sort of inherent in there is that maybe the idea that if you have a really complex app, it sort of forces you to go to the 90% of the work done in order to successfully answer the questions you wanted to answer with your spike in a way that maybe a better-structured app would not. Do you think that's true? STEPHANIE: Well, I actually think that if the app is complex, you're actually seeing that affect all parts of feature development, not just spiking, where everything takes longer [laughs] because you maybe feel less confident. You're nervous about breaking something. Edward called the real work ensuring that it interacts with the entire system correctly, and that's true of, I think, just software development in general. And so, I wonder if, you know, spiking happens to be one way that it manifests, but if there are signals that it's affecting, you know, all parts of your workflow. JOËL: There definitely is a cost, right? Complex software imposes costs everywhere. In some way, I think maybe spiking is attempting to get around some of those, in that there are some decisions that we can just say, you know what? We'll build the feature, and we'll just kind of figure it out as we go along, and we'll, like, build the thing. Spiking attempts to say, look, let's not build the whole thing. Let's fake out a bunch of parts because, really, we have a big question that we want to answer about a thing that is three steps down, you know. And maybe the question is, look, we're trying to build the super admin role, and we know it's got all these, like, edge cases we need to deal with. Maybe we need a list of the edge cases, and maybe that's how we, like, try to drive them out. But maybe this is a, hey, do we want to go with more of a, like, a role hierarchy inheritance-based approach, or do we want to go with some sort of escalating defaults? Or whatever the couple of different strategies you might want to do. And the spike might be trying to answer the question, how can we, as cheaply as possible while doing the minimum amount of work, sort of explore which of these implementations works best? And in a complex system, is it possible to get to the answer to those questions without building out 90% of the feature itself? I think, going to what you said, you might have to do more work if it's a complex system. But I would also encourage everyone to go absolute minimalist, like, keep your goal in mind: what is the question you're trying to answer? And then ruthlessly cut everything you need to get to your point where you can answer that question. Do you need to hard code? Do you need to metaprogram? Do you need to do just, like, the worst, dirtiest code that you've ever written? That's okay because, like, the implementation does not matter. The fact that you're not exercising the full system does not matter, as long as the part that you're trying to exercise and answer your questions does get used. STEPHANIE: Yeah, I like that a lot. And I wonder if the impulse to want to spike something is coming out of nervousness about how complicated the ask is. And it's like, well, I don't want to tell you that it's going to take a long time because this app is extremely complex, and everything takes a long time. You know, it's like not wanting to face that hard question of either we need to just set our expectations that things take longer, or we need to make some kind of change to make that easier to work with. And that is a lot of thought and effort. And so, it's kind of an answer to be like, well, like, let me spike this out and then see [laughs]. And so it may be a way to appease someone making a request for a feature. I don't know; I'm perhaps projecting a little bit here [chuckles]. But it could also be an important question to ask yourself if you find your team, like, needing to lean on spikes a lot because you just don't know. JOËL: That's really interesting because I think that maybe connects to a recent episode we did on breaking features down into smaller chunks. Spikes can often manifest, or the need for a spike can often manifest when you've got a larger, less well-defined feature that you want to do. So, sometimes, breaking things into smaller pieces will help you have something that's a little bit more well-defined that you feel confident jumping into without doing a spike. Or maybe the act of trying to split this sort of large, undefined task into smaller pieces will reveal questions that need to be answered and say, look, I don't know where the seam should be, where to split this task because I don't know the answer to this one question. If I could know the answer to this one question, I would know where to split this feature. That's your spike right there. Do the minimum amount of code to answer that one question, and then you can split your feature and confidently work on the two smaller pieces. And I think that's a win for everyone. STEPHANIE: Yeah. And you can listen back to our vertical slice episode [laughs] for some inspiration on that. JOËL: On that note, shall we wrap up? STEPHANIE: Let's wrap up. Show notes for this episode can be found at bikeshed.fm. JOËL: This show has been produced and edited by Mandy Moore. STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show. JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter. STEPHANIE: Or reach both of us at hosts@bikeshed.fm via email. JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week. ALL: Byeeeeeeeee!!!!!!! AD: Did you know thoughtbot has a referral program? If you introduce us to someone looking for a design or development partner, we will compensate you if they decide to work with us. More info on our website at: tbot.io/referral. Or you can email us at referrals@thoughtbot.com with any questions.Support The Bike Shed
Jan 29
31 min
Load more