rupicon 2014 solid
DESCRIPTION
Rupicon 2014 solidTRANSCRIPT
SOLID OO Design
Tudor Pavel
1 / 46
Change is inevitable.
2 / 46
Design prepares you for it.
3 / 46
Design Stamina Hypothesis
http://martinfowler.com/bliki/DesignStaminaHypothesis.html
4 / 46
PrinciplesSingle Responsibility (SRP)
Open/Closed (OCP)
Liskov Substitution (LSP)
Interface Segregation (ISP)
Dependency Inversion (DIP)
5 / 46
Single ResponsibilityA class/method should serve a single purpose.
There should never be more than one reason for a class/method to change.
6 / 46
Open/ClosedA class/method should be open for extension but closed for modification.
7 / 46
Liskov SubstitutionSubclasses should be substitutable for their base classes.
8 / 46
Interface SegregationMany client-specific interfaces are better than one general-purpose interface.
9 / 46
Dependency InversionDepend upon abstractions. Do not depend upon concretions.
10 / 46
Design is all aboutdependencies.
11 / 46
When you know something,you depend on it.
12 / 46
If it changes, you might change.
13 / 46
Example App
14 / 46
RequirementsWe are the FBI and our buddies from the NSApost some encrypted messages for us in asecret RSS feed.
Our job is to parse the messages from the feedand store them in clear text in our DB, since wedon't understand encryption.
15 / 46
EncryptionThe NSA are slightly better than us atencryption, so they've used e Caesar Cipher toencrypt the messages.
16 / 46
Message modelclass Message < ActiveRecord::Baseend
17 / 46
Public interfaceclass RSSMessagesParser def execute messages = fetch_messages messages.each { |m| Message.create(text: decrypt(m)) } end
private
def fetch_messages # ... end
def decrypt(message) # ... endend
18 / 46
Fetching messagesrequire 'rss'
class RSSMessagesParser def execute messages = fetch_messages messages.each { |m| Message.create(text: decrypt(m)) } end
private
def fetch_messages RSS::Parser.parse(open('http://secret.nsa.gov/rss').read).items end
def decrypt(message) # ... endend
19 / 46
Entire classrequire 'rss'
class RSSMessagesParser def execute messages = fetch_messages messages.each { |m| Message.create(text: decrypt(m)) } end
private
def fetch_messages RSS::Parser.parse(open('http://secret.nsa.gov/rss').read).items end
def decrypt(message) message.split('').map(&:ord).map { |a| a - 3 }.map(&:chr).join endend
20 / 46
But what if...the "secret" URL changes?
the encryption method changes?
reading other RSS feeds is required?
21 / 46
Knowing when to refactorIs it DRY?
Does it have a single responsibility?
Does everything in it change at the samerate?
Does it depend on more stable things?
! The answer needs to be YES for all of them to move on.
22 / 46
Is it DRY?
23 / 46
Does it have a singleresponsibility?
24 / 46
What does RSSMessagesParser do?Fetches messages and decrypts them and saves them to the DB.
Words like and and or are design smells suggesting a violation of SRP. This classis doing too much!
25 / 46
Extracting responsibilitiesclass MessagesParser def initialize @cipher = NSACipher.new @rss = RSSMessages.new end
def execute messages = @rss.fetch messages.each { |m| Message.create(text: @cipher.decrypt(m)) } endend
26 / 46
SRP should be applied tomethods tooclass MessagesParser def initialize @cipher = NSACipher.new @rss = RSSMessages.new end
def execute messages = @rss.fetch messages.each { |m| parse(m) } end
private
def parse(message) Message.create(text: @cipher.decrypt(m)) endend
27 / 46
The NSACipherclass NSACipher def decrypt(message) message.split('').map(&:ord).map { |a| a - 3 }.map(&:chr).join endend
28 / 46
The RSSMessages classrequire 'rss'
class RSSMessages def fetch RSS::Parser.parse(open('http://secret.nsa.gov/rss').read).items endend
29 / 46
Is it DRY?
30 / 46
Does it have a singleresponsibility?
31 / 46
Does everything in it changeat the same rate?
32 / 46
The RSS URL and the Cipher stepcan change more often than theclasses, because the NSA want tostay at least ahead of the novicehackers by changing themperiodically.
33 / 46
Using Open/Close Principleclass NSACipher def initialize(step=3) @step = step end
def decrypt(message) message.split('').map(&:ord).map { |a| a - @step }.map(&:chr).join endend
Add default step to simplify current callsThe class is now open for extension and reusability.NSACipher is essentially a generic Caesar cipher that can be used to decryptmessages, it has nothing to do with NSA, so we can rename it.
34 / 46
Reusable CaesarCipherclass CaesarCipher def initialize(step=3) @step = step end
def decrypt(message) message.split('').map(&:ord).map { |a| a - @step }.map(&:chr).join endend
35 / 46
The same with RSSFetcherrequire 'rss'
class RSSFetcher def initialize(url='http://secret.nsa.gov/rss') @url = url end
def fetch RSS::Parser.parse(open(@url).read).items endend
36 / 46
Since we're using Rails 4.1 and thetwo defaults are secret values, wecan move them to secrets.yml.development: default_caesar_step: 3 default_rss_url: 'http://secret.nsa.gov/rss'
def initialize(url=Rails.application.secrets.default_rss_url)
def initialize(step=Rails.application.secrets.default_caesar_step)
37 / 46
MessagesParser classclass MessagesParser def initialize @cipher = CaesarCipher.new @rss = RSSFetcher.new end
def execute messages = @rss.fetch messages.each { |m| parse(m) } end
private
def parse(message) Message.create(text: @cipher.decrypt(m)) endend
38 / 46
Does it depend on morestable things?
39 / 46
Using Dependency Inversionclass MessagesParser def initialize(cipher, fetcher) @cipher = cipher @fetcher = fetcher end
def execute messages = @fetcher.fetch messages.each { |m| parse(m) } end
private
def parse(message) Message.create(text: @cipher.decrypt(m)) endend
MessagesParser now depends upon abstractions being injected by callers
40 / 46
Alternative ciphers nowsupported
AES
DES
...
41 / 46
Alternative fetchers nowsupported
Files
Twitter :)
...
42 / 46
ConclusionsThe App is easily adaptable to futurechanges because of SOLID Design
Beautiful and abstract class designs, evenDesign Patterns (next), were discovered byrefactoring and following basic rules andprinciples
Learn to trust your nose and to be preparedfor the inevitable changes that are coming
43 / 46
ReferencesSOLID Object-Oriented Design, Sandi Metz,https://speakerdeck.com/skmetz/solid-object-oriented-design
44 / 46
BonusTo make sure the concept of SRP sinks in:
http://youtu.be/vlN17gMhnEk (get ready for British English and humour)
45 / 46
Questions?
46 / 46