Crafting Code Podcast

~/podcast

$ cd episodes/041-code-design

~/podcast/episodes/041-code-design $ ls -1a
. .. episode-summary.txt references.txt themes.txt transcript.txt get-mp3.sh
~/podcast/episodes/041-code-design $ cat episode-summary.txt

Should code design be something we should have in mind when starting, or should we allow it to emerge from a test-driven design process? How much mileage can we get by getting better the up front design process before we run into a Design Quality Paradox? Can coupling and cohesion guide us to how small we ought to break up a function? Are code comments important to design or something to be avoided? In this episode Allan and Dave react to a written debate between John Ousterhout and Uncle Bob Martin. You can read the debate at https://github.com/johnousterhout/aposd-vs-clean-code/blob/main/README.md

~/podcast/episodes/041-code-design $ cat references.txt ~/podcast/episodes/041-code-design $ cat themes.txt ~/podcast/episodes/041-code-design
$ cat transcript.txt

[00:00:16] Allan Stewart: Welcome to the Crafting Code Podcast, where we discuss the importance of doing the right thing at the right time with the right tools. I'm Allan Stewart, a software architect, and lately I've been thinking about the importance of finding the good that you can learn from imperfect humans who also make mistakes or believe differently than you.

[00:00:34] Dave Adsit: I'm Dave Adsit, a VP of engineering, and recently I've been thinking about the value in unstringing the bow. In other words, taking time to focus on something important other than work for a while.

[00:00:47] Allan Stewart: Our topic for this episode is code design. This came up for us because recently a friend sent us a link to a summary. Some discussions that went back and forth between John Ousterhout, not sure if that's how you say his name, but he's the author of A Philosophy of Software Design, and Uncle Bob, or Robert Martin, the author of Clean Code. And this prompted us to think about code design. And I haven't read A Philosophy of Software Design yet. It somehow didn't show up on my radar, but it's now in my list of books that eventually I get to. That same friend did say that he read it, and it was very enjoyable because John clearly cares about code. Uncle Bob cares a lot about code. And we all have our disagreements about some of the things, but this discussion back and forth, which we'll link to in the show notes, got us really thinking about code design and what matters with code design.

[00:01:54] Dave Adsit: Yeah. So I was going to say... One of the things that has become clear, I also have not read A Philosophy of Software Design yet, but I have read Clean Code many times, and I've mentioned that it was very impactful for me in my career. And it is, I guess, well-known or somewhat well-known that A Philosophy of Software Design is a not-so-subtle rebuttal to the concepts of clean code, which makes me wish that I had found it and taken the time to go through it. Oh. A while ago. It's been out for a few years now. But yeah, it's now also on my to-do list. Go through that book, become more familiar with those concepts. I find that it is important to understand a problem from multiple perspectives in order to truly understand it deeply.

[00:02:45] Allan Stewart: Absolutely. And certainly, we've mentioned in the past that there are aspects of clean code that we do not hold to or do not agree with. Despite our enjoyment or respect for the book as a whole. Right. So I think that the first thing that comes to my mind is, like, what is code design? And why does that matter?

[00:03:11] Dave Adsit: Well, I think that code design is, obviously, it's the way your system is put together. It's the set of principles and guidelines and decisions that you've made that lead to the overall system that you're working in. It's probably very similar to architecture. You know, we talk about architecture a lot. Design is maybe just another way of saying architecture. But when I talk about code design, I think a lot more about the habitability of the system versus when we're talking about architecture, I think more of, like, the components of the system. And maybe that's just the bias that I bring to the table. Right. I think when it comes to what is the design of this system, the first thing that comes to mind is how habitable is it? If I'm a developer working in it, in this system, what is that like?

[00:04:09] Allan Stewart: I think for me, design and architecture are very closely related, if not complete synonyms. But code design definitely does make me feel like it puts my mind into a context of thinking about some lower level code. Right. Maybe just a class and how it interoperates or just a couple of classes that interact rather than the higher level. Like the word architecture tends to make my brain think more up at a higher system level, whereas code design makes me think in a lower level. And I think those two things are highly related. And ultimately, all the details of what you did at the low level of code, you get the emergent property of the software. So I think that's where architecture at the high level. But yeah, when designing a class, I've definitely in the past come up with ideas. I would understand a problem. I would think about how is this feature supposed to work? How is this algorithm supposed to be written? And kind of build up a mental model of it. And not just a mental model of the thing itself, but how I want to build it. And then I'd go. Scurry off and write a whole bunch of code. But once I started getting into test driven development, it kind of reversed my thinking about that. And now it's very uncommon for me to work that way, to create the design first and then make the code match my mental model of that thing. But instead, I and people sometimes say that I'm a backwards thinker, but I found that TDD lets me describe the interface. How am I? How am I going to work with this piece of code? And what outcomes do I want to have happen? Like what? What result? What is it going to return? Is it going to interact with another piece? You know, I'm I'm definitely a mock test tester, so I will sometimes check those behaviors and say, hey, did it call the repository to save it into the database and verify those kinds of things? But I'm letting test driven design drive that implementation, and I no longer think about it in. Terms of this was my mental model, and now I'm putting that into the implementation. But it's much more of, well, what do I want it to do now? And however it affects the design of the code, I kind of just let it let it drive that process.

