Requirements always change. We need to write code that is able to change in the future. The process of doing so is usually called design. Design principles are often-cited rules that lead to easily-changed code. Design principles have been derived from the initial goal: to have easily-changed code. They are not arbitrary rules. SOLID principles are an often-used set of design principles.
A Responsibility is a Reason to Change.
Single Responsibility Principle: https://en.wikipedia.org/wiki/Single_responsibility_principle Separation of Concerns: https://en.wikipedia.org/wiki/Separation_of_concerns
Having a single responsibility forces abstraction, which in turn:
- Promotes code reuse.
- Reduces the number of usages of the dependency by 'hiding' it in the dependent class; when the dependency changes, you do not need to change the usages.
- Reduces the size of files/classes/methods, to improve readability.
- Improves self-documentation (because more things have names!).
Example:
class Gear
attr_reader :chainring, :cog, :rim, :tire
def initialize(chainring, cog, rim, tire)
@chainring, @cog, @rim, @tire = chainring, cog, rim, tire
end
def gear_inches
ratio * (rim + (tire * 2))
end
def ratio
chainring / cog.to_f
end
end
Refactored:
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(chainring, cog, wheel=nil)
@chainring, @cog, @wheel = chainring, cog, wheel
end
def ratio
chainring / cog.to_f
end
def gear_inches
ratio * wheel.diameter
end
end
class Wheel
attr_reader :rim, :tire
def initialize(rim, tire)
@rim, @tire = rim, tire
end
def diameter
rim + (tire * 2)
end
def circumference
diameter * Math.pi
end
end
How do we end up violating this? https://practicingruby.com/articles/solid-design-principles
- It's easier to add a method to an existing class than it is to create a new class.
- Watch out for bloaty classes!
- Modules aren't an excuse!
Another example: Underscore
- Perhaps the Array prototype is by default at the wrong level?
Refactor this:
class Reporter
def send_report
users = User.where("last_logged_in_at >= ?", 1.week.ago)
users.each do |user|
message = "Id: #{user.id}\n"
message += "Username: #{user.username}\n"
message += "Last Login: #{user.last_logged_in_at.strftime("%D")}\n"
message += "\n"
end
Mail.deliver do
from "jjbohn@gmail.com"
to "jill@example.com"
subject "Your report"
body message
end
end
end
Trade-off: More code for more flexibility.
What Is A Dependency? Practical OOR, page 36.
Reducing dependencies is useful because it reduces the number of reasons to change (Responsibilities).
Dependency Inversion Principle: https://en.wikipedia.org/wiki/Dependency_inversion_principle
Why do we do this?
- Useful for automated testing - swap in your mocks
- Decouples a class from its dependencies (by adding an abstraction between) - avoids class name, avoids arguments and order for an initializer. These could all change, which would lead to a change in the dependent class, but aren't necessary coupling.
Example 1: https://practicingruby.com/articles/solid-design-principles
Example 2: Wheel example from above.
Exercise: Refactor the report/mail example to satisfy the DIP?
DIP related to the Law of Demeter: https://en.wikipedia.org/wiki/Law_of_Demeter
Give a quick example of Law of Demeter and Primitive Obsession.
http://confreaks.tv/videos/goruco2009-solid-object-oriented-design http://confreaks.tv/videos/rubyconf2009-solid-ruby