Crafting Code Podcast

~/podcast

$ cd episodes/052-solid-principles

~/podcast/episodes/052-solid-principles $ ls -1a
. .. episode-summary.txt references.txt themes.txt transcript.txt get-mp3.sh
~/podcast/episodes/052-solid-principles $ cat episode-summary.txt

Coupling and cohesion are fundamental design considerations, but when sitting down to build a system we often find we need additional guidance. SOLID is a mnemonic for five important principles which help us lower our coupling and improve cohesion, improving our software architectures. In this episode, Dave and Allan discuss each of these principles and how they support and reinforce each other.

~/podcast/episodes/052-solid-principles $ cat references.txt ~/podcast/episodes/052-solid-principles $ cat themes.txt ~/podcast/episodes/052-solid-principles
$ cat transcript.txt

[00:00:15] 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. Lately, I've been thinking about picking my battles and deciding which hills I'm willing to die upon.

[00:00:32] Dave Adsit: I'm Dave Adsit, a VP of engineering. And recently, I've been thinking about progression, the challenges of growing and improving, and the importance of owning one's own career path.

[00:00:42] Allan Stewart: Our topic for this episode is solid principles. So the solid principles have been around for a while in discussing code. I'm most familiar with them through some of Uncle Bob's work, clean code and clean architecture. But he didn't create all of these principles. I think interaction between him and someone else, I can't remember offhand, it was really like the mnemonic solid that helps us remember what the principles are. That and then describing the principles in his books is a lot of the contribution that he gave to it. But I feel like it's been... It's been a really important set of principles for me as I have grown in my career, as I've tried to write better code, as I've wanted to improve software architectures. And so I felt like it's a great topic for us to cover.

[00:01:39] Dave Adsit: Right. I think that it was Michael Feathers, actually, who worked with him to put them in order so that they could spell a word instead of just being a collection of abstract principles from software developers. Yeah. So I think that one of the things that's important to look at is that the fundamental guidance, the fundamental considerations for good software development are coupling and cohesion. We talk about them all the time, right? And we're all always encouraged to have loose coupling and high cohesion in our components, whether they're classes or modules or whatever. Usually that guidance isn't quite enough for most of us. And so for me, that's where solid comes in. Solid guidance. It gives some deeper insights, some deeper principles that we can use to help us understand the concepts of coupling and cohesion and how the interplay of those two concepts leads to building a good software system. So the solid principles themselves, they interact with each other. They reinforce each other. If we were to name them in order, they are, single responsibility, open-close principle, Liskov substitution principle, interface segregation principle, and the dependency inversion principle. But I think that we should talk about them in a slightly different order because of the way that they interact with and support each other. Agreed. There's some principles that as we

[00:03:17] Allan Stewart: talk about them, it becomes more and more important to understand the other principle. So we'll go out of order. Right. And we'll start with single responsibility. And the single responsibility principle tells us that a module should be responsible to one and only one actor. And now this is one that often gets misunderstood because it has a lovely name called single responsibility. And so it's often misunderstood with a similar idea, which is also a good idea that a class should have only one responsibility, right? Like it should do one thing and do one thing. Right. And it comes into play with Conway's law.

[00:04:09] Dave Adsit: Yeah. One of the things that has come up many times when I've talked about single responsibility principle with teams is they're like, oh, so you're saying that a class should only have one function? I'm like, well, no, not even a little bit. But you can see how Right. And so when we talk about the actor, often it's, you know, it can be a person or a system, but it's the consumer of the module that you are responsible to. So you might say, if you're building an e-commerce system, you might say one of the actors is the marketing department and another actor is the head of finance. And those are two people with two different reasons, two different sets of needs. And I don't want to build a module that might change on the whim of marketing or accounting. I want a module for marketing that changes only based on the needs of the marketing group and a module for accounting that changes only based on the needs of the accounting group. I think we all have experiences where we have failed to follow this principle and we have gotten into trouble.

[00:05:41] Allan Stewart: Yeah, absolutely. And sometimes it's just, it can even just be weird little things, right? Like the marketing department wants everything to be in dollars and they want to, you know, they want to show their discount and how this is going to be the best discount because it is negative $25. But then the, you know, the finance department looks at that and they're like, you should put parentheses around that and draw and get rid of the negative because that's how we do it in accounting world. And so, so you go and you make this change. And then, then somebody else comes up and they're like, what the heck? Nobody understands what's going on. All of our marketing stuff is ruined because you changed, you changed it. You got rid of the, the, the minus sign. Everybody understood a minus sign. What are these parentheses for? Why did you color it red? Like now, now it seems dangerous.

[00:06:30] Dave Adsit: Right. Well, I was thinking about a much more insidious type of a problem that recently occurred for one of my teams, which is the, we have a data lake. And data flows through it. And sometimes that data goes into reports for marketing. And sometimes that data goes into reports for accounting and accounting decided they wanted to change the way they calculate a certain value. And so they made the change, but because we were maximizing code reuse, that function that they changed because of a change, they wanted changed a report and marketing. And suddenly the marketing department looked like it was not performing nearly as well as they thought they were.

[00:07:07] Allan Stewart: I bet that did not make them happy.

[00:07:09] Dave Adsit: No, it kind of turned into a. Fire drill for some engineers who were like, we don't even know what's going on. We haven't been part of this data lake project. So yeah, that module had two reasons to change, unfortunately.

[00:07:22] Allan Stewart: Right. And I think the, you know, we talk about Conway's law that thinks that gets us thinking about like our organization structure of the company. I think it may also be related to the social structures of your users. Right. So if you've got a user who is an admin that has a different level of permission than a regular user. Or you, if you're doing like B2B software, then you might have a user who is the consumer of your software, but they in turn have their own customers. And the need of the customer is different. You don't want to accidentally give that end customer access to view a lot of things that should be hidden and only available to the immediate customer, right? The tenant in the system. So yeah, there's, there's a lot of. Opportunity for single responsibility to help you narrow those scopes and, and say, no, we are responsible to this actor rather than trying to add the ever mounting complexity of getting it right for everybody. Right. Like your marketing and finance example. Yeah.

[00:08:29] Dave Adsit: I think that single responsibility is a way to overcome the unnecessary or destructive pursuit. Of reuse. I think I've often been heard to say that reuse is the false God of software development. Pursuing reuse can actually lead to substantially worse code. In fact, I tell my teams, don't worry about reuse until you've mastered use. Like build something that's usable before trying to make it reusable. And I think that. Single responsibility is a way that we can. Find. Places in our code that look like reuse would be a good idea and understand why it would probably not in this case. Yeah. Yeah. I like that a lot.

[00:09:21] Allan Stewart: So the next principle that we're going to talk about is Liskov substitution. And there are a couple of different ways that this can be stated, but I think that the one that makes sense most to me is that subtypes should be interchangeable. Right? So if you've got some kind of inheritance going on. Where all of your shapes. Inherit from a shape. Base class, or if you've got an interface. And things that are implementing this interface. Or you have, um, even just any, basically any kind of contract that defines. Here is how you can talk and interact with some other piece of code. Well, any of those subtypes. They need to be interchangeable, but what does that mean? Right? Like, what does that mean? What does it mean for them to be interchangeable? Cause there's a few different ways you can do it. And some of them violate the Liskov substitution principle.

[00:10:16] Dave Adsit: I think when I think about Liskov substitution, I think about it in terms of an example, which for me, the con the most common example that I use is the repository or the data access object or the data interaction. Right? I have an interface of base class. That's got a couple of methods on it. And I may have various different types of concrete implementations. Uh, if I'm building. Software for distribution, I may say, Hey, I support three different relational databases. And here is the, you know, I going back to the previous example, I, I want to load or save a, uh, an object. I might want to save it in a real, in an Oracle database or a Microsoft SQL server database or a, my SQL database. And so I might have three implementations of the load and save. I mean, load is. Probably the same for most of them because it's a select statement. But if you want to limit the number of records returned, that's going to be different in those different types of databases. But if you want to do save, save is going to be an upsert and each of those databases implements upsert in a different way. So you could use any of those subtypes in place of the base type, and that would be perfectly fine. They're completely interchangeable with the base type, but when we get into the. The shapes. Well, in. Geometry. A square is a rectangle, but in polymorphism, it is not because in polymorphism, a rectangle has a width and a length and a square only has a width. Right. The length is defined by the width. And so if we were to implement a square as a subtype of rectangle, it would not be interchangeable because we would either have to have. A no op on setting the width, the length rather, or we would have to. Have an unexpected unintended side effect.