[00:06:43] Dave Adsit: Yeah, I do something very similar. I like to start with the interface of the of the thing first, and then I use my test to define how do I want to interact with this class? Um. And then I use tests to drive the internal implementation as well. And I again, I also use mocks extensively because I like to work with an object or a class in isolation from its containing ecosystem. When I'm testing things, I feel like I can have a more consistent I can write high quality code more consistently by isolating each class. Class. From the greater ecosystem, the greater architecture of the of the code base. And I think a lot about different ways of testing different layers of testing different and and I encourage my teams to do acceptance test driven development where they write an acceptance test and then unit tests. Um. Whether that happens every time or not, I would say probably not, but I do encourage that. Because that allows us to think about. The code at different layers, right? I'm thinking about it from a high level of abstraction. What is the overall feature I'm trying to build? And I'm thinking about it from a low level abstraction. What is this code trying to accomplish? I think one of the things that comes up a lot for me when it comes to design is in a design principles. I've worked in object oriented systems most of my career, and we've got object oriented design principles and design patterns that we use. You know, we talk a lot. About things like single response. Well, there's the solid principles, right? There's single responsibility, open close. Um, the Liskov substitution principle, inversion of control and dependency injection. Those are all basically. Some of the principles that allow us to create hot, what we consider high quality object oriented code. And so those are some of the design tools that I keep in mind as I go, or maybe they're not even tools. Just principles. Um, but I, I, I think about them as I am writing code. And so I want to use tools like TDD, which encourage me to create good. Oh, oh, code. So in my experience, when I think about it, the unit testing test driven development. Encourages the creation of classes that serve a single purpose. It helps. Me write code that follows the single responsibility principle. Where a lot of code that I've written without TDD. Has not followed that principle very well. One of the things that was discussed at length in the article with John and uncle Bob. Is that. A good developer can come to the same type of solution, regardless of what tools they're using. They could use test driven development or bundling, which bundling is kind of a. Was described as more of your traditional design it. Code it, test it type of a model. But smaller batches than.

[00:10:11] Allan Stewart: The system way of doing it.

[00:10:14] Dave Adsit: Yes. Much smaller batches than at the level of a system, more like the level of a class. Um, and definitely something that I've done. I. I would say that I've used. Test driven development test first and just writing unit tests. Sometimes after the fact interchangeably at different parts of my career. Sometimes I have a design in mind when I start. And often I don't. If we start from testing the way it was taught to me earlier in my career, where we would do, we would gather requirements, define solution architectures, right? Design the code. Then implement the. The code, write the code and then do the testing. When I follow that pattern of design at first. Test it last. I have often run into constraints around how the code was put together that make testing harder. Um, and then when it's hard. And it's going to take time and it's at the end of the process, I have seen many teams that I've worked on skip that step. Like, well, the code works. We know we've, we manually verified it and now we can put it out there. It's just hard to add. Unit tests. So we won't do it now. We'll come back and do it later. Is a, that's the kind of thing that I've heard said many times. I've probably said it myself from time to time. And it's often not true that you come back and write those tests later. So that, that my experience when doing design first test after is code that is not as easy to test and therefore less well-tested and therefore possibly most likely more prone to. Bugs and errors. When I've done test first, which for me, test first is the idea that I've done the design and then I write the tests and then I write the code. I find that I have a lot higher code coverage, but there's maybe a edge case or two that I test manually because I couldn't. My design didn't support testing it. Well, I've also found that I may tend to have more than one responsibility in a class. When I do the design first, because I wasn't thinking about it in quite the right way. When I do test first test driven development. Rather, I have found that if I'm doing it according to the rules, which are that you write a test. You write only enough tests for the next thing you write just enough code to make that test pass. And then you review the code, the design to see if it needs to be refactored. Um, when I have done true test. Driven development, I find that I have code coverage, you know, all of the, all of the lines and all of the branches are covered. And that means I can have more confidence about what that code is doing. And I don't have to revisit that code as often. It very likely doesn't have bugs in it because if it did, it wouldn't have passed the tests. Um, but I also find that I have higher confidence about what the code is supposed to do, because at each step I write down what I think the code is supposed to be. Do. As well as do As As As As As As As As As As As ! As As As As As concerns more consistently, more effectively. I have higher code coverage and I have executable documentation of what this code is supposed to do. And also that documentation validates that it does in fact do that thing. So that's been my experience over the last probably 15 years of my career since I was introduced to test-driven development. My experience is very similar.

[00:14:18] Allan Stewart: And I do think that I can write testable code more easily. And now that I have practiced testing a lot, it helped me understand where are the kinds of seams that you want to do. But I tend to do a lot of test-driven development. Although when I work on just little side projects, little ideas, either it's like a spike for, I need to try this, if it's going to even work or if it's just, I just have these couple files and I need to pull some data out of it. And I'm not going to keep this code. I just need to like parse something weird. And so I'm just going to write a quick node script or these days get my LLM to write me a quick node script to do this thing. And it works well. And I look at that code and I say to myself, yeah, I could probably get decent test coverage on it. But there's definitely something there that when I take an idea and I say, this was my vision of the design. And I let that lead that I'm much more likely to get into implementation detail problems and testing detail problems that I just don't seem to encounter when I do test-driven development. Part of that I think is because refactoring is such a key component. I think that's a key component to the TDD process that I'm just willing to let the implementation change. And I'll say to myself, well, I don't really like this, or this doesn't feel like it fits one of those solid principles or some other constraint that I have in the system very well. And so I'm going to change it and I can just change it on the fly because the tests will continue to tell me if it's working or if I'm going to make a bigger change, That's one thing I do a lot now these days is that I'll have some code that is already tested and I'm making some changes. I'm adding some features and I realize, oh, before this wasn't really that big. It didn't really demand that it needed to be decomposed into separate classes, but the added complexity of this new feature that's getting added on top is calling for it. The tests are getting more complex. And so I changed the design, but it's driven because I'm noticing these problems in the tests and I can make the tests a lot simpler by doing an extract class kind of refactoring, where now an entire responsibility has moved over to a new location and it's handled separately. And I can move on and the tests are easier to run, because I don't care about the original design. And I think that that's really important when you're going to come... in and revisit code over and over again. In that article, they talked about some example code. And one of the example code pieces that they went back and forth on is an algorithm for generating the first n primes, you know, up to, you know, give me the first 100 primes or the first 50 prime numbers. And I got to thinking, coding algorithms can be pretty different from other types of code. And it made me wonder, do you really need to have an entire algorithm in your head to be able to write it? Like, do algorithms decompose? Well, it's maybe maybe a question like, are different are different problems going to have different constraints in that way?

[00:18:07] Dave Adsit: Yeah, that's a very good question. I spent a lot of time, obviously, when I was working on my CS degree, I went through classes on algorithms, and data structures. And we talked a lot about the different constraints of different algorithms. And I remember spending weeks coding multiple different kinds of sort algorithm for sorting an array of integers. And none of us would do that today. We would just call the sort algorithm. on the collection object in our language and assume that it's doing the right thing, because it probably is. But when you're writing an algorithm, it really matters if you're doing a bubble sort or a merge sort or a Proxmex sort or a quick sort or whatever, like there's all these different kinds of sort. And it really matters which one you're using, both for the complexity of the code and how you structure it. And if you don't understand it, you're not going to implement it well. And I think that maybe that's part of what we're running into in the examples here in this article is that these are specific algorithms for doing the thing. And one of the things that John and Bob went back and forth on is whether or not the code and the comments that they wrote explained well enough what you should expect when trying to implement this specific algorithm. And as we were talking about it afterwards, I just kept thinking, yeah, but it's not the only way to do it. And if I were doing test driven development, and I were trying to get the first X number of primes, my tests wouldn't necessarily even tell me how that code had been implemented, because I'd only be working through the interface. If I said, give me the first two primes, and it returned two and three, test passes. If I said, give me the first two primes, and it returned two and four, test passes. Right. So I may not even know my, my, when I use test driven development, I might not even, I might not drive the specific algorithm that you're looking for. In fact, you might be able to swap out the algorithm under the hood to get some other characteristic, for example, better performance, or from the perspective of CPU cycles or better performance from the perspective of memory consumption, or something, right? Right. Right. You could say, my algorithm for looking up the first hundred primes is to Google search it. I don't know, like that might not be the most, that might not be a very performant from the perspective of CPU, or wall time type of an algorithm, but it might be the easiest one to write, if you're really good with HTTP and making requests, right? So, and that, in fact, is one of the things that you were saying is, refactoring is a critical concept. Right. Right. Of writing code with TDD, you have to refactor. You write code, you implement a feature, a feature, you know, part of a feature, part of a feature, part of a feature, part of a feature, you've got a collection and passing test, and now you look at the code at each time and say, hey, is there something I should do to improve this code? And so you can make changes to that code that keep all the tests working, and now you know that you're still in the same state, even though you may have rewritten all of the code underneath. Right. I think that is one of, that is one of the things that I was thinking about quite a bit is that from my experience, from my perspective, the code that I've worked on has improved with revision. You know, I think when I was a kid, I would do that thing that you do where you put off writing your paper, your term paper until the very last day. So Sunday night at 11 o'clock, I'd be like, well, time to open the encyclopedia and write this paper that's due tomorrow morning at nine. Right. And I usually got by. But that's not the way that our production code looks. You know, you don't turn it in one time and take whatever grade it is. You're working on a product over time. And so you are constantly revising it. Right. And so I think you write a first draft and the code that you write as you pass your first test is your first draft. And so, you know, that going from a rough draft to a first to a second draft is a lot of work. But if you take a second draft to a third draft, that is substantially different than going from a blank page to your first draft. And so I have found that the code improves over time as we add more tests and more deeply understand the problem domain and the solution domain. And so I want to work in a system that encourages revision rather than trying to get it right the first time. If you could think harder, you'd get it right the first time. Yeah.

[00:23:08] Allan Stewart: And how many times have we said that? And then it's like estimating. How long is this going to take? Well, if we just keep working harder at getting better at estimating, then maybe one day we'll be really good at estimating. Maybe. And yet it doesn't seem to work. I seem to come back to the same old problem of, well, I don't know. I haven't done it before. Or I haven't done it exactly like this. But speaking of doing things again, sometimes I'll do a code kata. And if it's one that I've done before. Yeah. Sometimes I will bring a design in my head. Like I have an expectation of what the code might look like when I'm done. But if part of my kata practice is to really follow the test-driven design rules, sometimes I come up with a very different implementation than I expected. And honestly, some of them are much better than my initial idea of what I thought. What I thought it should be. But to get to that, especially when I'm really strict about the TDD rules, it doesn't come about the way that I thought. There's a bunch of kind of uglier versions of the code that only handle a couple of cases. And they've got guards in it. And it's very specific. But it goes from specific to more and more general. Meanwhile, the tests are getting more and more specific. Well, you have to pass this case and this case. And this case. And this case. And I found it interesting to see what can emerge from following that process. And I end up with something that looks very different than what I had initially planned on writing.

