Ruby
SOLID and TDD, Sitting in a
Summarized using AI

SOLID and TDD, Sitting in a

by Mike Nicholaides

In this video, Mike Nicholaides delivers a talk at the Rocky Mountain Ruby 2013 event on the relationship between Test-Driven Development (TDD) and the SOLID principles of object-oriented design. The core focus of the presentation is the assertion that while TDD is beneficial for improving code quality, poorly structured tests can lead to suboptimal code designs.

Key Points Discussed:

- Introduction to TDD and SOLID:

- TDD, or writing tests before code, is a practice aimed at promoting well-designed, maintainable code.

- SOLID is an acronym encompassing five key design principles that help in achieving good software structure.

  • SOLID Principles Overview:

    • Single Responsibility Principle (SRP): A class should have only one reason to change. Classes should be small with focused responsibilities.
    • Open/Closed Principle (OCP): Software entities should be open for extension but closed for modification. This suggests using mechanisms like inheritance and dependency injection to add functionalities.
    • Liskov Substitution Principle (LSP): Objects should be replaceable with instances of their subtypes without affecting the correctness of the program.
    • Interface Segregation Principle (ISP) and Dependency Inversion Principle (DIP): Abstractions should be preferred over concretions to enhance flexibility and maintainability.
  • Connection Between TDD and SOLID:

    • Writing tests first can help clarify design intentions and improve code maintainability when done correctly.
    • Poorly structured tests can lead to complex, brittle code, undermining the benefits of TDD.
  • Practical Example:

    • A case study of a Notifier class was shared, which initially handled multiple responsibilities (emailing, SMS, Facebook notifications) leading to convoluted design.
    • The presentation illustrated the transformation of this class through proper abstraction and attention to SOLID principles, making it easier to test and maintain.
  • Takeaways:

    • TDD is a valuable approach, but effective abstraction and separation of concerns are vital for achieving quality code.
    • Developers should aim for small classes and clear abstractions to facilitate easier testing and modification.

In conclusion, Nicholaides emphasizes that by practicing TDD while adhering to the SOLID principles, developers can produce code that is not only testable but also robust and adaptable to change, resulting in a pain-free coding experience.

