Crafting Code Podcast
$ cd episodes/052-solid-principles
~/podcast/episodes/052-solid-principles $ ls -1a ~/podcast/episodes/052-solid-principles $ cat episode-summary.txtCoupling 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- Clean Architecture. Robert C. Martin.
- Clean Code. Robert C. Martin.
$ 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, lately, I've been thinking about picking my battles and deciding which hills I'm willing to die upon.
[00:00:31] 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 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 in his books is a lot of the contribution that he gave to it. But I feel like 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 so that they could spell a word instead of just being a collection of abstract principles from software developers of yesteryear. So I think that one of the things that's important to look at is that, you know, the fundamental guidance, the fundamental considerations for good software development are coupling and cohesion. We talk about them all the time, right? 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 gives some deeper insights, some deeper principles that we can use to help us understand the concepts of coupling and cohesion and how the 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.
[00:03:14] Allan Stewart: Agreed. There's some principles that as we 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 well. Well, functions in particular should strive to that ideal, but that's different than single responsibility. Single responsibility is really about the actor.
[00:04:06] Dave Adsit: Right.
[00:04:07] Allan Stewart: 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 someone would make that leap, right? If you say a class, one of the ways it's described is often, it should have only one reason to change. Well, that's a little bit too vague. And so talking about having responsibility to one actor, I think is actually an important distinction from having one reason to change. 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, the, the consumer of the, of the module that you are responsible to. So you might say, if you're building, um, an e-commerce system, you might say one of the actors is the marketing department. And the, another actor is the head of finance. And those are two people with two different 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 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. Right. Well, I was thinking about a much more insidious type of 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 in 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 a lot of opportunity for single responsibility to help you narrow those scopes 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.
[00:08:28] Dave Adsit: Yeah. 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. It meant. 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 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 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 it mean for them to be interchangeable because there's a few different ways you can do it and some of them
[00:10:13] Dave Adsit: violate the Liskov substitution principle. I, 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 create implementations. If I'm building software for distribution, I may say, "hey, I support three different relational databases." And here is the, you know, going back to the previous example, I want to load or save an object. I might want to save it in an Oracle database or a Microsoft SQL Server database or a MySQL database. And so I might have three implementations of 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 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. 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:22] 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 to, 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 our code, like in your example, just pretty simple, save a record, load a record, that kind of thing is 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# 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 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 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. 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 repositories I've used. 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. Usually, if you adopt a framework, it's going to bring a bunch of stuff with it. So much stuff. 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 you don't use the tax calculation component because 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 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 that has something I do need, now I've got twice as much stuff that I'm updating all at once. 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 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 consumer.
[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# 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 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. So it's popular to see libraries that, oh, don't worry, I will help you with X dependency you're, you're going to call this ap 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, when they do an update? Right, exactly the same thing as with your framework, you, 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#, 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 changed its signature. Recompile.
[00:19:42] Dave Adsit: 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 the command objects with the command interface, you know, update this record, delete this record, send this email. And the query interface, which has a, you know, query object with the query interface, and you ask it questions. How many emails have I sent? What is the current state of this record, et cetera, et cetera. And so that is a pattern that is used to separate the, even more thoroughly, you separate those two concepts from, hey, sometimes I'm reading data and I want to look at the, you know, 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, right? So if I'm depending on the read module, I'm not going to be doing rights. If I'm depending on the right 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.
[00:20:59] Allan Stewart: So the next principle that we're going to discuss is dependency inversion. There's a bunch of different ways to think about this. I like to think about it it in terms of dependency injection, but it, 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 of anything concrete and volatile. Right.
[00:21:29] Dave Adsit: Yeah. So it's okay to depend on things that are not volatile, like string or list in your base class library. Right. Those are pretty stable interfaces that have been tested for a long time.
[00:21:43] Allan Stewart: Yeah, the toString or the startsWith or substring, like those things that they exist, they're stable. They're not changing.
[00:21:54] Dave Adsit: So you can name a string directly, right? You can say, "hey, this is a string. I'm using a string right here because it's not volatile. It's concrete, but it's not volatile." So on the other end, maybe you don't know that you have a list or need a list. Maybe you're looking at a collection. And so you would say, "I want the ICollection interface." And so now it is probably not volatile, but it's not concrete either. Like if I'm depending on an interface instead of a concrete class or an abstract class instead instead of a concrete class, 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, new monster. And I don't want to depend on it directly because because I'm not sure how many bugs it has in it yet.
[00:22:58] Allan Stewart: Yeah. Yeah. Which it's an interesting concept. 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 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 concrete. Like even things that are somewhat volatile, like I think, like going back to the idea of interfaces, right, especially the keyword interface, 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 thing about abstraction is abstraction makes things more stable because the little details don't matter, right? Like those are incidental to what you're trying to accomplish. And if you have a good abstraction, it makes it less volatile. And so I think it's not really, is it yes or no binary volatility? I think it's more of a spectrum. And the more like, cause even, I mean, even base classes can change, right? You could 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.
[00:24:42] Dave Adsit: Well, one of the things I like to think about here is 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 a third party. Okay, well, I can build that interface into my core logic and depend on that interface. That interface becomes stable. Table. 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 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 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 to some service 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. You could compare what you had in your file to what service A and B both provide from, from APIs and you, 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 because it doesn't care. It just says, "dude, somebody." It's like I come to the tiller window and I say, "hey, would you please give me a tax rate?" And I don't care who's working behind the desk. They're going to toddle 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 your code, right? Your 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.
[00:27:19] Dave Adsit: That's right. And so when we're doing 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. 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 I tax calculator interface. Right. A concrete implementation of that.
[00:28:38] Allan Stewart: Sort of the, "sure, I will calculate the tax for you and include it in this transaction. You just have to give me 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. Yeah. So there's, 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, pluggables 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 codebases I've worked on is using a service factory or a service locator as a basically a static service that is available anywhere in the codebase. 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 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 just a big factory that you say, "hey, I need you to give me either this 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 and version of control containers, basically service locators, to say, "hey, when I am creating this constructor, when I'm calling this controller in the web layer, the constructor has dependencies on several services. Go find the concretes 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 use IOC containers or service locators to build fairly complex object graphs with a single call, a single factory call. "Go get this for me."
[00:31:11] Allan Stewart: Yeah, my preferred way of working is with an IOC container and doing constructor injection. If you're careful about how you write your classes, and 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 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# tool I really like called Scrutor 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 to 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 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 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 dependency and inversion, when you've got this, you've got single responsibility, Liskov, and the interface segregation, then you can start using some other patterns in code, things like decorators, where you can take, so like a simple example is like a logging decorator, right? 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, which is the open close principle.
[00:33:29] Dave Adsit: 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 other principles, right? 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. Like this is a super powerful principle. The more I 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. Closed 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, no. That's, that's, 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 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 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. It'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 send it off and get the result and deserialize it. 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 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. And dependency inversion allowed us to hand one or the other interchangeably. And now we've reached this open close principle where you say, "oh, well, I just added logging logging by adding a new class, a new decorator class, I didn't edit the API."
[00:36:33] Dave Adsit: Yeah. And I mean, I've used that for repositories. I've used that for API endpoints. There have 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 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, when do you not cache?
[00:37:04] Dave Adsit: Yeah. So what that looks like is you end up with a class that injects an API. You're like, "I need the ITACS API implementation or the ITACS service. Great. Okay. I take an ITACS 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 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 logging one, it asks for another implementation, another concrete implementation of the service. And it gets instead of 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 or changing in any way, so that we can manage the complexity of our system in a different direction. 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. We're going to just take that one out. And the system, again, it's irrelevant. No one knows because it implements the same interface. So we can add or remove decorators at our will.
[00:38:36] Allan Stewart: Right. 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 behavior 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.
[00:39:24] Dave Adsit: Well, and better than that, you know, we, 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 Yes, we have. 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 data in a seamless way that no one even ever
[00:40:31] Allan Stewart: noticed. I love that. Yeah. Yeah. So you, you, you've put this proxy layer in, in between, right, because you could open and close, you, you can hand off this proxy that knows how to talk to two different databases at the same time, and the calling code never, 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 B, and it's doing some fun stuff like, like maybe comparing Comparing data as you go and making sure that when you're writing to the new database, it's, and then you read from both. And you might only use the old value, but you can compare, make sure that like your migration is going smoothly. And then the next day you're just getting database B and, and the calling code never had to change.
[00:41:22] Dave Adsit: It never cared. 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 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 go and rewrite all of those things everywhere. Everybody's depending on this abstract interface, and they just use that. 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 have a report. Everybody loves their CSV downloads, 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 necessarily just want to do it in Excel all day. So could you make a PDF version? 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 CSV. You're essentially extending that, right? So, but 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 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 your system, 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. There's some objective. It might be a business objective or even with open source. 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? They're 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 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 system.
[00:45:36] Dave Adsit: 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 that you're using. 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 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 business logic and data access and everything either in the code behind file 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? And if I can follow single responsibility and have separate components that are responsible to different actors, now I've narrowed the scope of how much code 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. And now that makes it easier for me to enhance the code. And maybe the unstable thing for me is my interface. Maybe I don't know. Maybe at the beginning of the app, I don't know if I'm making a desktop app or a web app or a mobile app or whatever. Ever. That's probably not the case, but it is possible if you follow these principles to defer that decision until the last responsible moment. And I think that's one of the important things about a good system architecture is that it allows you to defer decisions until you have more information so that you can make them better.
[00:48:00] Allan Stewart: We had both been reading or reviewing from the 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 FitNesse testing framework that they, 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 a interface for it and started building against that interface. And once they started needing it to actually do something, I don't, I'm not sure. I can't remember the exact details, but I think maybe they started with some in-memory stuff
[00:48:43] Dave Adsit: and then they were like, "well, 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 that's, cause 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 like Docker and to 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 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.
Copyright © 2026 - Crafting Code Podcast