[00:24:53] Dave Adsit: Yeah. I have participated in, I don't even know, a dozen Global Day of Code Retreat events maybe. And I have found that if I start with a design in mind. I mean, Global Day of Code Retreat, it's always the same cata. Right? We're coding Conway's Game of Life. And when I start with a design that I want to implement in mind, I have found that I often don't get very far in my implementation. But if I just start writing down the rules of the game as tests and then making them pass, I approach the code from a very different perspective. And we'll get a lot further into a lot more interesting part of the code. Now, one of the constraints of Game of Life or the, not Game of Life specifically, the Global Day of Code Retreat is that at the end of each coding session, you delete all the code and start over. And that, again, is very artificial. It's not at all like what we do day to day. But I have found that one of the learnings I've taken away over doing this many times is that I will create a more interesting thing to look at, discuss, and think about. If I start with a simple test for one of the rules of the game versus trying to define the entire play surface of this game and trying to build a structure that will allow me to render, you know, build the whole world and do all of these things. And I think that there's something valuable there in, first, iteration. But second, doing the most important or most valuable part of the work. Right? And I think this leads me to what I propose as the design quality paradox. Which is that the more time you spend doing design before you start coding, the worse that design will be in both the short and the long run. And I know that that is counter to a lot of people's belief. But I've worked in a lot of big systems where we spent a lot of time doing design. So that we could get the design right. And inevitably, when your ideas, your pie in the sky ideas, hit the hard reality of writing code, of implementing this, you run into things you didn't think about. This is one of the things that's considered a problem for big design up front is that there's too many things to think about. And it takes too long to get to the point where you're getting feedback. And so, from my experience, I like to think. About the software I do not from the perspective of computer science, which is, you know, what I learned in school. But from the perspective of craftsmanship, I'm building software to solve a problem. To meet a need to build a product and deliver on that product. And so, I need to get from idea to implementation to validation quickly. And I need to iterate many, many, many, many times. In order to find the right solution. I've worked in a lot of products where we basically came up with an idea. And we said, we're going to build this idea. And as soon as we have the full idea, people are going to love it. And they're going to use it. And we're going to sell the crap out of it and make a ton of money. And almost every one of those has failed. But the projects where we said, hey, we think people have this problem. We're going to try to solve it. And we put something out there and iterate and iterate and iterate. Taking feedback. And improving both the feature set of the product. And the implementation of the product. We have gone a lot further. And created a lot more value for all involved.

[00:28:41] Allan Stewart: If I had to guess, I would imagine that John would tell us that one of the big problems with big design up front is that it's a big batch. Right? And so, one of the things he specifically calls out is that you should use a lot smaller batches. And I agree with that. I think that it does lead to better outcomes. For sure. But I just, I still struggle with like calibrating my mental model of how do I create a design batch that is small enough that I feel like I'm understanding the problem. And I can create an implementation where those, where when the little details crop up. Which they inevitably do. Little things that once you're actually writing the code, you start to realize. I don't know how to deal with those without either revising the design. Which is what test driven development allows me to do safely, quickly, just kind of naturally. Or sticking to my guns and saying, no, this is the design. Right? And I think that there's definitely something, there's like a mental head space that you can get into. Where you say, if this is the design that I'm trying to build. Then it's going to inevitably lead you to certain decisions. Which are different than if you're taking it from a perspective of this is what I need it to do. This is what I need to ensure works. And the thing that you mentioned before about, you know, with global day of code retreat. You always throw away the code after each session. I don't ever throw away my production code from my employer. Right? As much as I might want to at times. Because it is the functionality that is in there is important. It's an asset. Right? If I just start deleting stuff, customers get really mad really fast. And pretty soon you don't have a business anymore. And so I've got to keep that stuff there. I've got to keep it running. I'm going to have to go in and make iterative change to things. And sometimes it's well after the initial design process of that. And my experience. Has been that test driven development helps me to create better tests. You do have to have some. I think we've talked about coding with guts. Good unit tests. Ones that aren't overly brittle. They're not overly specifying the implementation. And what I've found for me is that the tests have helped me to make those changes. Because I can go in and say, oh, in this existing test, here's the change of behavior that I want to see. Edit the test. Run it. It fails. Okay. Now I can implement it. Great. Now that test passes. Oh, by the way, these other tests now fail because I have not yet updated them to match the same expectation. But within a few minutes, it's all good. And I've made the change. And test driven development allows me to keep that same process from when I initially wrote it to when I come back and revisit it again. And when I come back and revisit it again. Even though the context of my problem. And the purpose for me to revisit a piece of code has changed.

[00:31:57] Dave Adsit: Yeah. I think that there's something in there that is really critical, which is the revisiting. And I mean, we've mentioned it a couple of times. I think I will continue to mention it. That we revisit the same code and we improve it over time. And I think one of the things that I have a lot of sympathy for and I think I see in people who advocate for design first is the idea that there is a good design. And that we can know it. And in my experience, there is, there are not very many good software developer or software designers. People who can sit down and design a system from scratch. There's just not very many people who have enough experience that's relevant in order to do that consistently and reliably every time. Every system that I've worked in where we spent a lot of time designing first. Has suffered from over engineering. You know, inevitably someone's like this system is way too over engineered. We need to strip it all down. We need to start over and do something simpler. And the systems that are built test driven. Where you write a test to implement the next feature. And go at it from that perspective. I'm just going to do enough. Just enough, just enough, just enough. You may not have a good design initially. But you have working software. And working software. Working software is what pays the bills. Good designs don't pay the bills. And they, I just, I've worked so many times with over engineered systems. That were over complex because we're bad at predicting where we need to make the changes in the future.

[00:33:38] Allan Stewart: And ironically, they're not flexible in the way that you need them to be. Inevitably they're not. In one area, but they're not inflexible where you actually need them to flex.

