All about the operation lens
In this episode, I introduce the operation lens, its questions, and its goal of capturing the use cases of your software.
[00:00:00] All about the operations lens.
[00:00:03] Hello, my name is Eric Normand, and this is my podcast. Welcome.
[00:00:09] So I'm writing a book about executable specifications as a way of doing domain modeling, and I'm organizing the material into different perspectives. I'm calling them lenses. Each lens gives you a way of looking at your software and the problem it's trying to solve to get more information so that you can make better design decisions.
[00:00:44] And today I'm talking about the operations lens.
[00:00:50] In the operations lens, we ask the question, what are the use cases of your software?
[00:00:58] So we can list the use cases. The idea is once you list them, you kinda have a broader sense of all the stuff that your software is gonna need to do.
[00:01:12] I think one of the mistakes we often make when writing software is we'll just pick one use case and make that one work. That's a good strategy when you have no idea how you're gonna solve it. And you're looking at this long list of use cases and when you read them, you're like, well, they're, each one is just making it more difficult and more complicated, and I don't even know whether we need all of these, and so I'm just gonna pick one. I know we need and get started on that and just do a simple thing and then add one and add one and add one.
[00:01:56] The trouble with it is that's a design of no design. Just doing the simplest thing for each one is one of the ways that we get undesigned code. I know there's an movement that's taken over software. One of the common beliefs in it is that you should just do one thing at a time and do the simplest thing that could possibly work and then clean it up as you go. And I don't want to go into it. This is a whole other episode, but I don't think that cleaning up is the problem.
[00:02:34] It's not that your code is messy. It's that your code now doesn't represent, in a very good way, the thing that it's trying to represent. And we forget to talk about that. I named my methods wrong and my methods are getting too long and I'm nesting my ifs too deeply. We're just talking about code, like the code is the problem.
[00:03:05] The real problem is why did it get messy? Maybe because it's very hard to represent the thing that this new use case that you've now looked at for the first time, it's very hard to represent what it's asking you to do.
[00:03:20] I give the example a lot that you might have a list of use cases and one of the use cases, let's say you're writing a contacts app. One of the use cases is we have to sync between devices. So you had some easy use cases, like, oh, we need to store the address and phone number. Maybe we need to search it, and then like down the list, oh, we need to sync between devices. If you don't get to that until later, it's really hard. Syncing is really hard to do after the fact. Why? Because there is no reason to gather the information you need to do sync correctly. And by correctly I just mean don't lose data.
[00:04:13] So if I have a contacts thing and I update something on the web interface on my laptop, and then I disconnect from the internet so it doesn't have a chance to sync, and I then open up my phone and type something else in and the same person, and then we sync. Which one takes priority? Which one overwrites the other? Do we even have a way of deciding that?
[00:04:51] And you say, oh, you just used a timestamp. Well, yes, if you had a timestamp, but did you have a timestamp? And what about all the stuff from before you implemented this use case? You didn't record a timestamp. You didn't record what device it was written from. You just just wrote the person's phone number in a, in a field somewhere in a database row. That's it. You didn't capture the metadata you need to be able to sync. Okay, so I don't want to go too far into that.
[00:05:24] The idea with this lens is that we can get a view of all of these use cases without overwhelming ourselves, like the common mentality says we will.
[00:05:42] Yes, there are gonna be use cases that will change. There's always change. If we can figure out the ones we know we're gonna want, we could put them in. Now, how do we do that in a way that doesn't turn into overdesign, over architecting and especially the architecture astronomy, the becoming architecture astronauts where you are trying to build for use cases that are basically imaginary.
[00:06:22] Now this is where the lens kind of interact with each other. Cause we have a lens that's called domain slash reality. So you, you do have this lens asking you the question, is this a real problem? Do you really need to solve it? Is it imaginary, right? So I'm not gonna go too far into that side, right?
[00:06:49] But what I do wanna avoid is overdesigning it and not being able to keep it grounded in the reality of it has to be able to run and do the stuff it needs to do. And so how do I do that? What I turn to, and what I'm gonna talk about in the book is again, this idea of executable specifications.
[00:07:14] We can start with the function signatures. The function signature is just the first line of the function, let's call it. It's the name of the function, because that gives you a lot of semantic information about what the function does. And then the types of the arguments and the return type. So the types of the arguments those show you, what are all the inputs that I need for this use case? And then the return type tells you, what is this use case going to result in? What is the result of this operation?
[00:07:54] And so you can write out the use cases as function signatures, and you can write the body later once you've worked out what are the actual signatures because again, this is an iterative process.
[00:08:05] You write down the function signatures. That doesn't mean it's the final one. It can change while you're doing, while you're going through this process, but you write it down and it gives you this view. What information do I need to do this use case? So in the case of syncing, you can see, well, I'm gonna need the, the key and value from this device, but I also need to know like what version it was working on and maybe what device it was on. By doing this, you work out some of the harder problems, the more difficult questions.
[00:08:48] Why do we write 'em all out? All the use cases? Let's say all the real use cases, right? You don't have to tackle the whole thing at once. That's not the point. But you write out the ones that are important, that you know you want. And you do that so that, one, you can get a consistent view of it. Since you're not implementing it, you're doing about 10% of the work, but it's the most important 10%, right? You're taking a shortcut basically. Instead of implementing it, and then realizing three functions in, oh, I need to do it a different way. I need to use a hash map instead of a list because of this use case, I gotta go change my functions that I've already written. No, you start with just the function signatures and that gives you this view that like, maybe I'm forgetting something, or maybe it's not as consistent as I thought, or like, wow, this use case needs this new information that none of the others need. Maybe there's something going on there and I'm missing something.
[00:09:57] And it also lets you rank them by complexity. Which ones are harder to implement, harder to get right than the others. You can go back in the podcast and find other episodes about this. My argument is that you want to do the most complicated ones first.
[00:10:22] We often think, oh, we'll do the easy ones first because they're easy, and we'll just get them out of the way. And now we'll get to the tough ones. But they're tough for a reason. They're tough because you haven't answered the questions, you haven't figured out exactly what you're gonna need, and so they have the most chance of having a big discovery. Oh, we needed a hash map instead of a list. And so all the work you've done to write all the easy ones, it has to be redone.
[00:10:59] I'll give an example. Very common in a lot of programming circles. People will write a class with a few fields and then they write all the getters in setters. Cuz those are easy, just get 'em out of the way. But after you start writing the more complicated methods, you realize, oh, I don't actually need this field. I need this other field. Or this needs to be these three fields combined into one thing and like now all your getters and setters, like they were pointless. Why did you even do them?
[00:11:36] Now I know there's a button you press and it generates your getters and setters for you. Sure. So it's not like that much work wasted, but the principle is the same. It's wasted. And if you had simply started with the more complex ones, you would've realized. You wouldn't have wasted all that work.
[00:11:56] Lemme just put it another way. The easy ones are so easy, you could do it anyway anytime. Why not put 'em off till the end? Do the hard ones first.
[00:12:06] And then there's a couple more things. One is we want to take these use cases and we're turning them into function signatures. We'll call 'em operations. You wanna find a minimal set. You don't want to have a function for every single possible thing you could do and kind of custom write it. You wanna see ways of implementing some of the operations in terms of others. And then find the set of basic operations, that aren't implemented in terms of the others, that is sufficient to implement all of them. And what that does is it minimizes the surface area of your domain and helps you find an elegant solution.
[00:13:08] We're talking about design here. Elegance does have to come in. This the reason we find a minimal one. I don't know if I really have a good handle on why it's important, but I find that it helps me know I'm on the right track when I can start seeing, oh, these things are just the reversal of that one. And I start seeing symmetries, and patterns, balance between things. It makes me feel like we're on the right path. We're finding a formal model that is capturing something deeper about the domain.
[00:14:01] It's the same aesthetic that we see in physics models. So let's say in Newtonian mechanics, it's just three equations. Oh, you have Maxwell's equations, or did these equations kind of describe all of electromagnetic phenomena? You know, we have this aesthetic sense that the complexity doesn't lie in the bottom of it. The complexity is mostly based on the size and the interaction of everything. The rules themselves are not that complicated, and when we have these minimal sets, it feels like, we're finding something deeper. It's very hard to prove. I don't know if I could prove it, but it feels like, oh, we're finding something deeper, that there's a structure to this domain that has those same qualities as a Newtonian mechanics. The domain has structure and we're capturing it.
[00:15:16] Another thing that we want to talk about in this lens is total functions. We want our functions to be total. I went over this before in another episode, but briefly, a total function, it has a valid return value for every combination of valid inputs. Meaning the operation, whatever you pass it, if it's a valid input, it should give you the right answer. And we want that because once we start building operations in terms of other operations, it becomes harder and harder to make sure that we are passing in the right kinds of arguments.
[00:16:02] The composition makes it harder to reason through what is going on. Like is this argument that finally passes through like this function and then returns out this way and then passes to this function, is it gonna be a valid thing? So we want our functions to be total because they're much easier to compose.
[00:16:27] And it also means we're onto something. It means we found some relationship between these arguments and this return value that holds for all arguments, the truth. And we can rely on that truth.
[00:16:52] Now, ways that functions don't respect this, that aren't total. It could throw an exception for certain arguments. It's very common. Divide by zero. It's non total. Or, oh, it doesn't work for negative numbers, right? Or index out of bounds. You ask for a value that is not in the vector.
[00:17:25] And in the episode about total functions, I talked about the three ways that you can, that you can make a function total. I'll just list them here. One is to restrict the arguments. You find a smaller set of arguments that are valid. You enhance the return value to include all the cases that, let's say were an error before, right? So you include the error as part of the return value, the return type. And finally you change the meaning of the function. So the substring takes a string and a start Int and an end Int. What happens when the end is past the bound of the string? You could throw an error. This is a common thing to do, but you could just say, well, I'm just giving you the intersection of your bounds and the string. So the end doesn't mean I'm gonna try to find all the way to the end. It just means I'm gonna give you the intersection. And so if there's no intersection, I just return the empty string.
[00:18:51] Once we have the function signatures and we've figured them out and they're correct, we've gathered all this information about what we want the operations to look like, what they need to look like, just based on their implement implementation needs, then implementing them is actually quite easy. We've done this semantically hard work already, figuring out what are their inputs, what are the outputs. And it's implementing them that's kind of straightforward, or you hope it is. If you've done the work upfront, it should be pretty easy.
[00:19:30] My name is Eric Normand. This has been another episode of my podcast. Thank you for listening, and as always, rock on!