Skip to content

Instantly share code, notes, and snippets.

@fernandes
Created April 23, 2016 15:48
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fernandes/a23298a945bef6149bc31e3ff3ccbba1 to your computer and use it in GitHub Desktop.
Save fernandes/a23298a945bef6149bc31e3ff3ccbba1 to your computer and use it in GitHub Desktop.
My Annotations of POODR (Practical Object-Oriented Design in Ruby) Book by Sandi Metz
# Chapter 2: Designing Classes with Single Responsability
Single Responsability
* Ask Each Method: 'Class whats your :method?'
* Hide Instance Variables
* Hide Data Structures
* Extract Extra Responsability From Methods
Dependencies
* Must known no Constant
* Must _not_ call methods on other objects (constants)
* Must do _not_ depedend on methods arguments (call methods knowing the arguments)
* Most not know the order
Solutions:
* Inject dependencies
* Isolate instance creation (on a method)
* Isolate vulnerable external messages
* Remove argument-order dependencies
* Explicity define defaults
# Chapter 3: Managing Dependencies
1. some classes are more likely than other to have changes in requirements
2. concrete classes are more likely to change than abstract classes
3. changing a class that has many dependents will result in widespread consequences
1.
ruby < framework mature < framework imature < your code
2.
prefer depend on duck typing instead of is_a?(Class)
3.
avoid classes that _the whole world_ depends on
Many
d | |--------------------------|------------------------------|
e | | Abstract Zone (A) | Danger Zone (D) |
p | | changes are unlikeli | these classes WILL change |
e | | but, it they occur, will | and the changes will cascade |
n | | have broad effects | into dependents |
d | |--------------------------|------------------------------|
a | | Neutral Zone (B) | Neutral Zone (C) |
n | | changes are unlikely | changes are likely but they |
t | | and have few side | have few side effects |
s | | effects | |
| |--------------------------|------------------------------|
Few
Less ------------------------------------------------- More
Likelihood of Requirements Change
# Chapter 4: Creating Flexible Interfaces
Finding the Public Interface
* Constructing an Intention
* Asking for "What" instead of telling "How"
* Seeking context indepence
* Trusting other objects _This blind trust is a keystone of OO design. It allows objects to collaborate without binding themselves to context and is necessary in any application that expects to grow and change_
* Using messages to discover objects (TripFinder -> Operation)
Methods in the public interface should
* Be expecitly identified as such
* Be more about _what_ than _how_
* Have names that, insofar as you can antecipate, will not change
* Take a hash as an options parameter
Demeter Law
* Dont use delegation
* Focus on what sender wants, not on receiver behaves (to make customer walk by bike, instead of `customer.bicycle.wheel.rotate` that tells the whell to behave rotating, I want the customer to ride, so `customer.ride` makes more sense.)
# Chapter 5: Reducing Costs with Duck Typing
Recognizing Hidden Ducks
* case statements that switch on class
* kind_of? and is_a?
* respond_to?
module Diameterizable
def test_responds_to_diameter
assert_respond_to @object, :diameter
end
end
class WheelTest < MiniTest::Test
include Diameterizable
def setup
@wheel = @object = Wheel.new(rim: 29, tyre: 1)
end
def test_diameter
assert_equal @wheel.diameter, 31
end
end
sources: https://pooreffort.com/blog/testing-duck-types/ (check on how to test doubles!)
# Chapter 6: Acquiring Behavior Through Inheritance
Using The Template Method Pattern
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(args = {})
@size = args[:size]
@chain = args[:chain] || default_chain
@tire_size = args[:tire_size] || default_tire_size
end
def default_chain
'10-speed'
end
end
class RoadBike < Bicycle
# ...
def default_tire_size
'23'
end
end
class MountainBike < Bicycle
# ...
def default_tire_size
'2.1'
end
end
Decoupling Subclasses Using Hook Messages
* Dont force a subclass to ask `super` to participate
class Abstract
def initialize(args = {})
@size = args[:size]
@chain = args[:chain] || default_chain
@tire_size = args[:tire_size] || default_tire_size
post_initialize(args)
end
# force subclasses to define local_spares and do not override public interface
def spares
{ tire_size: tire_size, chain: chain }.merge(local_spares)
end
# dont break `spares` if subclass do not override
def local_spares
{}
end
# abstract has a default value for attribute
def default_chain
'10-speed'
end
# provice information about not implemented methods on subclasses
def default_tire_size
raise NotImplementedError.new('Subclass must implement: ')
end
# provide message hooks
def post_initialize(args)
nil
end
end
# Chapter 7: Sharing Role Behavior with Modules
# Chapter 8: Combining Objects with Composition
# Chapter 9: Designing Cost-Effective Tests
What should be tested?
* Incoming messages should be tested for the state they return
* Outgoing command messages should be tested to ensure they get sent
* Outgoing query messages should not be tested
Never inject concrete classes as dependencies, use abstract classes (and test them under injected class test, because if you change the abstract test will pass, but will get runtime errors!).
Use mocks to test outgoing messages
@observer = MiniTest::Mock.new
@observer.expect(:changed, true, [param1, param2])
# ...
@observer.verify
Testing Inherited Code
* Specify the inherited interface
* Specify subclass responsabilities
* confirming subclass behavior
* confirming subclass enforcement
* Testing unique behavior
* testing concrete subclass behavior
* testing abstract superclass behavior
class Bicycle; end
class RoadBike < Bicycle; end
# Ensure all subclass of bicycle honor the contract (Liskov Principle)
module BicycleInterfaceTest
def test_responds_to_method
assert_respond_to(@object, :method)
end
end
class BicycleTest < MiniTest::Unit::TestCase
include BicycleInterfaceTest
def setup
@bike = @object = Bicycle.new({tire_size: 0})
end
class RoadBikeTest < MiniTest::Unit::TestCase
include BicycleInterfaceTest
def setup
@bike = @object = RoadBike.new
end
end
Specifying Subclass Responsabilities
According to class and subclass interaction, subclasses are suposed to implement, some methods, so we need to create a test to ensure this interface
class BicycleSubclassTest
def test_responds_to_method_hook
assert_responds_to(@object, :method_hook)
end
end
class RoadBikeTest < MiniTest::Unit::TestCase
include BicycleSubclassTest
end
Confirming Superclass Enforcement
class Bicycle
def initialize(args = {})
@tire_size = args[:tire_size] || default_tire_size
end
# Subclasses are supposed to implement
def default_tire_size
raise NotImplementedError
end
end
class BicycleTest < MiniTest::Unit::TestCase
def setup
@bike = @object = Bicycle.new({ tire_size: 0 })
end
def test_forces_subclasses_to_implement_default_tire_size
assert_raises(NotImplementedError) { @bike.default_tire_size }
end
end
@ellcs
Copy link

ellcs commented Jun 19, 2018

In line 203 is an end missing.

Thank you for publishing! ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment