Practical Object-Oriented Design: An Agile Primer Using Ruby - podcast episode cover

Practical Object-Oriented Design: An Agile Primer Using Ruby

Sep 12, 202522 min
--:--
--:--
Download Metacast podcast app
Listen to this episode in Metacast mobile app
Don't just listen to podcasts. Learn from them with transcripts, summaries, and chapters for every episode. Skim, search, and bookmark insights. Learn more

Episode description

Explores practical object-oriented design (OOD), particularly within the context of Ruby programming, offering guidance for creating changeable and maintainable software. It covers fundamental principles such as single responsibility, managing dependencies, and designing flexible interfaces, emphasizing the importance of messages over classes and the strategic use of public and private interfaces. The material also explains advanced concepts like duck typing, classical inheritance, and object composition, contrasting their benefits and costs to help developers choose appropriate architectural relationships. Furthermore, it provides essential advice on designing cost-effective tests that support refactoring and reduce overall development costs, highlighting that well-designed applications and tests inherently lower maintenance burdens.

You can listen and download our episodes for free on more than 10 different platforms:
https://linktr.ee/cyber_security_summary

Get the Book now from Amazon:
https://www.amazon.com/Practical-Object-Oriented-Design-Agile-Primer/dp/0134456475?&linkCode=ll1&tag=cvthunderx-20&linkId=24f33aad7f008deb8a0a75f9d4114df4&language=en_US&ref_=as_li_ss_tl

Discover our free courses in tech and cybersecurity, Start learning today:
https://linktr.ee/cybercode_academy

Transcript

Speaker 1

Welcome to the Deep Dive, the show where we sift through the noise, snack up the sources, and extract the most potent nuggets of wisdom just for you. You know, if you've ever tried to create something, especially software, you get that drive, right, that feeling of building something meaningful. But then sometimes that joy kind of fades, doesn't it, When the cost, the sheer effort of changing or maintaining it just seems to outweigh the value.

Speaker 2

Oh, absolutely is a path. Pretty much everyone in software walks. You're excited, you ship something, it works, Yeah, But then comes the long game. How does it live? How does it adapt? That's where the real challenge often kicks in, sometimes the.

Speaker 1

Pain too exactly, And that challenge that's precisely why today we're doing a deep dive into object oriented design ODE. We're leaning heavily on a fantastic book, Practical Object Oriented Design and Agile Primer Using Ruby by Sandy Mets. But let's be clear, this isn't really about learning to code, specifically Ruby or anything. Our mission here is more like a shortcut, a shortcut to understand how to arrange code well so your software stays joyful to work on productive

and it can actually handle future changes gracefully. It's about getting really clued into those design principles that make working on software a pleasure not a chore.

Speaker 2

And it all starts really with one fundamental truth about software dot change, it just happens. Yeah, you can imagine this like fantasy world where software is written once requirements are perfect, never changed.

Speaker 1

Oh wouldn't that be nice? No bugs, no new features needed, just done forever exactly.

Speaker 2

And in that world, honestly, design wouldn't matter that much. Your code could be a mess, but who cares, It just runs. But reality, well, as Sandimes puts it so well, change is unavoidable. It is ubiquitous, omnipresent, and inevitable. It's just the water we swim in.

Speaker 1

Okay, So if change is truly that inevitable, what's like the most counterintuitive thing a developer should maybe start doing differently right now, something that isn't immediately obvious.

Speaker 2

That's a good one. I'd say the most counterintuitive shift is probably aiming for FlexIt ability or trying to get it perfect on day one. So many devs they want the ideal solution immediately, trying to guess every future need but if change is guaranteed, your perfect first guess is almost certainly wrong or at least incomplete. So instead you should focus on making the code easy to change, even if that means putting off some decisions like unmanaged dependencies.

They are the real killers here, often silent ones. They're like these invisible strings tying bits of code together. If you don't manage them carefully, even a tiny tweak, well, the damage can just radiate outward and overlapping concentric circles. Suddenly nothing feels safe to touch. You start with something small and successful, and before you know it, it's a tarpit. Every step feels dangerous.

Speaker 1

Right the tar pit. And that's where the books says something really interesting. Design is thus an art, the art of arranging code. It's not just about making it work now. It's about setting it up so it's understandable and changeable.

Speaker 2

