Lesson 4 of 15
Simplicity is Complicated
Learn how to make your test code and production code simpler with some simple rules.
In this lesson
Roy Osherove explains why he solves things in the simplest way possible (external link, opens in new tab) but not simpler, since planning too much for the future can make the code bloated and complicated. He measures simplicity visually: a line at the beginning of a method is simpler than one in the middle and less nesting is simpler, as when students replaced all the new delimiters with commas in the first line of the string calculator (external link, opens in new tab). He describes TDD (external link, opens in new tab) as breaking down one assumption at a time while wearing two hats, the laziest developer who writes the code and the person who writes the next test that proves an assumption wrong.
Transcript
[00:04] (external link, opens in new tab) Now in the test-driven development (external link, opens in new tab) process, one of the things that I keep pushing for is simplicity. I keep saying solve things in the simplest way possible (external link, opens in new tab) but not simpler. And I want to explain what does it mean when I say that something is simple. Now the basic idea is that you shouldn't plan too much for the future because the future is probably going to change. So the risk is that you invest too much in having big upfront design of something and the code becomes bloated and and complicated and then you don't even get the thing that you planned for.
[00:39] (external link, opens in new tab) That's a risk that usually we we take a lot. So simplicity is there to try to solve that and also to help us create tests that we might not have thought about. So how do I measure simplicity? It's very hard to measure that but I try to measure it in a visual way. If we have the definition of a method then simplicity at the visual level is something that is shorter and
[01:09] (external link, opens in new tab) Less wide. I'm going to say so it has to still be readable. But if you think about it, if you have an existing method and to solve the current test, you want to you can either add a line at the middle of the method or add a line at the beginning of the method. Which one would be simpler? What do you think? At the beginning? Why? >> That's where you start reading. >> That's where you start reading. Well, it's instinctive to say, "Yeah, it's simpler." But if you think about it, it is technically simpler because if you put a line in the middle of a method,
[01:47] (external link, opens in new tab) Until you get to that line, you have to go through other lines of code. So to be able to get to that line, you have to perform more operations. More operations is more complexity. If the solution is just a single operation, it's a simpler solution. If your solution needs you to put the line exactly in the right place between two different pieces of execution, it's a more complicated solution. Make sense? At the same time, the more nested something is inside ifs, inside loops, the more complicated it is.
[02:20] (external link, opens in new tab) The more complicated it is visually, the more complicated it is probably in terms of complexity. And we can talk about there are algorithms for measuring the complexity of code. Do you do you remember what that's called something complexity? I don't remember the the name now but I do remember it usually but not in mornings. In mornings I usually remember only 50% of the things. So I try to make it the important stuff. So the more nested things are the more complicated they are because they have to happen inside a loop inside an if inside a try catch.
[02:57] (external link, opens in new tab) That's more complicated than not having it inside an a for loop at right. So the more to the right things are the more complicated they are. So for example, one of the ways that some that some of my students have solved the string calculator (external link, opens in new tab) problem with changing the parame the the the the delimiters is that instead of parsing for the limiters and splitting based on multiple different delimiters, I had a couple of students that just said, okay, when I have a different delimiter, the first line of the method is to replace
[03:33] (external link, opens in new tab) All the new delimiters with a comma. And now I don't have to change any line of code because I can already handle commas. Right? So once I have a common delimiter, the first line of code is to replace all the new delimters with commas. And that's it. If you think about it, it's a very very simple solution. It's actually much less complex than trying to find multiple delimters. And so the first line of the method becomes the focal point of changing the the delimiter problem. And what happens is that the that line becomes like cleanup, right?
[04:07] (external link, opens in new tab) Get rid of the new lines, prepare the string so that you can handle it just like you used to handle it when it only had commas. So you evacuate the problem into a separate method and it's just the first line of code. And that's a very very simple way to think about it. So when you approach a solution and your solution is to add something at the middle of the method, your approach should be could there be something at the beginning of the method that I could do that would be simpler and that's something that's hard to come by this instinct.
[04:40] (external link, opens in new tab) That's one of the things that we will practice more and more questions. So that's one way to look at simplicity. But there's another way. Test-driven development is the continuous breaking down of assumptions in your code. If you think about it, when you write code without TDD, what we are trying to do as developers is to write code as generically as possible. A code that can handle almost anything.
[05:10] (external link, opens in new tab) It doesn't know what's going to happen. Whatever the input is, it could be strings, it could be commas, it could be anything, it could be whatever. But when we try to do things in a test-driven way, one of the best ways to accomplish small steps is to decide on having as many assumptions as possible in the code at the beginning. An example is the input will always be empty. That's the first test. So I'm assuming the input will always be empty. And then when that test passes, because I'm just returning zero,
[05:44] (external link, opens in new tab) I don't need to check an if because I'm assuming that it will always be empty. Then I can then write a new test that says, actually that's wrong. If I send in a number, that test will fail. And so I'm actually telling the person who wrote the test that they had one assumption too many. And so TDD is a continuous effort in breaking down one assumption at a time and each test breaks down one assumption hopefully. It helps if you think about writing test-driven development when where you are the person who writes the tests and the code but it's actually two people.
[06:22] (external link, opens in new tab) You wear two different hats. The person who writes the code makes as many assumptions as possible is the laziest developer in the world has to go home in five minutes. Okay. What would they do? They can just assume. You didn't tell me it was never going to be empty, right? You just told me, look, here's a failing test with an empty string. Okay, there are no other tests. So, I'm just going to assume that's an empty string always. No need to check with an if. Okay, bye. I have to go home. And then you switch hats and say, ah, actually that's not good enough.
[06:53] (external link, opens in new tab) That assumption is not always true. For example, I send in a number. And then you switch hats again, okay, a number. So I can assume that if it's not empty, it's definitely that number. And then you write another test says, you can't always assume it's always one, what if I send in two? What if I send in two numbers, etc., etc., etc. So think of it like a wall where you take it down brick by brick, and each brick is an assumption. And what you end up with is the same generic code that you would have ended up with anyway.
[07:25] (external link, opens in new tab) But every piece of that code is tested and has a reason to exist. There was a test somewhere that says that assumption cannot be made. Yes, you probably need to loop because you don't know how many numbers there are. But yesterday when we talked about the string calculator, I urged you not to loop. And that's one of the reasons because I wanted you to make the assumption that there are exactly two numbers. And that would force you to write a test that said you can't assume there are always two numbers. Here's three numbers.
[07:57] (external link, opens in new tab) Okay, you can't assume the delimiter is always a comma. Here's a different delimter. You can't assume that the numbers are a single character. Here's a number with two characters, etc., etc., etc. So, the more assumptions you learn to make in your code while test driving, the more test you will think about because you are critiquing your own code and saying, I don't like that code. And when we had that first instinct of saying just return zero, just return one, that instinct is the instinct that will cause you to write the test.
[08:29] (external link, opens in new tab) It says that's a bad assumption. But we want to make that assumption because we it forces us to write the tests. The the the worst way to write test is when the person who's writing the tests is the person who's writing the code that's being tested. I think we all know that developers should not be trusted to test their own code because they are not objective. By making the assumptions, we are forcing ourselves to be somewhat objective about our code and testing for failure instead of testing for success,
[09:05] (external link, opens in new tab) Which is the best way of testing, making sure the code doesn't work and then proving the test wrong instead of the other way around. So that's the that's kind of the way because we know we cannot trust ourselves because we always want things to work. We make as many assumptions as possible and we're breaking down through tests. And that's that's one way we make sure that our tests are actually testing the right things and that we don't forget to test some