[00:33:49] Dave Adsit: Yeah. Because again, it goes back to prediction, right? We're not good at predicting the future. And so for me, spending a lot of time designing up front is not, has not been valuable. Whereas sitting down and writing tests and implementing the tests has been. And I have rewritten a lot of code that I started TDDing and it took me down a path that didn't pan out. And so I had to come back. But I would have run into the same problems designing up front. Because I didn't even know that those problems could come up until I faced them. I will say that there is something that needs, that like, I don't know, concession or not. Planning is important. Sitting down and discussing the problem and making sure that you understand it is very important. I would just say that the plans that you make may not be. Like if we sit down and we talk about a whiteboard something for a while. And we say, here's this and this and this. We're going to put all these pieces together. We're going to use your experience and my experience and your knowledge and my knowledge of both the problem and potential solutions. And we're going to write a bunch of stuff down. And then we think this might be it. And we start, then we go start writing the tests. I think, you know, maybe that help, that will help us understand the problem to a deep enough level that we can actually start to solve it. But I'm not sure that I consider that to be software design per se. When I think about software design. I think about it at a much lower level around like the actual implementation of classes and methods and how we're going to use values and variables and all of those types of things. So again, it comes back to being willing to say, okay, we have a good enough idea of how to solve this, that we're going to get started. And as we discover more, we're going to change the code. I mean, this is the, this is the fundamental thing about software versus hardware, right? The software is soft. It's malleable. It can be changed presumably quickly and easily. Whereas hardware, you bake your algorithm into, into your hardware. You're going to have to go design a new chip and, and print a new integrated circuit.

[00:36:01] Allan Stewart: Yeah. It reminds me also that there's very rarely a right design or a perfect design, but there are lots, there are lots of ones that might fit your context and there are some that do not fit your context. Yeah. The other thing that I've been thinking of still kind of trying to wrap up this TDD concept. When I go in with a pre-envisioned solution in mind, it's hard for me to stop early because I usually have like a plan of like how this thing is going to work. So let's say I'm doing something to pull a cata example, right? Like I'm going to do Roman numerals. It's like, okay, well I've got the plan. I've got the plan. I know what all of the Roman numerals are. And why would I stop? I'm going to, I'm just going to implement the whole thing. But when I'm doing test driven development, it's a lot easier for me to, to stop at a certain point. And, and, you know, the product manager comes and tells me, it's like, well, we're only doing Roman numerals for clocks and clocks have 12 numbers because I've, at least I've never seen a clock that is like 24 hour, like military stuff. That would be interesting. But it's got hands and yeah, like an analog clock. But in a case like that, right? They say, oh, well, all we want is one through 12 and we're never going to give zero. We're never going to give 32. We're never going to give 176. So don't write any of those cases. We never leaned a letter M. We never need the letter L or C or D. And if I've done it test driven, it's a lot easier for me to just say, oh, well, I guess this small amount was 80% of the value and let's ship it. And if we need to come back later, well, we'll add those in later. And maybe that'll change the algorithm that we choose to use at that point. But I'm not sure that it matters because, and I might have a very different algorithm for now because knowing that all I really need is the first 12. Well, it just might be an in-memory map or a hard coded lookup table because that's going to be super vast and done.

[00:38:09] Dave Adsit: Yeah, that that's exactly right. Usually I have found that if I'm. If I'm iterating through a problem test driven at every point, the code works. And at any point I can declare victory. If I am designing first, I may not be able to ship anything until I've got everything. Just to restate what you're saying until I put that keystone in the arch was the arch is not stable. And so I, I have, I have fallen for that trap in the past myself, where I have. A really, really clever, really, really straightforward design. This design is going to be awesome. And then it takes longer than anticipated to implement it. But nothing of value can be delivered until the entire thing is there. I've seen this where frameworks are envisioned versus extracted. You know, we've talked a lot about everything a framework does for you. It does to you. We talked about how some of the best frameworks come from finding the commonalities between multiple implementations of similar types of products. Yeah. And then you have the You know, my team at work is using Laravel 11. No one could have sat down to write Laravel 11 from a blank. From a blank editor in PHP, because they could have never predicted all of the things that would have needed to go into creating that version of that framework. And it wouldn't have been the right thing to do if they could, because it's taken how many, how many years to write that version. And how many of the features of that version would have been irrelevant back when the very first version was shipped or extracted from the code bases that, you know, had been built. And so I guess at the end of it all, the difference between design upfront and test-driven development or test-driven design, which is what I would call it. I often call it. Is accepting the constraints of being human for me. Like we can't predict perfectly into the future. We can't predict all of the potential case edge cases and problems that we'll run into. And we know that we're not good at holding all of these things in our mind at once, which is why writing those tests first and letting them drive out the implementation and just enough implementation to solve the problem at hand. Allows us to, in my experience, build simpler code and ship it sooner, iterate more often. And we still can get to a good design if this code actually matters a lot, because as we spend more time adding more functionality, we're going to be doing more refactoring and more iterations of the design of this part of the system. And eventually we'll have something that does everything we need it to. We'll stop working on it. And the design will be good enough. Because it covers all of the criteria. And if a new criteria comes along like, oh, this code isn't performant enough, or this code talks to the database too often, or this code uses an API call from a vendor that is no longer available, we'll make an iteration. And we'll make some changes. And it'll solve the problem. We'll have driven out the code in a way that will allow us to make changes to it easily.