Later, precisely. And we have tools for this art right design principles, design patterns, These are our guides. Academics have even tried to measure this stuff. You quantify goodness. There are metrics from to Damburn camera. Back in the day, they looked at things like how many connections does the class have how complex are its methods? And they did find correlations between using these kinds of design techniques and

ending up with higher quality code. But it's important to remember bad ode metrics are indisputably a sign of bad design. If your metrics look bad, your code's probably hard to change.

Speaker 1

But getting good scores doesn't automatically mean your design is great.

Speaker 2

Does it exactly right? Good scores don't prove the opposite. You could definitely over anticipate the future. You might build this elegant structure that anticipates all the wrong changes. Then when the real future shows up, it's actually expensive to adapt, which brings us to technical debt. It's like borrowing time from the future. You take a short cut now, but you know you'll have to pay it back later, probably with interest. Sometimes it's a bunch's choice, but you have to plan to repay.

Speaker 1

That's a really useful way to think about it. So, yeah, okay, design can fail. What does it look like when there's just no design you just code? It works for a bit.

Speaker 2

Yeah, that leads to code that's easy to write but gradually impossible to change. Your programmers start saying things like yeah, I can add that, but honestly, it's going to break everything. That's a bad sign.

Speaker 1

Okay, so that's one extreme. What about the other overdesign?

Speaker 2

Ah? Yes, the complicated, beautiful castles of code. They look amazing, architecturally.

Speaker 1

Sound, but totally inflexible.

Speaker 2

Exactly, they're hemmed in by stone walls. Now, the response to a new feature is no, I can't add that feature. It wasn't designed to do that.

Speaker 1

So no design is bad. Over design is bad. What about trying to do all the design first, that whole big upfront design thing, but you have to separate the designers from the coders.

Speaker 2

Yeah, that often creates this weird tension, you know, designers versus programmers and the design itself. It's almost always wrong in some way because as the books as, certainty is unattainable in advance of the application's existence. You just can't know everything upfront. And this is where agile comes in. People sometimes think agile means no design, but that's wrong. Agile does not prohibit design. It requires it. In fact,

it requires really good design. Design that's simple, flexible, ready for change because change is the constant.

Speaker 1

Okay, so if good design is about arranging code to handle change, let's dig into object oriented programming itself. How does OP help with this flexibility compared to older ways.

Speaker 2

Well, think about older procedural languages. You generally had your data structures over here and your functions that operate on that data over there, separate things. OO languages like Ruby, which the book uses, they combine them together into a single thing, an object.

Speaker 1

Hah.

Speaker 2

So now you're not thinking about data and action separately. You're thinking about things objects that no stuff and do stuff all bundled together. It changes how you model the problem. And Ruby, for instance, is open ended, you can invent brand new types of your own. What happens is your application sort of evolves into its own unique programming language that is specifically tailored to your domain. You could express ideas about your problem really naturally.

Speaker 1

Building your own language. That's a cool perspective. Okay, let's get practical. What are the core OD principles that help us build these flexible systems? Starting with big one Single responsibility principle SRP.

Speaker 2

Right SRP. The core idea sounds simple. A class should do the smallest possible useful thing. It should have a single responsibility. If a class is juggling too many jobs, It gets hard to reuse, things get tangled up. Changes in one responsibility break others. Yeah, it's a mess.

Speaker 1

Single responsibility sounds easy, But how do you actually know if a class is doing too much? Is there a practical test?

Speaker 2

Yeah, there's a neat trick. The book suggests you interrogate it. You try phrasing its methods as questions directed to the class. So please, mister gear, what is your ratio? That sounds totally normal. Gear should know its ratio. But please, mister gear, what is your tire size? That just sounds wrong? Doesn't it? A gear doesn't have a tire. If the question feels awkward or nonsensical, the class is probably doing things it

shouldn't be. It has too many responsibilities, and this ties into asking yourself, what is the future cost of doing nothing today? Sometimes the best move is to not make a big design decision yet, wait until you know more. Making a class too early.

Speaker 1

Is just guessing, and this idea single responsibility. It applies to methods too, not just classes.

Speaker 2

