A podcast about development and design.
We do our best to be code-agnostic but we talk a lot about Rails, JavaScript, React, React Native, design, business and startups.
php/*
A podcast about development and design.
We do our best to be code-agnostic but we talk a lot about Rails, JavaScript, React, React Native, design, business and startups.
Copyright: © ¯_(ツ)_/¯
Metz, Sandi. Practical Object-Oriented Design in Ruby
Chapter 7. Sharing Role Behavior with Modules
Welcome to iteration
A weekly podcast about programming, development, and design through the lens of amazing books, chapter-by-chapter.
Last episode we talked about inheritance which was kind of an extension of duck typing… but sometimes we need to combine the qualities of two existing subclasses, something that inheritance cannot readily accommodate.
Many object-oriented languages provide a way to define a named group of methods that are independent of class and can be mixed in to any object. In Ruby, these mix-ins are called modules.
Discovering the Schedulable Duck Type
The Schedule expects its target to behave like something that understands lead_days, that is, like something that is “schedulable.” You have discovered a duck type.
This specific example illustrates the general idea that objects should manage themselves; they should contain their own behavior.
Mountain Bike? Mechanic?
Now, in the code above, the dependency on Schedule has been removed from Bicycle and moved into the Schedulable module, isolating it even further.
Like Using Inheritance
COMES BACK TO AUTOMATIC MESSAGE DELEGATION
Loggable Example
When Bicycle includes Schedulable, all of the methods defined in the module become part of Bicycle’s response set.
When a single class includes several different modules, the modules are placed in the method lookup path in reverse order of module inclusion. Thus, the methods of the last included module are encountered first in the lookup path.
When a single class includes several different modules, the modules are placed in the method lookup path in reverse order of module inclusion. Thus, the methods of the last included module are encountered first in the lookup path.
Picks:
John: Type to Siri - Accessibility > Siri > Type to Siri
JP: Scrimba - https://scrimba.com/playlist/pKwrCg
Acquiring Behavior Through Inheritance
A weekly podcast about programming, development, and design through the lens of amazing books, chapter-by-chapter.
Sani Metz - Object-Oriented Design in Ruby
“Inheritance is, at its core, a mechanism for automatic message delegation. It defines a forwarding path for not-understood messages.”
Where to Use Inheritance
Objects that share a common parent -
The objects that you are modeling must truly have a generalization-specialization relationship.
Bicycle Touring Company - FastFeet
Bikes - Mountain and Road Bikes
Bikes have an overall size, a handlebar tape color, a tire size, and a chain type.
Create a new empty Bicycle Class
Let RoadBike > Bicycle
And MountainBike > Bicycle
When in doubt put less code in the parent, it’s easier to promote code later when you need a shared code.
“The general rule for refactoring into a new inheritance hierarchy is to arrange code so that you can promote abstractions rather than demote concretions.”
A superclass may have many subclasses, but each subclass is permitted only one superclass.
This family tree image is, however, a bit misleading. In many parts of the biological world, it’s common for descendants to have two ancestors.
It’s really useful to rails a NotImplementedError in the parent class in the methods that are required from their children, for example, default tire_size -
“Creating code that fails with reasonable error messages takes minor effort in the present but provides value forever.”
Initialize > Post Initialize to append any or overrode attributes from the parent initialize method.
Initialize + post_initialize
Closes with a mic drop - Initializing a new RecumbentBike is so DRY and painless!
“Inheritance solves the problem of related types that share a great deal of common behavior but differ across some dimension.”
“The best way to create an abstract superclass is by pushing code up from concrete subclasses.”
“When your problem is one of needing numerous specializations of a stable, common abstraction, inheritance can be an extremely low-cost solution.”
PICKS
Practical Object-Oriented Design in Ruby
A weekly podcast about development and design through the lens of amazing books, chapter-by-chapter.
Duck types are public interfaces that are not tied to any specific class.
Objects that are defined by their behavior than by their class name.
if an object quacks like a duck and walks like a duck, then its class is immaterial, it’s a duck.
In a way, the “+” operator is a “duck type” - because it responds differently to a different type of objects.
Preparer
TripCoordinator, Mechanic, Driver can all be a preparer - they all respond to the prepare method in their own ways.
At a higher level, what are these all doing? They are preparing something - we can trust within this shared interface.
Within mechanic - we only call prepare_trip vs gas_up or full_water_tank-
Instead of switching on type
You
Talk through this Object:
Notification Object?
https://github.com/withbetterco/wellstart/blob/master/app/models/notification.rb#L156
It has this insane method checking to see if the user’s notification settings match the notification that’s trying to send
We are wanting to extend the notification object, for another type - Push Notifications
Object (message, comment, like)
User
NotificationSettings
Notifier
Notification Policy
“The ability to tolerate ambiguity about the class of an object is the hallmark of a confident designer.”
Recognizing a duck:
“You can replace the following with ducks: • Case statements that switch on class • kind_of? and is_a? • responds_to?”
Polymorphic Objects
Went from a ParticipantNote, and ProgramNote, and ApplicantNote, to one single Note object through a Notable Module. They all have “ has _many: notes, as:: notable” instead of each having “program_notes”, applicant_notes, etc.
“you are missing an object, one whose public interface you have not yet discovered.”
Dot files - .zshrc - Custom prompt - with my git status -
Slippers from JP - Quadie
At an object-oriented level, applications are made up of classes but defined by messages
Imagine a restaurant kitchen. Customers order food off a menu. The menu is a kind of public interface. Within the kitchen, messages get passed and the way a meal is prepared (implementation details) are private to the customer.
Public Interfaces
Private Interfaces
The public interface is a contract that articulates the responsibilities of your class
Use case
The distinction between a message that asks for what a sender wants and a message that tells the receiver how to behave may seem subtle but the consequences are significant
trip and mechanicmechanic HOW to prepare the bike.mechanic WHAT to do and leave the implementation details up to the mechanic.The best possible situation is for an object to be completely independent of its context. An object that could collaborate with others without knowing who they are or what they do could be reused in unanticipated and novel ways
Recall the use case: A customer, in order to choose a trip, would like to see a list of available trips of appropriate difficulty, on a specific date, where rental bicycles are available
Customer, Trip, and BicycleTripFinder class that is responsible for finding suitable trips
Methods in the public interface should:
public, protected, and privateJP’s: HackingWithSwift
John’s Pick: MacStadium.com -
To collaborate, an object must know something about others. Knowing creates a dependency. If not managed carefully, these dependencies will strangle your application
An object has a dependency when it knows:
Gear expects a class named Wheel to existself.
Gear expects a Wheel instance to respond to diameterGear knows that Wheel.new requires a rim and a tireGear knows the first argument to Wheel.new should be rim and the second should be tiresee 1_inject_dependencies.rb
Wheel class changes, the gear_inches method must also changegear_inches is explicitly saying that it is only willing to calculate gear inches for instances of WheelGear will only collaborate with any other kind of object even if that object has a diameter and uses gears!It's is not the class of the object that's important, it's the message you plan to send to it.
Gear needs access to an object that can respond to diameter - a duck typeWheel instance outside of the classsee 2_isolate_dependencies.rb
Isolate Instance Creation
Wheel.new from gear_inches and into Gear's initialize methodWheel into its own wheel methodIsolate Vulnerable External Messages
gear_inches depends on Gear responding to wheel and wheel responding to diameterdiameter method to hold wheel.diameter, we remove the dependency within gear_inchessee 3_remove_arg_orer_dependencies.rb
Use Hashes for Initialization Arguments
initialize method must be passed in the correct order. we can pass an object instead to remove this dependencyExplicitly Define Defaults
fetch method to set defaults when using hashes in our initialize methodfetch expects the key you're fetching to be in the hash and supplies several options for handling missing keysfetch will only set the default if the key is not found in the hashGear depending on Wheel or diameter - but the code could have easily been written so that Wheel depends on Gear or ratioDepend on things that change less often
John’s pick - Pick Krisp -https://krisp.ai
The foundation of an object-oriented system is the message, but the most visible organizational structure is the class
Questions to ask yourself:
A class should do the smallest possible useful thing; that is, it should have a single responsibility
Small Gears
Large Gears
harder to pedal, fast
sends you flying down those steep hills
one pedal rotation with your foot might cause the tires to rotate multiple times
Let's start with a small script and then extrapolate classes out of it:
chainring = 52
cog = 11
ratio = chainring / cog.to_f
puts 'Large Gear:'\
"\n#{chainring}-tooth chainring"\
"\n#{cog}-tooth cog"\
"\n#{ratio.round(2)} rotations"
chainring = 30
cog = 27
ratio = chainring / cog.to_f
puts "\nSmall Gear:"\
"\n#{chainring}-tooth chainring"\
"\n#{cog}-tooth cog"\
"\n#{ratio.round(2)} rotations"
Since we're talking about gears, it only makes sense that we start by creating a Gear class based on the behavior above
see 1_gear.rb
Our Gear class has three methods: chainring, cog, and ratio
Gear is a subclass of Object and thus inherits many other methods besides the three that we defined
What I'm trying to say is that the complete set of behavior / the total set of messages to which it can respond is fairly large
This is great and all - but what if we want to extend the functionality by taking into account the effect of the difference in wheels
Consider this formula
gear inches = wheel diameter × gear ratio
(where)
wheel diameter = rim diameter + (2 × tire diameter)
see 2_gear.rb
Gear.new(52, 11) no longer works because we added 2 more arguments to our initialize methodGear class - perhaps it is doing too muchgear_inches, which is fine - but calculating the tire size seems a little weirdGear class, there's something off about having rim and tire in there.Gear is transparent and reasonable - this doesn't mean that we have great design. All it means is that we have no dependenciesGear lies about its responsibilities as it has multiple responsibilities in that it has to do "wheel" calculations in our gear_inches messageHere are some techniques that help you write code that embraces change
Hide Instance Variables
Always wrap instance variables in accessor methods instead of directly referring to variables, like the ratio method does.
We can do this by using an attr_reader
def ratio
@chainring / @cog.to_f
end
def ratio
chainring / cog.to_f
end
If your instance variable is referred to multiple times and it suddenly needs to change, you're in for a world of hurt.
Your method that wraps your instance variable becomes the single source of truth
One drawback is that because you can wrap any instance variables in methods, its possible to obfuscate the distinction between data and objects
But the point is that you should be hiding data from yourself.
Hiding data from yourself protects code from unexpected changes
Hide Data Structures
see 3_obscuring_references.rb
Struct class to wrap a structuresee 4_revealing_references.rb
diameters method now has no knowledge of the internal structure of the arraydiameters just know that it has to respond to rim and tire and nothing about the data structurewheelify methodExtra Extra Responsibilities from Methods
def diameters
wheels.collect { |wheel| wheel.rim + (wheel.tire * 2) }
end
this method clearly has two responsibilities
we can separate these into two methods that each have their own responsibility
def diameters
wheels.collect { |wheel| diameter(wheel) }
end
def diameter(wheel)
wheel.rim + (wheel.tire * 2)
end
separating iteration from the action that's being performed on each element is a common case of multiple responsibilities
Wheel class from our Gear classsee 5_gear_and_wheel.rb
A weekly podcast about development and design through the lens of amazing books, chapter-by-chapter.
Pivot a bit: Less agnostic, more useful to just lean into the technologies we use every day for a while.
We’ll do our best to keep the information generally useful to all software developers.
Moving forward this is a Web Development Podcast covering Ruby on Rails, Javascript and React.
New book:
Practical Object-Oriented Design in Ruby by Sandi Metz
Also known as POODR
About the Author:
Sandi Metz: She is one of the foremost thought leaders in clean code.
If you want some amazing programming talks just look up Sandi Metz on YouTube.
About the book:
“The Complete Guide to Writing Maintainable, Manageable, Pleasing, and Powerful Object-Oriented Applications”
About Object-Oriented Design:
Object-oriented applications are made up of objects and the messages that pass between them. Messages will turn out to be the more important of the two, but in this brief introduction (and in the first few chapters of the book) the concepts will get equal weight.
“In a world of objects, new arrangements of behavior emerge naturally. You don’t have to explicitly write code for the spouse_steps_on_cat procedure, all you need is a spouse object that takes steps and a cat object that does not like being stepped on. Put these two objects into a room together and unanticipated combinations of behavior will appear.”
“Unfortunately, something will change. It always does. The customers didn’t know what they wanted, they didn’t say what they meant…. Even applications that are perfect in every way… change is ubiquitous and omnipresent”
WHY IS CHANGE SO HARD!?
Dependencies stand in the way of change
Object Oriented Design is about Managing Dependencies
“In the absence of design, unmanaged dependencies wreak havoc because objects know too much about one another.”
“Part of the difficulty of design is that every problem has two components. You must not only write code for the feature you plan to deliver today, you must also create code that is amenable to being changed later.”
Design Principles
Design Patterns
Service Objects, Factory, Adapter, Decorator - This book doesn’t cover these.
Big Up Front Design Pitfalls
Design takes time
Next Chapter - we really get our hands dirty
Sandi Metz Links:
https://www.youtube.com/watch?v=npOGOmkxuio&t=1841s
https://www.youtube.com/watch?v=29MAL8pJImQ
https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1s
Pick:
JP: https://testingjavascript.com/
John: SOLID Design Patterns in Javascript
https://www.youtube.com/watch?v=GtZtQ2VFweA&feature=share
- - - - -
New Book: Practical Object-Oriented Design
Examples of when to not "fail fast":
Sometimes you just need a way to proceed through steps and have the system tell you what parts succeeded and what parts failed
Sideband data
When communication failures without resorting to exceptions, we need a sideband: a secondary channel of communication for reporting meta-information about the status and disposition of a process.
Multiple return values
hehe, this reminds me of stuff I do in javascript
ruby supports multiple return values in the form of array splatting. in JS, you could do this with destructuring
def foo
result = 42
success = true
[result, success]
end
result, success = foo
puts "#{success ? 'succeeded' : 'failed'}; result #{result}"
Or you can use an open struct
def foo
OpenStruct.new(:result => 42, :status => :success)
end
Output parameters
Caller-supplied fallback strategy
if we're not sure we want to terminate the execution of a long process by raising an exception, we can inject a failure policy into the process
def make_user_accounts(host, failure_policy=method(:raise))
# ...
rescue => error
failure_policy.call(error)
end
def provision_host(host, failure_policy)
make_user_accounts(host, failure_policy)
end
policy = lambda { |e| puts e.message }
provision_host("192.168.1.123", policy)
JP:
John:
Failure flags and benign values
Sometimes responding with a nil is good enough, i.e.
def save
# some failing code
rescue
nil
end
Related to this is the concept of "benign values"
The system might replace the erroneous value with a phony value that it knows to have a benign effect on the rest of the system
When the system's success doesn't depend on the outcome of the method in question, using a benign value may be the right choice. Benign values are also helpful in making the code more testable.
Example:
begin
response = HTTP.get_response(url)
JSON.parse(response.body)
rescue Net::HTTPError
{ 'stock_quote' => '' }
end
'ing, we can usewarn`Warning as errors
Check out this hack:
module Kernel
def warn(message)
raise message
end
end
warn 'uh oh'
Remote failure reporting
Circuit breaker pattern
Ending the program
exit ends the whole programexit in the Whiz Tutor codebase?John: User Onboard - https://www.useronboard.com/ by https://twitter.com/samuelhulick
JP: singlediv.com - https://twitter.com/samuelhulick
In this short season, we are going through EXceptional Ruby by Advi Grimm
Let's talk about some definitions first:
"Failures cause exceptions which are due to errors"
raise or fail methods🗣 Discussion: When do you find yourself using raise?
🗣 Discussion: When do you find yourself using rescue?
rescue clause should be a short sequence of simple instructions designed to bring the object back to a stable state and to either retry the operation or terminate with a failuresyntax
Starts with the rescue keyword, followed by one or more classes or modules to match, then a hash rocket and the variable name to which the matching exception will be assigned
Subsequent lines of code up to an end keyword will be executed if the rescue is matched
rescue IOError => e
puts "Error while writing to file: #{e.message}"
end
Moar Code
begin
raise RuntimeError, 'specific error'
rescue StandardError => error
puts "Generic error handler: #{error.inspect}"
rescue RuntimeError => error
puts "runtime error handler: #{error.inspect}"
end
🗣 Discussion: Have you ever used retry?
tries = 0
begin
tries += 1
puts "trying #{tries}"
raise
rescue
retry if tries < 3
puts 'I give up'
end
Picks:
JP: Overcast https://overcast.fm/
John: Apple Refurbished