[00:41:31] Allan Stewart: So another aspect of code design. That got. I think it's related to the question I asked earlier about how far do you decompose functions? Or how much do you break up big functions into small functions? And one of the things that I got to considering, and I think it's related to the question I asked earlier about when you're coding algorithms, is that different than coding other types of code? I think that the difference between a large size of code and a large size of code is kind of a question about the coupling and cohesion aspects of the thing that you're trying to break down smaller. I think that I think methods can be cohesive. Classes can be cohesive. And within a class, you might have high coupling. I think there's an interesting question to be asked about whether cohesion is just kind of like the flip side of the coin of coupling. And it's telling you, oh, well, these things are cohesive. If they can't be decoupled. And I wonder if that kind of goes into some of the debate that they had back and forth about their example. Because their example was that same prime number code. And I think for me, like my ideal version, if I was test driving it, I would lean on the tests. And I'd probably end up with kind of a mix between their two solutions. Going for neither one in particular. But it's like, okay, this algorithm feels cohesive. One of the examples that they showed was very OO centric. Where you have some private static members. And only the function can edit those members. And so it's safe. Right? In an encapsulation sort of mindset. And you can call this over and over. And it will work. Because nobody else can, you know, muck up your private variables. But I kind of like just the simplicity of the, well, we just declared everything in line and just ran with it. And so I kind of think that I would end up with a version that was just one function. Maybe a little bit longer. Probably without any comments. But we'll talk about comments in a minute. And then, again, leading on the tests to say, hey, was this right? If it's right, then I can ship it. And if it doesn't pass the tests, then I don't ship it. And if it turns out that later on we need to do a new test. And if it doesn't go to performance optimization, well, we'll come back and do that when it's needed.

[00:44:03] Dave Adsit: Yeah. When I think about composition of functions and smaller functions. Like as I was going through that, I think that perhaps some of the code, some of the discussion was framed from a perspective that we no longer hold as an industry. I say that. But then I have counter examples in my own code base right now. Right? The code that I have worked on in the past in my career. Has been very, very, very verbose. One big, long, rambling function with hundreds of lines and jump statements and all kinds of crazy shenanigans that make it really, really hard to follow. And so the advice in the clean code book is write methods that pretty much do what you expect them to. Look at the name. The name tells you what this method does. And then this method will probably call other methods that do things that are obvious. Given their name. I very much align with that. That is very much how I like to write code. And that means that I tend to write smaller methods than you might, that I would have seen earlier in my career. I worked on one project where we actually had a template for every class and the template had 10,600 lines in it. So you create a new class. Like new Bob or new foo. And the new, the foo class. Started with 10,600 lines. And only went up from there. That code is very hard to reason about. Very hard to work in. I have found that there's a, when it comes to decomposition, I am a big fan of inline and extract. Inline and extract. I will extract methods and then inline methods. And I will sometimes end up with a method that is a little bit bigger. Maybe 20 or 30 lines. And then oftentimes I'll end up with methods that are very small. Fewer than 10 lines. Like in general, I find that smaller methods are easier to reason about. I think that there are times when it becomes obvious that you are arbitrarily and unnecessarily splitting logic across things. But again, this goes back to my general architectural instruction is that you, we need to be good at taking things apart, making them smaller, but we also need to be good at putting them together. Anybody who wants to embark on a microservices journey. Needs to get good at taking apart their monolith. And they need to get good at putting two microservices back together when they inevitably split them wrong. It turns out these are very cohe these are very coupled there. These need to be cohesive. This is one. Microservice one service that is trying to live in two homes. And so you need to get good at both taking apart and putting together. And so. I don't know where I would end up if I were trying to implement that. I would probably end up with a one or maybe a couple of different methods. I lean on private class methods. When I'm doing things like predicates. So if I have a check, I would maybe move that, that, that check that the predicate for the, if, or the while or whatever into a separate function. So that it has a network. them and working them and working them again, you know, making multiple iterations, multiple passes at the same code base as we improve it. And as we add value to it and as we add functionality to it. And so I don't know, I would say I lean on having smaller, more concise methods. I want SRP, the single responsibility of principle to apply also to the methods in my code. What is this method doing? It's doing this. Oh, okay, great. Now I've seen people say, say, if you're initializing two variables, should that be two methods? And like, no, it should not be initialization as a concept, right? I would like to have it. I would like to implement the concept of initialize this object. In fact, I often use a method, a single method to mutate two or three separate things when that mutation is an atomic mutation of the object. All of these, all of these changes need to happen together, as opposed to if I had a square class or not a square class, that wouldn't make any sense. But I mean, it'd be even less sensical than if I had a rectangle class, right? If I have a rectangle class that exposes height and width, well, I might not want to allow you to change one without changing the other. You know, the definition of the rectangle is its height and its width. Sure. Or its location is defined by its X and its Y or in X, Y, and Z. And so if you want to You can't change its X and then change its Y and then change its Z. You can only give me a new X, Y, and Z. And maybe if I'm building a game or something and X, Y, I want the new X, Y, and Z for this object. And I want its translation function. I want it to slide or jump or, you know, whatever. So those, those changes come together. And so the length of the method is determined by accomplish. And so I would say in general, my advice is write smaller methods, extract more, extract and name concepts more often than you are used to. In my, in the code base I work on in production today, we have one method is very mission critical to our business. It's 300 lines and it's not very well designed by accident. I'm sure that someone set out to design it well at the beginning of time, but where we are now, it is not a very well-designed method and it could serve from some iteration.