Oh, absolutely, methods should also do one thing well. Keeping methods small and focused has huge benefits. It makes the code clearer, often eliminates the need for comments Because the method name says it all. It encourages reuse. Plus, small focus methods are much easier to move to a different class if you realize later that the responsibility belongs elsewhere. Sometimes you might even start by putting a small related responsibility inside a temporary structure like a structin Ruby, within

the main class. It's like a holding pattern before deciding if it needs its own full class.

Speaker 1

Okay, so keep things focused, single responsibility. But these focused things need to work together, which brings us to dependencies the glue. The book calls it a little dot of blue, but warns that too much can strangle your application. How do we manage that glue?

Speaker 2

Managing dependencies is maybe the central challenge, and a key technique is dependency injection. Sounds fancy, but the idea is simple. Instead of an object creating its own collaborators, like Gear making its own wheel, you pass the collaborator in from the outside, so Gear just gets given a real object when it's created. The result, Gear actually becomes smarter because it knows less. It doesn't need to know how to create a wheel, or even that it is a wheel.

All Gear cares about is that the object it receives responds to the messages it needs, like diameter. That's it immensely powerful for flexibility.

Speaker 1

That makes a lot of sense Practically speaking, how often do you find yourself using dependency injection? What's the biggest win you see from it?

Speaker 2

It becomes second nature. Really, you're constantly thinking, does this object really need to know how to create that other object? Usually the answer is no, So in jen acting dependencies, especially through the constructor, becomes standard practice. The biggest win,

hands down, it's testability and substitutability. You can easily swap at a fake test wheel when testing gear, or a completely different kind of wheel later without changing gear at all, as long as it provides that diameter method and related tip, the book mentions using keyword arguments for initialization instead of relying on the order of arguments. You name them year dot new chane ring five bay good fifty two, cog eleven wheel dot mywheel. This makes the code much clearer

and less brittle. If you add or change arguments later, no more remembering was chain ring first or second? Stepping back, the big idea here is depend on things that change less often than you do. Concrete classes, specific implementations, they tend to change. Abstract interfaces these common stable qualities they change much less often. So you want your pendencies pointing towards abstractions, not concrete details.

Speaker 1

Okay, focus responsibilities, manage dependencies, But how do these objects actually communicate effectively? How do we design those conversations well?

Speaker 2

Fundamentally, an object oriented application is defined by messages being sent between objects. The book uses a great analogy a restaurant kitchen. The menu is the public interface. Customers order off the menu. They don't need to know the complex process happening inside the kitchen. That's private. Your classes should be the same. A clear public interface, the menu of methods others can call and private internal.

Speaker 1

Workings and sequence diagrams come in handy here. The book calls them a perfect low cost way to experiment. It shifts thinking from what does this class do to who responds to this message exactly?

Speaker 2

That shift is crucial because, as Met says, you don't send messages because you have objects. You have objects because you send messages. Thinking about the messages first helps you discover the objects you actually need and what their public interfaces should.

Speaker 1

Be, which leads to another key idea, asking for what instead of telling how Take the example.

Speaker 2

Of a trip class and a mechanic object. Instead of trip telling mechanic every single step, check breaks and flight tires Lubrik chain the trips. You just ask the mechanic to do the what prepare trip and trust the mechanic to know how to do that. This is blind trust in ode. Objects collaborate by relying on each other's public interfaces without needing intimate knowledge of the internal details. It keeps them loosely coupled.

Speaker 1

And what about controlling access? Ruby has public protected private? Are these strict walls?

Speaker 2

Not really? You know, the book calls them flexible barriers in Ruby, and like some other languages, these aren't absolute guarantees. Because of that, many experienced rubyists actually prefer using comments or naming conventions like starting private method names with an underscore to signal intent, rather than relying solely on the key words. It's more about convention and trusting other developers.

Speaker 1

Okay, so flexible barriers. This sounds related to minimizing dependencies. Again. Are there common ways people mess up object communication anti patterns?

Speaker 2

Oh? Definitely. A classic one is violating the law of demeter. The simple version is only talk to your immediate neighbors, or even simpler, use only one dot. You see code like customer dot, bicycle, dot wheel, dot rotate. That's often called a train wreck.

Speaker 1

A train wreck, okay, why is that so bad?

Speaker 2

Because it deeply couples the code calling rotate to the entire internal structure of customer, bicycle and wheel. If any part of that chain changes, maybe bicycle no longer directly holds a wheel, this code breaks even though just wanted to rotate something. These chains are not transparent, reasonable, usable, or exemplary. They hide dependencies and make the code fragile. When you find yourself writing code like that, it's a strong signal. Listening to demeter tells you where your public

interfaces are lacking. You probably need a method on bicycle itself like rotate wheel to hide that internal chain.

Speaker 1

That makes sense. It forces better interface design. Okay, let's shift gears to structuring behavior. How can we use dynamic behavior like duct typing to keep costs down?

Speaker 2

Right? Duck typing. The core idea is it's not what an object is that matters, it's what it does. If an object walks like a duck and quacks like a duck, than for the purpose of the code interacting with it. You can treat it as a duck. You care about its behavior. It's public interface, not its specific class name. The book uses a funny metaphor. An object is like a partygoer at a masquerade ball that changes masks. Its

role can change depending on the context. A big warning sign the book points out is seeing a case statement that switches based on class names like caseobject dot class, when wheel, when gear. That kind of toad should grab your attention as if it were playing trumpets. It usually means there's a hidden duck type, a shared role or interface waiting to be discovered. For example, maybe different objects all know how to prepare themselves. Instead of checking their class,

you just call prepare on them. You've identified a preparer duck type. This makes your code way more flexible. You can add new kinds of preparers later without changing the code that uses them. It's easier to extend but casting a veil over the underlying class, which is often a good thing.

Speaker 1

Okay, so that's dynamic behavior with ducks. What about more structured, hierarchical behavior using classical inheritance.

Speaker 2

Inheritance is basically automatic message delegation. It's like a family tree. A subclass automatically inherits behavior from its superclass. Subclasses are the specializations of their superclasses. A mountain bike is a special kind of bicycle. Finding the right abstraction level and inheritance can be tricky. The book suggests a strategy push everything down and then pull some things up. Start by

making things specific in subclasses. Then identify common code or behavior and carefully pull it up into a shared superclass. Throughout this, you should constantly ask what will happen when I'm wrong. This encourages making conservative choices, keeping the cost of future changes low.

Speaker 1

Inheritance can get tricky, especially with super when subclasses have to remember to call their parents' implementation. That seems error prone.

Speaker 2

It definitely is, and that's where the template method pattern shines. Often using hook methods. Think of the superclass providing a template an overall algorithm, but it defines placeholder methods. The hooks like post initialized for customs, setup or local spares for bike specific parts. Subclasses can then override these hooks to plug in their specific behavior without needing to explicitly

call super in the main method. This structure removes the need for subclasses to send super in many cases and dramatically reduces coupling between the layers. It makes the hierarchy much cleaner. General advice create shallow hierarchies. Deep inheritance trees build up a lot of dependencies and become hard to manage. Keep it simple if you can.

Speaker 1

All right, So we have ducks for dynamic behavior, inheritance for hierarchical specialization. What about combining distinct things together composition.

Speaker 2

Composition is about building complex objects by combining smaller, independent parts. It's a has a relationship. A car has an engine, has a wheels. The whole becomes more than the sum of its parts. Like notes forming music, the parts work together.

Speaker 1

And the book's recumbent bike example really drives this home right Well, it's.

Speaker 2

A fantastic example. Using inheritance. Adding support for a new recumbent bike took like nineteen lines of code chain just scattered around, But by refactoring to use composition. Where a bicycle has a parts object created by a parts factory, adding the recumbent bike became just three lines of configuration. That's it. It shows the power of composition for managing variations and adding new features. The decision rule of the

book offers is pretty stark. If you cannot explicitly defend inheritance as a better solution, use composition.

Speaker 1

Inheritance carries more risk, so composition is generally preferred unless there's as strong is a relationship. What are the main benefits and drawbacks exactly?

Speaker 2

Composition usually leads to systems that are more transparent, usable, reasonable, and have a high tolerance for change. The parts are independent, easier to test, easier to swamp out. The main cost is that sometimes the overall structure the whole might feel less obvious than a clear inheritance try and you often need to write explicit delegation code to pass messages from the whole object to its parts. But the guideline holds.

Use inheritance for is a relationships, true specialistation, low risk, shallow hierarchies. Use composition for has relationships. When an object is built from distinct parts, composition is usually the safer, more flexible.

Speaker 1