[00:12:18] Allan Stewart: Right. You change the length and it changes the width on you.

[00:12:23] Dave Adsit: Right. You would not expect that in a rectangle, you would expect to be able to change the length in irrespective of the changes of the width. Right. Without changing the width rather.

[00:12:33] Allan Stewart: Yeah. And I like, I like your database example because I think it's. It's an interesting one where we can see how. Yes, we do want to be able to support. Right. So like at, at a certain level of abstraction, it makes a lot of sense that these different databases are interchangeable, right? They have different implementations of how they do these things. But what we want to accomplish with, with our code. Like in, in, in your example, just pretty simple, save a record, load a record, right? That kind of thing. Is pretty simple. Pretty easy to generalize. However, I've also seen, I'm a big fan of the repository pattern from domain driven design. And I've seen a lot of implementations that try to generalize it. So in the C sharp world, that would be like using generics in other, other types of other languages, kind of a similar idea where I want to be able to pass anything. I just want to have like a generic interface for. Any. Repository. And, and isn't this going to be great because, and then everything works the same way. But the problem is the repository pattern as described in DDD wants to have very specific methods for specific kinds of access for specific things that you want to be able to do. And so it's really easy to consider. It's like, oh, well, we'll just have this interface repository that is generic. And so obviously we need to save and we need to load. And we need to delete. And as soon as you hit delete, that stops working for certain kinds of tables. Like if you have an append only log, well now what happens when you call delete, it's going to be unexpected in some way, right? You could just make it a no op. It's like, oh yeah, I called delete and nothing happened. Well, if you called delete on your file system and the file didn't go away, you wouldn't feel good about that. And you don't feel good about it. If, if you try to tell the database. It said, it said I could delete, but it was a lie. And that is breaking Liskov substitution.

[00:14:43] Dave Adsit: Yeah. So let's talk about the next one, which is interface segregation principle. I think in a previous episode, I completely missed this one when talking about solid. So it's good to have an opportunity to correct that. The interface segregation principle is that you should not depend on things that you do not use, which can be hard to do if there's too much stuff in your. Interface. Mm-hmm. Right. So talking about the interface that we were talking, the, the, the repository interface, if we've got. Save load and delete. Okay. Well, those are, what if I don't need delete at all? Well, now I've taken a dependency on a concept that is not part of what I need. And so maybe I need to have a different interface for saving and a different interface. Well, I did an interface for saving and loading different interface for deleting in many of the references. If you were to go that far, you might have interfaces for different queries that you make to return collections of objects with different conditions. But those are not used very generally, right? They're only used in certain conditions. And so you wouldn't want to clutter that interface. What I think about most when I think about interface segregation is frameworks. Hmm. Usually if you adopt a framework, it's going to bring a bunch of stuff with it. So much stuff. Yeah. And that's why you get a framework because you don't want to have to re-implement HTTP and message dispatch and queue management and whatever other things there are that come for free with that framework. On the other hand, all of those things come together. And so now you've brought in a very, very wide framework with a lot of components in its interface. And you depend on them all and you may not need them all. So if, if you don't use the tax calculation component, because your, your web application doesn't deal with commerce. Well, now you have to make hard decisions when they update the tax calculation module and ship an updated version of the, of the framework. Do I update my whole framework for a piece? I don't really need. Maybe not. But now when they release the next version. I've taken on twice as much risk because I'm skipping versions in my update path. And so those are some of the things I think about in terms of interface segregation is depending on the things you need, making your interfaces clean enough that all of the components in them, all the methods in them are cohesive. They're all related to each other in a way that makes sense for your component.

[00:17:35] Allan Stewart: I like that you use framework as an example here, because the word interface can immediately cause you to think in a particular way, just depending on what kind of interface is common for the kind of code that you write. So I do a lot of C sharp. And TypeScript and both of them have something in the language that is called interface. And this principle applies to those interfaces. Yes. But it's not just to things like. that have that specific keyword in a language, like the interface of the framework that you suggested is a good example, right? If there's other kinds of interfaces, application interfaces, I mean, even APIs that work over networks could potentially be an interface that you are using, and you just need to be careful what you're doing with that, right? So like it's popular to see libraries that, oh, don't worry, I will help you with X dependency. You're going to call this popular API, and so you should use this framework that knows how to talk to all of the endpoints of that API. Well, are you going to use all of the endpoints of that API? And if not, what happens when they do an update, right? Exactly the same thing as with your framework. You have decisions that you have to start making. And the other place where it really can hurt is inside your own code, depending on the kind of code you're writing. But if you use a lot of strict types in something like C Sharp, well, an interface changing requires you to now recompile. And now you have to rebuild and have knowledge of this other thing that even though you weren't using it, right? There's five methods on the interface, right? And now going back to the idea of a keyword interface, there's five methods on it. I only used two of them, but one of the three I wasn't using

[00:19:38] Dave Adsit: changed its signature. Recompile. Yeah. Well, and maybe this is a little bit too meta, but let's talk about interface segregation in terms of another principle, which is the command query segregation principle or pattern, right? In that pattern, you very explicitly create You have to recompile it. And then you have to recompile it. And then you have to recompile it. from, hey, sometimes I'm reading data and I want to look at the results of data. And sometimes I'm writing data and I want to make sure that I'm not making a mistake of doing a read, modify, write. So if I'm depending on the read module, I'm not going to be doing writes. If I'm depending on the write module, I'm not going to be doing reads. So that is a way of thinking about interface segregation kind of taken to the next level. Yeah. So the next principle that we're going

[00:21:02] Allan Stewart: to discuss is dependency inversion. There's a bunch of different ways to think about this. I like to think about it in terms of dependency injection, but the principle is actually slightly different from that. And there's a few different ways to go about it. But one that we were talking about earlier that I think is a really clever way to think about it is to never mention the name

[00:21:25] Dave Adsit: of anything concrete and volatile. Right. Yeah. So it's okay to depend on volatile, like string or list in your base class library. Right. Those are pretty stable interfaces

[00:21:41] Allan Stewart: that have been tested for a long time. Yeah. The to string or the, you know, the starts with or substring, like those things that they exist, they're stable. They're not, they're not changing.

[00:21:54] Dave Adsit: So you can name, you can name a string directly, right? You can say, Hey, I'm D this is a string. I'm using a string right here. you know, I've moved away from that rule. I can name a, I can name an abstraction, but I can't necessarily, I don't want to necessarily name the underlying class because maybe, maybe in this case I used an array list or my own thing. I wrote my own linked list, concrete class. You monster. And I don't want to depend on it directly because I'm not sure how many bugs it has in it yet. Yeah. Yeah. Which it's an interesting concept.

[00:23:04] Allan Stewart: Like as soon as you start using something that is volatile, well, anytime it changes, that's going to impact you. And so now I have to go and edit that code again. Right. And so the, the more widespread you let that be, the more often you're going to have to go and fuss with it. On the other hand, if it's abstract, right. Like you're getting away from that, that concrete, like even, even things that are somewhat volatile, like I think like going back to the idea of interfaces, right. Especially the keyword interface, those, those can change. And if they are volatile, then you're going to have a bad time because of the interface segregation principle that we were just talking about. But if it's even just relatively like the, the thing about abstraction is abstraction makes that, that, that, that, that, that, that, that, that, that, that, that, that, that, that, that, that, that, that, that, go to a new version of your favorite language where they have completely rewritten some of the base classes, right? Like that's possible. It's not that they will never change. It's just that they don't change very often. And like the, the, the scope of the change is much less than the code that you're writing. That is taking advantage of those things. Well, one of the

[00:24:43] Dave Adsit: things I like to think about here is the, the interface type or the abstract class type. I will put something like that inside of my domain because I'm depending then on an abstraction, right? Like you said, in my domain, like the core logic of my application, I know that I need the ability to save and load data. I know that I need the ability to request a tax rate from Okay. Well, I can take that. I can build that interface into my core logic and depend on that interface. That interface becomes stable, but the concrete class, you know, tax rates change all the time. As an example, maybe my concrete implementation, I separate that to a separate module that's compiled independently. And it's depended on through a binary interface, as opposed to being compiled into my app so that every week when I get the updated tax file, I don't have to rebuild and redeploy my application. I can just rebuild my tax module, which is a separate component and it implements the interface. And then none of the logic in my application has to change because I have an updated tax file.

[00:25:59] Allan Stewart: Right. Or you, or you decide, you know what, we're really going to go with one of those services. And so now we're going to change the implementation so that it makes an API call to some service. Yeah. To get that data for you. Right. And maybe it caches that for you. And right. Like, and, and all of a sudden there's this additional stuff that is happening, possibly, you know, completely, if you, you could, you could compare volatile stuff. Yeah. You could compare what you had in your file to what service a and B both provide from, from APIs. And you've got a caching layer. So you're not calling it all the time. And all of that complexity is completely hidden to the rest of your application. Yeah. It doesn't care. It just says, dude, somebody I it's like I come to the, you know, to the, to the tiller window. And I say, Hey, would you would you please give me a tax rate. And I don't care who's working behind the behind the desk that you know, they're going to toggle off to the back and find me a tax rate. And I don't care how many people they talk to, because they come back and they give me what I need. And so that you so your code, right, your your code. calling code doesn't have to change. It doesn't care where you got it from, just that you gave it the thing that it needed so it can move on with life. That's right. And so when we're doing

[00:27:21] Dave Adsit: dependency inversion, when we're at the code level and we're talking about injecting the concrete versions, the concrete implementations of these interfaces into our code, there's a lot of ways to do that. And you and I have both worked a lot in systems where we use constructor injection. Injection. So you'll say, hey, this class is defined in order to construct this class, you have to have a concrete implementation for the following three interfaces. Okay, great. I like that one. I think that one makes testing easy. It makes reasoning about the thing easy, et cetera, et cetera. Hopefully you are building a class that's highly cohesive so that you're using most of those dependencies and most of the methods throughout the class. That one is easy to reason about for me. There's other options. You can do method injection. Say you have something that's only used intermittently. Maybe we only calculate taxes under certain circumstances and we don't want to inject the tax service when we construct the payment processor. So instead, we only inject the tax service when we calculate tax. And so we call that method and the calculate tax method depends on an ITax calculator interface. Right. A concrete implementation of that.

[00:28:38] Allan Stewart: Yeah. Sort of the... Sure. I will calculate the tax for you and include it in this transaction. You just have to give me... You just have to tell me how. A tax calculator. Yeah. You just give me one of the things that is able to do that so that I can.

[00:28:54] Dave Adsit: I will do that for you. Yeah. So those are two options. Another option that you can use for creating dependency inversion is, I would say, substantially less common in the code I've worked on. You can use a plugin framework where you've got, you know, in your class that then the framework itself will inject the right one at runtime. That can work. Like I say, I haven't seen that work very often. One of the things I have seen more often in some of the code bases I've worked on is using a service factory or a service locator as basically a static service that is available anywhere in the code base. And you say, I need this thing. And so it gives you that thing in the moment that you're about to use it. And there are pros and cons to that strategy. One of the, I find that it's typically harder to test when I'm doing that kind of thing, because it's harder to set up the mock implementation of the mock implementation of the service locator that returns the mock, you know, you have to have mocks returning mocks. And I don't really love that. But at the end of the day, that's kind of what an IOC container is, is it's a, just a big factory that you say, Hey, I need you to give me either this concrete or concrete class or this interface. And it just goes and finds all of the things it needs in order to construct that for you. So in a lot of the applications that I've worked on, we've used IOC containers in version of control, in version of control containers, basically service locators to say, Hey, I, when I am creating this constructor, when I, when I'm calling this controller in the web layer, the constructor has done a lot of work on it. And so I'm going to go ahead and inject the concrete for them and inject them automatically so that when I get this controller, it has all the pieces it needs in order to do its job. When I am trying to call this workflow or this service, go get all the pieces necessary to build this service properly so that when I get it, it's fully constructed, but it is still depends loosely on other components. And you can actually, you can actually use IOC containers or service locators a single call, a single factory call, go get this for me. Yeah. My preferred way of working

[00:31:14] Allan Stewart: is with an IOC container and doing constructor injection. If you're careful about how you write your classes and I like, I try to make it so that all the things that could be injectable follow an interface and the constructor for the base class or for, for the actual concrete implementation only takes other things like it. Right. So it can also take dependencies, but I'm not, I'm not going to pass in anything concrete in, into those. And if you work that way, there's a C-sharp tool I really like called that I've been using for a while. It can just scan the assembly for me and say, Oh, all of those interfaces, every class that has a matching interface. Okay. Yep. They are now loaded up into the container ready to go for you. And so I had hardly to write any code as soon as the. Program starts up, it's all ready to go. That's right. But in any case, you, you still have like, even if you do a factory, right? Like a factory is one of the easiest ways to start in on that journey. Right. But then it's like, okay, the factory is now, I don't know what kind of a thing I'm getting, like the concrete type. I don't know. It's based on this interface that I'm getting, but, but like you say, you're going to very quickly then want to have an abstract factory. So I don't know. What kind of factory it is that I'm getting because different implementations might really matter. And then the other thing that I start to think about with, with dependency and inversion, when you've got this, you've got single responsibility, LISC of, and the interface segregation, then you can start using some other patterns in code, things like decorators, where you can take a, so like a simple example is like, I want to be able to log what happens whenever I call this API, but I don't want to change the API to add the logging. And because I follow these other principles, well, I could just inject one that has logging wrapped around it, which leads us to our final principle,

[00:33:25] Dave Adsit: which is the open close principle. And really we took this one out of order at the end because it depends on strong understanding of and implementing so many of the principles that we've So originally stated the open close principle was a software artifact should be open for extension, but closed for modification. That means of course, that once I've written code, I can never change it. Right, Allan?

[00:33:55] Allan Stewart: Not so much, which is why I don't, I love the idea of this. It like, this is a super powerful principle. The more I've thought about it, maybe it's like, this is the pinnacle of what you're trying to achieve by doing all the other four. But when it's stated that way, open for extension, but closed for modification. Like I look at that and I start thinking, yeah, what does that mean? Okay. Close for modification. Okay. That makes sense. I cannot edit it ever again. Open for extension. Okay. I need to add in a bunch of hooks into all the classes that I write because I don't know if they'll ever have to be changed. No, no, no. That's not the right way to go. That's not the right way. Right? So like if you are working at a company, and there is a user class, you can edit it. It's okay. You can edit the user class. Sometimes you need to, sometimes there's a bug in it. Sometimes you want to add additional features or functionality or something that didn't exist before. And now it's going to exist. It's okay to go back and edit your code. What open closed is talking about is that you should be able to change your system. So not just change a specific class or change specific code, but you can change the behavior of your system by adding new code, not just editing existing code. Right? So like that closed for modification, I, I like a little bit softer approach. There's like, you could, you could edit the existing code and that would change the behavior, but what if you could just add new code? Right? So going back to my last example, I want to add logging to this API call, but I don't want to change the API class that actually knows how to make an HTTP request and a little bit softer approach. But I don't want to add logging to that. I don't want to add logging to that. It's already messy enough. So I'm going to use a decorator pattern. And so for that to work, I have to have a good interface so that I can apply the decorator onto the interface. And if we've used interface interface segregation, well, that's nice because now I don't have to try and add logging to a hundred different methods, just maybe one or two that actually want to log. And because we've used Liskov substitution, I can hand you a decorated, API interface, and it will work the same as if you had just the regular API interface. Right? But the consumer doesn't care. It doesn't know. **Matt Staufferfield** And dependency inversion allowed us to hand one or the other interchangeably. And now we've reached this open closed principle where you say, oh, well, I just added logging by adding a new class, a new decorator class. I didn't edit the API.

[00:36:33] Dave Adsit: **Matt Staufferfield** Yeah. And I mean, I've used that for repositories. I've used that for APIs. **Matt Staufferfield** API endpoints. There've been APIs where I've started running into rate limits because I'm calling them too often. And so now I want to add some mechanism for caching the result. But again, you don't want to make the, you don't want to edit the original API interface because you are going to, you would end up adding a bunch of unnecessary complexity.

[00:36:57] Allan Stewart: And you don't want the calling code to have to know all the choices about when do you cache,

[00:37:03] Dave Adsit: when do you not cache? Yeah. So what that looks like is you end up with a class that injects a, an API. You you're like, I need the ITAX API implementation or the ITAX service. Great. Okay. I take an ITAX service. What I don't know is that inside the inversion of control container, you have already configured it so that when I ask for an ITAX service, I get, or I get a logging tax service and the logging tax service and its constructor depends on, an ITAX service. So great. Now I've got the logging one, but the log, the logging one, it, it asks for another implementation, another concrete implementation of the service. And it gets, instead of this, the tax service interface, it gets a caching layer that knows that tax rates should be cashed for eight hours or 24 hours. And so it pulls that down. And then that one depends on the concrete. And now we've added layer after layer, after layer of decoration to this original service without the original service, knowing about our changing in any way, so that we can manage the complexity of our system in a different direction. Right. And if we decide later, we don't care about logging. We haven't seen any errors in the tax service in two years, and we're filling our logs for no reason, and no one wants to review them, whatever. And we're going to just take that one out. And the system again is it's irrelevant. No one knows because it implements the same interface. So we can, add or remove decorators at our will.

[00:38:35] Allan Stewart: Right. And, and it's not limited to just decorators either, right? You could, there are other patterns, right? So a strategy pattern is a similar way that you can use these kinds of concepts, right? Like the polymorphism that you got through your interface usage and your dependency inversion allows you to just put in different ones, depending on different needs, right? So we talked about, you know, I want to change the of this system. Your example from before, I want to use a different database. You know what? I decided that SQL server is a little too expensive in its licensing, and I would rather use a Postgres database. Well, so we configure it and we hand it over the Postgres version of that database implementation and off you go. Now you're using Postgres. Well, and better than that,

[00:39:26] Dave Adsit: you know, we talk about, you know, what if you want to move to a new database? And everybody's like, yeah, nobody actually ever does that. That's basically true. Very rarely do people actually migrate to a new database. You and I have worked on projects where we have migrated to a new database. Yes, we have. And instead of just injecting the implementation of the interface for the new database, we have instead injected a dual write original system read implementation as part of its dependencies. It takes both of the repositories for the old database and the new one. Right. And we run that for a while. And now you're writing to both databases and reading from the old one. And at a certain point, you change which concrete you're using again. And now you're reading from the new one and writing to both. And when you're happy with that, then you remove all of that and you only return the concrete for the new database because you've successfully migrated all of your seamless way that no one even ever noticed. I love that. Yeah. Yeah. So you've put this

[00:40:35] Allan Stewart: proxy layer in between, right? Because you could. Open and close. You can hand off this proxy that knows how to talk to two different databases at the same time. And the calling code, it never knows the difference. Because one day it was handed one that used database A. And then the next day it was handed a proxy that can use both database A and database B. And then you can hand And it's doing some fun stuff like maybe comparing data as you go and making sure that when you're writing to the new database, and then you read from both. And you might only use the old value, but you can compare, make sure that your migration is going smoothly. And then the next day, you're just getting database B. And the calling code never had to change. It never cared.

[00:41:23] Dave Adsit: And if we depended on the concrete implementation directly, if we violated the rule that you don't call something both concrete and volatile, and apparently in this case, which database we're using is volatile. If it's changing every day, if we don't call new, we don't name the repository by its concrete name, all of these things become possible. But if we did say, hey, I'm using the SQL server tax repository. Well, now, now I can't do any of those things that allow my code to evolve in a seamless way.

[00:42:05] Allan Stewart: Right. It basically forces you to go in and modify, right? You have to modify the existing code, right? So going back to that idea, open for extension, closed for modification, I think you're open for extension by being flexible with these interfaces, with these interchangeable types. You're closed for modification because you don't have to, you don't have to go and rewrite all of those things everywhere. Everybody's depending on this abstract interface and they just use that. And it's, and so you've narrowed it down to, there's only one place. Maybe it's in your ILC container. Maybe it's in your abstract factory. Maybe it's in your plugin framework, whatever it was to pick the right one. And now you can make all of these changes, right? Another example, you, you have a report. Everybody loves their CSV But now there's a customer that comes along that says, well, CSVs are kind of hard to read. If I just like, I just want to open it and see the data in the report. I don't, I don't necessarily just want to do it in Excel all day. So could you make a PDF version that's, that's more readable? Like, sure, we should be able to do that, right? That might introduce some new pathways, right? There's a new endpoint that, you know, maybe there's some new code for PDF generation, but you should be able to reuse a lot of what was powering the data. And the CSV, you're essentially extending that, right? So, but if you, if you didn't follow these principles, and you just have this one class that handles all of these different responsibilities, going back to single responsibility, it's trying to do everything for multiple actors. Well, then it's hard to add in the PDF version, because all of the data access and analysis or processing of the data and CSV formatting, they're all in one place. And so you can't just swap out a report format interface, you have to go in and make a bunch of changes to the code. And, and that's harder. It adds a lot of time and complexity to the work that you're doing, making it more and more expensive. And so I think that starts to hint at kind of the last thing we wanted to talk about is just like the architectural significance of these principles. When you are able to add and change the code, you can go faster, you can deliver more value with less expense. And I think, ultimately, every system architecture, right, every software architecture that I have examined or looked at, that's the goal, right? Like there's some other there's some objective, it might be a business objective, or even like with open source, like there's something that you're trying to accomplish. And the software is the way to do it. And you want to be able to do that as quickly and easily and reliably and well. But because software systems have a tendency to want to change, right, their software, they're not hardware, we could have programmed logic gates to do all of these, and baked them all onto silicon wafers. But then you have to replace the silicon every time that you want to change a little bit of behavior. And that's, that's no fun. So instead, we create software, and we want the software to be soft, we want it to be malleable and changeable. These principles really help you make a system that is much more open to change a more malleable

[00:45:36] Dave Adsit: system. Well, and one of the things that I would add to that is that by following these principles, you also make a system that is more habitable for the developers who are working in it. When you have created clean interfaces that are narrow, you can more easily reason about the code You can more easily test the code that you're writing, you can more easily identify and resolve issues in the system. If you are following the principles that allow that lead you to structure your code in a way that is compatible with the solid principles. The alternative being you just kind of have a big ball of mud. I'm reminded of some of the early systems I wrote and some of the early systems I worked in, where we put all of the, you know, the, you know, the, you know, the, you know, the business logic and data access and everything either in the code behind file or the, or the controller file or whatever. And so you just basically have one big mess. And anytime you had to make a change, it was really hard to reason about how do I improve this system without breaking something else? Right. And if I can follow single responsibility and have separate components that are response or responsible to different actors. Now I've narrowed the scope of how much that's what I have to look at in order to make an enhancement. If I'm following Liskov substitution, then it doesn't matter which of the implementations I'm looking at. They do the right thing. They do the same thing. If I have inverted my control, if I follow dependency inversion principle, now my more stable things are depending on interfaces instead of less stable things.

[00:47:58] Allan Stewart: information so that you can make them better. We had both been reading or reviewing from the book, Clean Architecture, as we were preparing about this. And you reminded me when we were talking before our recording, there's an example of that right in the book. Uncle Bob and his people, they were working on the fitness testing framework that they wanted to build. They had this idea, they wanted to create it, and they weren't sure what database they were going to use. So they created an interface for it and started building against that interface. And once they started needing it to actually do something, I'm not sure, I can't remember the exact details, but I think maybe they started with some in-memory stuff and then they were like, well...

[00:48:45] Dave Adsit: It needs to persist between restarts, so let's write it to a file.

[00:48:48] Allan Stewart: Let's write it to a file because we always have a file system on all of these computers that we're using, but we don't always have a database running. And this was prior to Docker. And some of those things that made those much easier to just run a database. But we'll just write it to the file for now. It'll be good enough for now. And they went and they did all of this work and they got their program working and running and working for a lot of people and decided that, you know what, actually the file system is good enough. We don't need the extra complexity of database. And then they didn't have to choose which kind of database because it didn't matter. Everybody has a file system and it was good enough.

[00:49:28] Dave Adsit: Well, and then... And that is one of the things we talked about. We've talked about in previous episodes is that one of the ways that we can accelerate development is by maximizing the work not done. Don't do unnecessary things. And if we don't do unnecessary things, we can do more of the things that are necessary in a shorter timeframe.

~/podcast/episodes/052-solid-principles $ cat ../../copyright.txt

Copyright © 2026 - Crafting Code Podcast