[00:50:28] Allan Stewart: Yeah. I think I agree that I want to err on the side of smaller than big in the code that I write. But that's not, but that's not my primary function, right? That's not, that's not the core rubric that I use to decide whether or not something needs to change. Like for me, it's more like the idea of the code smell, right? I see a long function. I think, Hmm, this is long. Is it long of necessity or is there something that is begging to, to be extracted out of this and made smaller and increase the understanding in some way? And so I think I, I use that as a way to guide me, but I'm, but when I actually go to make the decision of how to make something smaller, I'm, I'm likely to be looking at coupling, how related are you to the code? How are these things? Do they need to go together or not? And, um, and abstraction, uh, just like you're saying, take a concept and remove that into it, into a named function so that I can understand it's like, oh yeah, conceptually, this is what is happening now. It's not the details of what's happening now, because that's what an abstraction is for is it hides implementation detail to push it down a layer. Um, or. Yeah. If I see that, Yeah. Long function is, is attempting to, to, to do too many things at different layer levels of abstraction. Then that's, then that's a clue for me that, yeah, this can be made shorter, but the shorter is more of a byproduct of moving the things around to where they belong to create a better coupling and cohesion, a better abstraction rather than because I want it to be shorter.

[00:52:16] Dave Adsit: Right. And I think that one of the critical things that I want to reemphasize, there is that a method should be at a level of abstraction. The goal is to talk at one level of abstraction through the course of a method, which means that when I hit a second level of abstraction, I want to pull that out to a separate method.

[00:52:37] Allan Stewart: Yeah.

[00:52:38] Dave Adsit: Because I find that that helps people think more clearly about what's going on. Um, it helps me understand the code more, more quickly, more concisely. And so the last thing that I want to make sure we touched on as a, a, you know, fallout from reading this article is around that specifically things that help us understand the code. And the two things that were brought up over and over and over were naming and comments. And, um, John and uncle Bob come at this from completely different perspectives. Uncle Bob wants to use names of classes, methods, variables, in the code to communicate and then issue most comments other than the very few comments that explain why this is done this way. Uh, and John has a very different experience and brings a very different, uh, perspective. He wants code to be well-documented, well-commented. Uh, and from my perspective, that seemed to be very heavily commented. I think one, it would be, it would be worthwhile pointing out that both of them agree that you shouldn't have to use names of classes, but you have to have comments that repeat the code verbatim. And I think that might be where their, where their consensus on what makes a good comment ends. The comment should not reiterate or re should not just be a restatement of what the code itself is doing.

[00:54:07] Allan Stewart: Yeah. And I don't know when, when I have small, like kind of carrying forward, the last thing we were talking about, about the smaller functions and decomposition, small function that has a good level of abstraction and it says what it's doing. I just don't have the desire to have as many comments because it says in the name, right? It's right there on the 10. And when I have a good variable name or a function name, I just don't want to repeat it around with, with all the comments. But I do wonder if the level of programming experience of the person reading code affects how much desire there is to have comments. I mean, John and Bob, obviously both have a lot of experience, but John talked a lot about teaching students and students tend to have not as much experience, hence the title student, they're, they're learners, right? Like hopefully we're all students of the craft, right? And so I don't, I don't want to read too much into that, but I do wonder if, if people who are first starting to learn to code, code, well, I mean, we call it code, right? We don't call it plain text, right? Although hopefully rewrite our code in plain text and we've overloaded all the words that we use to describe anything, but, but it's encoded, right? Like the, the syntax is often hard to parse and understand. It's hard to look at it. You know, if, if you show even well-written, you know, self-documenting code to a non-programmer, they're going to have trouble because they're going to be hitting over these, mental speed bumps constantly. Like, what does this mean? Why does the order of these, of the things on either side of the equal sign, why does that matter? Why does this, that, and the other, and they're trying to figure it out. And I wonder if students are more, more likely to want comments for that reason, because they know how to read English already or whatever language they speak. But, but reading code now, now I have to take, the idea and have it, I have to understand the idea, but I also have to understand the syntax of what makes the code work and why is there a for loop here?

[00:56:29] Dave Adsit: Well, I would go a little bit further than that and say that, you know, students often want partial credit and software is binary. It worked or it didn't. And if it didn't work, you get no, you get nothing, nothing for you. No, but if, if I can explain what I was trying to do, what I thought I did, then maybe I can get partial credit for the work. By adding these explanatory comments. And I also think that, you know, one of the tech techniques that I was taught when I was a novice programmer is to go into create a method and then make it write a comment and then write a, write what you're trying to do in comments through that, in that method, between the braces of that method. Right. And so you have maybe eight or 10 lines of English that are calm, all comments. And now all I have to do is write the code that does those things. And I leave the comments there so that I have markers of where, where I'm trying to do each part of the algorithm that I just invented to do whatever it is that this math, this, this assignment requires. And I was very much encouraged to write code that way and to leave those comments in for myself, for help debugging, help identifying issues, and also to explain to came behind me what the world was going on. One of my very first programming classes, we had to print out and turn in our program listings instead of submitting a digital file that could then be executed. And so somebody who came along behind me was actually probably not going to retype my program, but they were going to look at my printout and just decide if it worked or not, or if it was good enough or not, and give me some kind of, of a percentage grade, as opposed to now, the way that I write code and what I use for the products I build is I write tests and those tests tell me, does this work or not? And if it works, then it's, I just rely on the names. I rely on the names and the tests and the names and the tests will tell me what the coding is trying to do and what it's actually doing. And I really find that most comments are a distraction, if not an outright lie. And I know that this was one point of contention between John and Bob is that John finds that most comments that he's encountered in his career are valuable and good and truthful. And Bob only trusts the comments that he has been able to validate against the code. And my experience puts me much more firmly in Bob's camp than John's. I have seen too many comments that are nonsense, that are lies, that are just restating what the code is already doing in a more verbose way, but with less, I mean, English is just not a great programming language. That's why we don't program in English. English has got too much ambiguity and requires too many words to say the same thing as a programming language would say. And I don't know, I think my experience pushes me to distrust comments more. And to trust tests more. And so I lean on tests and I lean away from comments. And I do find that there is truly value in, in writing code that is self-documenting. And I know everybody has, you know, their own experiences with self-documenting code, but the concepts that you get from a ubiquitous language, the concepts that you get by implementing what Eric Evans put in his book on domain-driven design, and all of the books, that have come since will take you a long way towards explaining the what and the why in code. Bob is somewhat famous for saying that a comment is an apology for not being able to express ourself better in the code. And I have found that I agree with that, where John very much does not agree with that in any way, shape, or form. I lived for a couple of years in a foreign