00:00:30 Hello everybody.
00:00:31 Um, hi! My name is Mike Nicholaides.
00:00:36 I'm going to be talking about testing today.
00:00:40 A little bit about myself first.
00:00:43 I'm a partner at PromptWorks; we're a small consultancy in Philadelphia.
00:00:49 We do a lot of work in Ruby on Rails, JavaScript, and infrastructure automation, like Chef, that kind of thing.
00:00:51 Besides just developing products for our clients, we engage in a lot of pair programming with them.
00:00:57 That's because when we leave, you know, when the project is over, we want to continue them on the path of test-driven development, good object-oriented design, and a solid process.
00:01:04 That's somewhat of an inspiration for this talk.
00:01:06 Um, testing is something that's important to the way that we work. Excuse me, I work with a bunch of super talented folks here, and we have some really great clients, and I'm honored to be a part of that.
00:01:17 All right, so let's get started. You've probably heard it said that if you do TDD— that is, if you write your tests first before you write your code— you're going to end up with well-designed code. Your code will be easy to understand, easy to change, easy to reuse, and of course, correct.
00:01:34 But I'm here to tell you that that's just not true. I can show you many examples of tests that were written first. Some of them look like this:
00:01:51 Can you guys in the back read that? Yeah, well, it doesn't matter because if it was in front of you, you probably still wouldn't be able to. You know, if you write tests like that, your code that makes it pass is going to look something like this.
00:02:07 Um, if you buy into that idea that writing your test first is going to give you good code, don't be surprised when you find yourself ensnared in thorny, brittle code and tests that are even worse.
00:02:27 You see, well-tested code— or I'm sorry, well-designed code— is easy to test. Inherently, testing is hard when code's not well designed.
00:02:43 If we write our tests first, the design of our code is going to match the design that our tests expect. So, you need to know how to listen to your tests and how to use your tests to describe good design.
00:03:00 So I'm going to show you what I mean.
00:03:03 But first, let's talk about what I mean when I say good code.
00:03:06 If you're unfamiliar with SOLID, it's an acronym for five design principles.
00:03:09 These principles were identified and collected by Uncle Bob Martin in the early 2000s. He noticed that the good software he saw was easy to maintain, easy to extend, and that it all seemed to follow these five principles.
00:03:25 So I'm going to go over them briefly and kind of put some emphasis on what I think this means for Ruby development.
00:03:30 Then we're going to do some other stuff and get into some tests.
00:03:34 I'm going to take them in a different order than the acronym because I think it's easier to explain that way.
00:03:36 So first, let's get this one out of the way. The 'I' in SOLID stands for Interface Segregation. I don't think it belongs with these four fundamental principles.
00:03:42 The other four principles provide benefits that go beyond just making compiling statically-typed code bases faster. So, like C++ and Java, we don't have that problem. Good for us.
00:03:55 The 'D' in SOLID stands for Dependency Inversion. Don't worry about the name; it's sort of a throwback to implementation details in the statically typed world, but the thing to remember is this: depend upon abstractions, not concretions.
00:04:06 So, what are concretions? What are abstractions? Let's look at some code that I think can help explain a little bit better. This method depends upon a concretion— it's calling file.open directly.
00:04:29 Okay, well, let's compare that to depending on an abstraction. In this version, the 'save my game' method depends on the abstraction of what we call a persistent store. So, instead of using file.open directly, all it has to know is how to call a save method and pass it its game data.
00:04:41 So this file store is an abstraction, and the file.open part is the concretion. For everyday Ruby development, I think that the Dependency Inversion principle is telling us we should hide concretions behind abstractions.
00:04:58 Next up is the Open/Closed Principle. This one says that software entities should be open for extension but closed for modification. Entities being like methods, classes, and modules, that sort of thing.
00:05:10 In more concrete terms, this means that we should have swappable pieces for dependencies, collaborators, and complicated logic. You're actually already familiar with techniques that let us abide by this principle.
00:05:23 Blocks, for instance. So, think of array.map. We pass that method a block to change the way it works. So the method is open for extension, but the closed for modification part means that we're not changing the way the map method works for everybody else, just in this particular case.
00:05:38 Inheritance, too. We can extend any class by subclassing it, and this doesn't change the original class. Dependency injection allows us to swap out dependencies and collaborators, letting us change how an object works without modifying the class or the source code.
00:05:54 So let's look at our example. This is an example of dependency injection. In this version, the 'save my game' method takes a persistent store as a parameter.
00:06:09 Instead of hard coding it to use this method, we can pass in any kind of persistence store, and that's what dependency injection is: we're passing in the dependency instead of hard coding it.
00:06:21 So if you're confused about what that meant, don't be scared by the weird name and the reputation that other communities have given it.
00:06:35 So this method is now open for extension. You can change how it behaves by passing in objects that behave differently. For everyday development, I think this principle just means we should be making our entities extendable— make your methods and make your classes extendable.
00:06:53 And your objects. Next, we have the Liskov Substitution Principle. You've probably heard this principle described in terms of classes and subclasses.
00:07:05 But in Ruby, classes actually have nothing to do with it. In Ruby, we use duck typing. Even if you're using subclasses and stuff, everything is duck typed. So, like Lionel said earlier about duck typing, if it quacks like a duck, it is a duck; that's what duck typing is.
00:07:24 This principle tells us a kind of different way of saying: if you want it to be a duck, you have to make it quack like a duck.
00:07:34 So let's go look at this example. Whatever object we pass into this method, as long as it quacks like a duck— so as long as it responds to save and accepts the game data as an argument— then we can use it.
00:07:51 And we'll know that we're following this principle if we're not using 'is a', or 'kind of' or 'respond to' to check the type. We're just trusting the type that we're given.
00:08:06 The last principle we have is the Single Responsibility Principle. This is the one that says that a class should have one and only one reason to change. In terms of everyday Ruby development, it means that our classes should be small.
00:08:29 Our classes should do one thing, and instead of having a few big classes, we should have lots of small classes. So SOLID for everyday Ruby development just means this: we should be hiding concretions behind abstractions, which makes entities extendable.
00:08:45 Use duck typing— this is Ruby, after all— and classes should do one thing.
00:09:00 Okay, so now that we have some guidelines, let's look at some code that does not follow them.
00:09:06 We're going to listen to the tests and see what they're telling us about the design.
00:09:19 Okay, so a little context: we've been given this app.
00:09:22 Um, it's a Twitter clone, but don't tell that to the owner because it's better than Twitter— because you're not limited to 140 characters.
00:09:26 In this app, users can send each other messages. Don't call them tweets; our lawyers get very upset. They're messages.
00:09:34 So here's an example of the message model. Just like with Twitter, we can use the at sign in front of a username to mention somebody. Then we can call mentioned users on a message that gives us an array of the users that we mentioned.
00:09:47 So, let's look at some code that I found in one of the controllers. Um, I thought— I just want to give you a little bit of context, and so here's some code.
00:10:03 We have this Notifier class. It looks like maybe it's about notifying people who were mentioned in a message. I know that this app was written test first, so let's go look at the test, and we'll find out what it does and see how it's designed.
00:10:16 Here are the tests. I removed the test codes; this is just the description. I will re-read them for you if it's too hard to read back there.
00:10:26 Okay, so the Notifier class, when we call notify on it, emails the messages mentioned users who prefer to be emailed. It SMSs those who prefer to be SMSd, and at Facebooks— I guess that's what the 'fb' is— Facebooks the messages mentioned users who prefer to be Facebooked.
00:10:38 Um, so it looks like this thing sends out emails, SMSs, and something on Facebook— maybe that's a Facebook wall post. Um, also, it looks like this business about who prefer to be emailed, and who prefer to be SMSed— it looks like that users can opt-out of getting emails or SMSs, and this class will honor those preferences.
00:11:02 So before we start examining what's wrong with this, let's talk about what's good here. First, this class is tested; that is wonderful.
00:11:19 Secondly, the test descriptions are accurate and complete, as far as we can tell; there have been some details that have been embedded in the test descriptions, and that's a good thing. I think that all tests should have that property: that all the details are embedded in the descriptions.
00:11:34 Third, all the logic is hidden behind this abstraction, this Notifier class, so the controller doesn't have to know anything about emailing or SMSing.
00:11:47 It just says notifiers.notify, and that's a nice abstraction for us.
00:12:02 Good! So let's start listening to what the tests are telling us about the design. We're going to take two approaches: the first approach is going to be a little more methodical or effortful, as Brian would say.
00:12:15 And then we're going to try to build a little bit of intuition and see what our intuition should be telling us about these tests.
00:12:21 So the— I want to talk about— I'm going to use the word 'details' a lot, and when I say details in reference to the tests and to the code, this is what I mean.
00:12:36 Um, what it does is two things: what it does— that's the things that this class is expected to do, so that's responsibilities— and remember our classes are supposed to have one responsibility. And then what it knows— so that's things that this class has to know in order to do its job— these are concretions.
00:12:49 So remember, we're supposed to be hiding concretions behind abstractions. Okay, so let's start with what this class has to do.
00:13:03 We're going to be methodical about it; we are just listing out everything. So what are its responsibilities? It has to email users, SMS users, and Facebook users. And now, what does it have to know?
00:13:17 What are the concretions? The message. So it has to know which message it's notifying about. It has to know how to— it has to know who to notify.
00:13:32 And it has to know who to determine needs to be notified by each method. So who wants to be emailed, who wants to be SMSd, and who wants to be Facebooked, okay?
00:13:47 So if we do that with our— we don't always have the time. If we practice TDD and we're going through this cycle, we don't always have the time or the energy to list out everything that's going on here.
00:14:03 So let's talk about some more intuitive sense that we can build to listen to our tests.
00:14:18 So first let's look— I mean just listen to what the tests say. In this case, it's a dead giveaway that it's describing more than one responsibility. Right? It emails, it SMSs, it Facebooks.
00:14:38 Um, and then another thing that we can notice is that each described responsibility has the same structure. When you have multiple responsibilities that have the same structure, it's usually a sign that there's an abstraction that we've failed to identify.
00:14:52 Next, let's look at the language. Look especially at the phrase 'who prefer to be emailed'— it's sort of awkward phrasing, and it comes from the need, but I think the correct desire to encode this detail in the description.
00:15:03 If this detail was not encoded here, it would seem to indicate that every mentioned user should be emailed, when that's not the case; only the ones who have opted in.
00:15:19 So, like I said before, that's good that the details are encoded here, and that it's accurate and complete.
00:15:30 One of the reasons that's good is because it makes it clear when our phrasing starts to get harder and harder to get it right. When we have to agonize over the words, it might be telling us that we're embedding too many details here.
00:15:43 And that's why it's hard to embed them in the test description. So, when we have strained, long test descriptions, it's a sign that there are too many details and not enough abstractions.
00:16:01 So where are we at so far? We've looked at the test descriptions and we hear them telling us that this Notifier class probably has too many responsibilities and it's not hiding its concretions behind abstractions.
00:16:15 We haven't even looked at the test code yet, so that's already pretty good indicators of what's going on.
00:16:29 Next, let's look at some of the test code and listen to what that might be telling us.
00:16:40 When I say look, I mean look— not even read this. Don't bother! This is the third test case; this is the one about Facebook.
00:16:55 Um, and what is the first feeling that you notice when you look at this? Yeah, you see— it's big, it's long, and the first feeling that I get is that it's scary.
00:17:07 So if my boss says I need to change something about how Facebook messaging works or there's a bug in this code somewhere, I'm not excited to fix it.
00:17:23 And I'm not excited to— like if we have another case left, it's like copy this test and change it.
00:17:35 So, um, I can tell just by looking at it that it's going to be difficult to follow. And it's not because it's like spaghetti; it's actually linear.
00:17:44 There are three sections— like a given, when, then— that takes that format, but it would be hard to get your head around because there are so many details here.
00:18:06 And we can tell that just by looking at it. That's part of the reason why your tests— and my tests— are hard.
00:18:32 So this is what all the tests look like for this class. This is the three different test cases, and this is the actual code that makes the test pass. So we could— maybe clean this up, we could break things into smaller methods.
00:18:51 But that would really just be putting lipstick on a pig because we would still be hiding the fact that there are still three responsibilities at least in this class, and there are too many concretions in this class— not enough abstraction.
00:19:06 So, your boss or client calls you at 3 AM and says there's a bug in how we're SMSing people, and you know people are complaining because they have the 25 cents per text message plan, so that's really expensive for them.
00:19:22 So we have to fix it now. So what's your reaction going to be? Yeah, um, so I've definitely felt this.
00:19:34 So, we can do better. Now that we know the properties of good object-oriented design or have some sense of them at least, let's fix this.
00:19:49 First, I think that we need some guidelines for how to write our tests— how to do TDD in a way that gives us pain-free tests.
00:20:01 So these are the guidelines that I think we should follow. We should defer our responsibilities to other classes. So, when we're encountering that we have multiple responsibilities, we should be pushing them off into dependencies or into collaborators or pushing them somewhere else.
00:20:22 I mean, pushing off the ones that are more than one— we should only take one responsibility per class.
00:20:39 Secondly, we need to assume good abstractions. Remember that when we're writing our tests first, we're defining the API of the class that we're going to write.
00:20:59 So we have to assume that our class is going to give us a good abstraction, and if we do, we'll have to write it in a way that makes a test pass, and we'll be left with a good abstraction.
00:21:16 So with these guidelines, we can make testing easy on ourselves.
00:21:34 Okay, so let's kind of go through this a little bit. First, let's deal with the fact that we have so many details in here.
00:21:45 More so, the fact that we have this repeated structure and there's an abstraction that's in there that we need to get out. So let's remove the details.
00:22:03 Okay, so now since all three of these are the same, let's combine them into one, and now we're going to put the details back in. And this is going to force us to use an abstraction.
00:22:27 I just put all the details back in and it reads horribly— it notifies the SMS slash Facebook email.
00:22:41 It reads horribly, but we've identified which places are where our abstraction is. We need some abstraction to handle SMS, Facebook, email, um, and combine them into a single abstraction.
00:23:00 So what sort of thing would cover that? Um, I'm thinking maybe like— we'll call it a notification channel— like SMS, Facebook, and email; they're all channels for us.
00:23:16 So let's put that abstraction into our test case. Okay, so that's good. We still have this 'via their preferred notification channels' language.
00:23:32 So if we leave this as is, this test is telling us that our class is responsible for determining which channels which users should be notified on.
00:23:49 Right, so that's another responsibility or maybe it's a concretion— it's kind of fuzzy. But let's defer that and force another class to pick it up.
00:24:01 So we'll defer it by just saying it messages them via notification channels.
00:24:14 Um, so where's that? You know, we can't just tell our clients to stop giving us requirements, right?
00:24:28 I mean, sometimes we do, but really the problem is that we haven't abstracted things well. So where is the responsibility of knowing who to notify via which channel?
00:24:45 It's going to live. So we have a Notifier class and we have this Notification Channel abstraction.
00:24:58 So we're testing the Notifier class, so it's not there, so it's going to have to be in the Notification Channel.
00:25:07 And that sounds fine, I guess— we'll see if that works.
00:25:18 Um, so let's clean up the language a little bit. And that's a lot better; it notifies mentioned users via notification channels.
00:25:31 Okay, we can all take a deep breath— yeah, a deep breath.
00:25:46 Uh, so we're taking a deep breath, a sigh of relief because we've shrunk our test description considerably.
00:25:56 Um, and this is going to be much easier to test; we have a good abstraction, we think far fewer details, and one responsibility. So let's write this test.
00:26:14 Ah! Okay! Isn't that better? It's short. Um, you don't necessarily have to read it— I'll just kind of explain really quickly what's going on here.
00:26:29 Given— we're going to do a given one, then given a mock message and two mock channels, and a notifier that's initialized with those channels.
00:26:40 When we tell the notifier to notify about a message, then we assert that each channel was given a message and told to notify about it.
00:26:58 This is the code that makes this test pass. So what's going on here? This Notifier class can be initialized with a list of notification channels, right?
00:27:12 So that's dependency injection. When we call dot notify on a Notifier instance, it's going to go through the list of channels and tell each one of those to notify and give it the message.
00:27:24 This time, the test— this class is way easier to test and to implement. So maybe you're thinking— and I sort of hope you are— that I'm kind of cheating, right?
00:27:40 So, of course, this was easier to test; it hardly does anything. Um, you know, the other tests were testing a lot of stuff.
00:27:54 And I say in response to that that you're absolutely right and that's exactly the point. When we have small classes that depend on abstractions, they're super easy to test.
00:28:08 And we want all our classes to be small and easy to test.
00:28:23 Interestingly enough, this kind of just came out of it when I was putting this together— the details about which notification channels to use becomes a configuration option.
00:28:36 And so right here, we have in production, we can have some channels that we're using, and in staging, we can use different ones.
00:28:50 That's now become configuration, and we got that for free because we are depending upon abstractions.
00:29:03 So depending upon the idea of a notification channel rather than particular ones, and we are injecting the dependencies and not hard coding them.
00:29:17 So we made the class open to extension.
00:29:31 So now this is what the test for the email channel looks like. Let's go through really quickly.
00:29:41 Given that we have a message with three mentioned users, two of which want to be emailed— and I'm sorry, I think I skipped a piece here.
00:29:54 There we go. Um, yeah, so we have a message with users, and then we stub out our message mailer— that's like our ActionMailer thing in Rails.
00:30:07 We're going to stub that out so we can later test that it was called correctly when we make a new email channel and tell it to notify.
00:30:21 Um, then we will assert that the message mailer did, in fact, get called with the message and only the users that it's supposed to— that the email channel is supposed to notify.
00:30:38 So, yeah, that's a lot cleaner than the first example. You'll notice something that we're depending upon more concretions here.
00:30:49 There's more details. So we're building real— in this case, I decided to build real user objects with FactoryGirl instead of using mocks.
00:31:02 And we stubbed out the message mailer instead of injecting a mailer, like a generic mailer into our email channel, which we could have done.
00:31:19 But why are we using concretions here? Well, it's because we're listening to our tests. It was easy to test; there wasn't a lot of pain in putting this test together.
00:31:29 It was easy to use real objects— it's not hitting the database, so it's fast. Using mocks would not simplify this test.
00:31:44 And another layer of abstraction wouldn't help anything. So that's the signal that we use to determine when we can lay down our concretions.
00:31:57 These details— these concretions— they have to end up somewhere. Right? I mean, we can't always be— something has to talk to the file system or the network somewhere.
00:32:12 And so if we take this approach of deferring responsibilities and assuming good abstractions, we end up with small classes that contain some concretions but only the ones that are relevant to the class.
00:32:27 And another word for that is cohesion.
00:32:35 Okay, so let's, in that vein, take a look at where the details are in both of these cases.
00:32:39 In our first attempt, or the first version of the code that we got, most of the details were in the Notifier class, so I just listed them out here.
00:32:53 The only thing to see is the number of them. In our second attempt, like once we went and redid it, the details get spread out among a bunch of small classes that have one responsibility and very few details.
00:33:08 And each of these small classes is really easy to test.
00:33:23 So before I finish up, I just want to say a few things. Um, the code that I'm criticizing here is code that I wrote.
00:33:37 And it reflects the style that I've written tests for a long time, and sometimes I still do. If your tests look like those, the ones that I was criticizing, that's awesome because you have tests, and you're way ahead.
00:33:54 If you don't have tests and you start writing them and they look like that, that's awesome, because now you have tests.
00:34:07 Right? Testing is hard, so I think that any amount of testing is laudable.
00:34:20 So to close, use TDD. Write your tests first, but that's not enough.
00:34:32 In your tests, assume good abstractions and defer responsibilities into other classes. If you do that, you'll end up with small classes that are easy to test.
00:34:48 Abstractions that are easy to understand and manage, and an application that's easy to change.
00:34:58 And that is SOLID code. Thanks.
00:35:17 You.
Explore all talks recorded at Rocky Mountain Ruby 2013
+13