Bet Okay, awesome. We've designed our flexible system using these principles. Now how do we make sure it actually works and stays working? Testing and the book argues the purpose isn't just finding bugs.

Speaker 2

Not at all. Just like design, the true purpose of testing is to reduce costs. Good tests make changes cheaper and safer. Bad tests or hard to write tests increase costs. And there's a direct link. Tests are the canary in the coal mine when the design is bad. Testing is hard. If your tests are painful to write, it's often assign your design needs improving.

Speaker 1

That's a powerful connection. If testing is about reducing costs, how does that change what we test? How do we write cost effective tests?

Speaker 2

It radically shifts the focus away from just code coverage towards testing behavior and interfaces. The guidelines are pretty clear. Test incoming messages that change state, So incoming messages should be tested for the state they return. If you call deposit one hundred, test that the balance is now one hundred higher. Test outgoing commands. Outgoing command messages should be

tested to ensure they get sent. If your object is supposed to tell another object to send mail, tests that the send to mail message was sent, not the details of how. But importantly, don't test outgoing queries. Outgoing query messages should not be tested if your object just asks another object for data like get chooser name and doesn't change anything. You don't need to test that interaction directly in the calling objects test testing the query method itself

is enough. This approach minimizes the coupling between your tests and the implementation details, making tests less brittle and cheaper to maintain when the code changes. Makes sense.

Speaker 1

What about test doubles, stubs and mocks. They can sometimes create this alternate universe where tests pass but the real app is broken. Right.

Speaker 2

Yeah, that's a real danger. Your doubles might behave slightly differently than the real objects, leading to false confidence. The way to combat this is by using tests to document roles or interfaces. You can actually write tests for your test doubles. For example, create a diameterizable interface test. Any object claiming to be usable as a diameterizable thing, like your real wheel and your diameter double used in tests,

must pass this interface test. This ensures your test doubles honor the interface this test expects and keeps them synchronized with the real object's behavior.

Speaker 1

Okay, interesting. What about testing private methods? Big debate there sometimes?

Speaker 2

Huh. Yes, The book's advice is wonderfully pragmatic and a bit cheeky. Never write them, and if you do, never ever test them, unless, of course, it makes sense to do so. Basically, testing private methods directly is usually a bad idea. It couples your test tightly to implementation details that should be free to change. If the public interface works, the privates are probably okay. However, the unless is important if you're dealing with a really complex private method, maybe

as part of a temporary refactoring. Writing a targeted test for it can sometimes reduce the barriers to refactoring by giving you confidence as you clean it up. But treth those tests as temporary got it.

Speaker 1

And finally, testing code that uses inheritance where subclasses rely on shared superclass behavior.

Speaker 2

This is where those interface tests and what the book calls subclass responsibility tests really shine. You can write tests that define the contract a subclass must fulfill to work correctly with the superclass, template method or hooks. These tests take all of the pain out of testing the common behavior of subclasses. A new developer can come on, create a new subclass, run the tests, and the tests will tell them exactly which methods they need to implement correctly.

It makes extending hierarchies much safer.

Speaker 1

Wow. What an amazing journey through arranging code We've covered responsibilities, dependencies, interfaces, ducts, inheritance, composition testing. It's a lot, but it connects beautifully, it.

Speaker 2

Really does, And that final insight from the book is so profound. Object oriented design is fractal. The central problem is to define an extensible way for objects to communicate, and at every level of magnification, this par looks the same whether you zoom out to the whole system or zoom into a single method interacting with its variables. That core challenge of clear communication and managed dependencies is always there.

Speaker 1

Fractal design. That's a fantastic way to think about it. It scales, and the book leaves us with a final piece of wisdom, almost a challenge. The rules of design are meant to be broken. Learning to break them well is a designer's greatest strength.

Speaker 2

Absolutely, It's not about blindly following rules. It's about understanding the principles behind the rules so you know when and how to bend them effectively. So yeah, persist, practice, experiment, imagine, do your best work, and all else will follow.

Speaker 1

So listening to all this, what's the one thing that really stands out to you? Maybe it's designing for that inevitable change or managing that glue of dependencies, or thinking about ducks, whatever it is, let that idea challenge how you think about building software next time See where it takes you

Transcript source: Provided by creator in RSS feed: download file
For the best experience, listen in Metacast app for iOS or Android