[01:00:47] Allan Stewart: And I learned to speak a second language. And I can definitely see, especially in the light of thinking about multiple languages, just how imprecise spoken language can be, because we're trying to convey ideas in a way that, you know, sometimes words are overloaded and they mean multiple things. Or sometimes there's a concept that is just really hard to describe in English, but there's a perfect word for it in another language, or vice versa. And I wonder if that doesn't catch us out sometimes in code. There's a concept in this code that is trying to be expressed. And if you use the language of code, and I'm not trying to say we should all write in binary or anything, but if you use the idiomatic way of expressing things in code, then that's good. And when you know how to read code, that's better, right? So going back to my previous assertion, well, when I learned how to speak English, I learned how to read code. When I learned how to speak a second language, I had to go through a translation process almost constantly. And so if you're learning how to write code and you have to constantly translate, what does this syntax mean in English, then having the comments on every line that tells you what it does might be helpful because it's like having your very own Rosetta Stone just right there in front of you. But eventually you're going to know how to speak the language well enough I don't want to go through the translation layer because it's extra mental overhead. And maybe that's just where I've arrived personally is that I don't want to see the translation of the code and the mistakes and the, well, that doesn't actually mean, that word doesn't mean what you think it means when it applies in this way. And I just want to see what is it actually going to do and run my tests to verify that.

[01:02:43] Dave Adsit: Yeah, I have similar experiences learning another language. And it takes a long time to get fluent enough that you start thinking in the language as opposed to thinking in your native language and then translating. And that is definitely a possibility for the desire to have comments. But again, I would say that I get the benefit, there is a purported benefit of having well-documented, heavily commented code. And I feel like I get that benefit and more by having a well, test-driven and well-named code base. And I am a big fan of rename refactors. I will rename a class over and over and over, rename a method over and over and over until I find a name that actually expresses what I'm trying to use that method or class for. And again, that refactoring, the regular refactoring of code that we do as part of a clean code lifecycle, as part of, working as craftsmen on a product over time, allows me to find the edges and find what the code is trying to tell me. The code is often trying to tell us, I mean, you talked about code smells. And if we go back to that concept briefly, there are times when I keep trying to change the name of a class because it doesn't quite fit. It doesn't quite fit. It doesn't quite fit. And I realize the reason it doesn't fit is because the coupling or the cohesion is wrong. And I may have And when I finally let them free and extract the class, extract class and have now two classes with different names, the code can stabilize and become, the design can emerge from the system as we are building it. And so, I mean, I guess that's the, at the end of it all is like, we want to have good design, well-designed code. We want to have a good design. We want to have a habitable system where things make sense. They're expressive. We want to have a good design. They're easy to understand. And I know that for some people that means comments. And for some people that doesn't, comments just get in the way. And I find myself in the get in the way category. But I think when it comes down to it, you can approach design from an upfront design perspective or from a evolutionary design perspective where as the code changes, as it evolves, the design emerges from the series of changes that we make on it. I think for me, the systems that I have enjoyed working in the most and have felt the most natural, the most easily understood, the most well-designed are the ones where we continuously worked on the design through refactoring, where we had a strong test suite that was generally created test-driven development style. We wrote the test first. We had small classes, small responsibility was applied everywhere. We used inversion of control and we injected dependencies and we used composition over inheritance and all of these different design principles. And we regularly reworked the code as we became more familiar with the design problem and the solution space. And those are the systems I like to work in. And those are the systems where it feels most rewarding and where I feel like I can add the most valuable, valuable value in the most reasonable timeframe.

[01:06:20] Allan Stewart: One way to sum up the discussion that we've had here, I think, would be to say, oh, well, Allan and Dave are on one side of the fence. They think they like TDD, small functions and comments. And I think that that would be an accurate description, but it's not the one that I hope that people will take away from this episode. But rather, what I hope people take away, is that these are things that are not inherent to code. And we have to apply ourselves to create better code, create code that have better designs, that are cleaner, that are better, that we can enjoy and inhabit. And that requires us to challenge our assumptions from time to time. And one of the reasons that we got so excited or passionate or fired up about this topic was But for me, it did challenge those things. And it did force me to go back and look at them and say, hey, why do I believe the way that I believe? Is it justifiable? Do I have enough experience with both sides of the aisle? And of course, we're summing up years of experience. And so we can't give you all the details of all the times where I tried it this way, I tried it that way. But I hope that anybody who is listening will take a chance to reevaluate your stance on something that you you believe very much. Dave and I believe in clean code and many, not all, of the principles that are in the book of the same name. And it's good for us to go back and revisit them from time to time and make sure that we still believe that. And if something has changed or the industry has evolved, that we're prepared to say, okay, here is a better way. And we're going to start changing starting tomorrow.

~/podcast/episodes/041-code-design $ cat ../../copyright.txt

Copyright © 2026 - Crafting Code Podcast