15. Requirements
We are the FBI and our buddies from the NSA
post some encrypted messages for us in a
secret RSS feed.
Our job is to parse the messages from the feed
and store them in clear text in our DB, since we
don't understand encryption.
15 / 46
16. Encryption
The NSA are slightly better than us at
encryption, so they've used e Caesar Cipher to
encrypt the messages.
16 / 46
18. Public interface
class RSSMessagesParser
def execute
messages = fetch_messages
messages.each { |m| Message.create(text: decrypt(m)) }
end
private
def fetch_messages
# ...
end
def decrypt(message)
# ...
end
end
18 / 46
19. Fetching messages
require '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)
# ...
end
end
19 / 46
20. Entire class
require '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
end
end
20 / 46
21. But what if...
the "secret" URL changes?
the encryption method changes?
reading other RSS feeds is required?
21 / 46
22. Knowing when to refactor
Is it DRY?
Does it have a single responsibility?
Does everything in it change at the same
rate?
Does it depend on more stable things?
! The answer needs to be YES for all of them to move on.
22 / 46
25. 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 class
is doing too much!
25 / 46
26. Extracting responsibilities
class MessagesParser
def initialize
@cipher = NSACipher.new
@rss = RSSMessages.new
end
def execute
messages = @rss.fetch
messages.each { |m| Message.create(text: @cipher.decrypt(m)) }
end
end
26 / 46
27. SRP should be applied to
methods too
class 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))
end
end
27 / 46
28. The NSACipher
class NSACipher
def decrypt(message)
message.split('').map(&:ord).map { |a| a - 3 }.map(&:chr).join
end
end
28 / 46
29. The RSSMessages class
require 'rss'
class RSSMessages
def fetch
RSS::Parser.parse(open('http://secret.nsa.gov/rss').read).items
end
end
29 / 46
33. The RSS URL and the Cipher step
can change more often than the
classes, because the NSA want to
stay at least ahead of the novice
hackers by changing them
periodically.
33 / 46
34. Using Open/Close Principle
class NSACipher
def initialize(step=3)
@step = step
end
def decrypt(message)
message.split('').map(&:ord).map { |a| a - @step }.map(&:chr).join
end
end
Add default step to simplify current calls
The class is now open for extension and reusability.
NSACipher is essentially a generic Caesar cipher that can be used to decrypt
messages, it has nothing to do with NSA, so we can rename it.
34 / 46
35. Reusable CaesarCipher
class CaesarCipher
def initialize(step=3)
@step = step
end
def decrypt(message)
message.split('').map(&:ord).map { |a| a - @step }.map(&:chr).join
end
end
35 / 46
36. The same with RSSFetcher
require 'rss'
class RSSFetcher
def initialize(url='http://secret.nsa.gov/rss')
@url = url
end
def fetch
RSS::Parser.parse(open(@url).read).items
end
end
36 / 46
37. Since we're using Rails 4.1 and the
two defaults are secret values, we
can 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
38. MessagesParser class
class 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))
end
end
38 / 46
40. Using Dependency Inversion
class 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))
end
end
MessagesParser now depends upon abstractions being injected by callers
40 / 46
43. Conclusions
The App is easily adaptable to future
changes because of SOLID Design
Beautiful and abstract class designs, even
Design Patterns (next), were discovered by
refactoring and following basic rules and
principles
Learn to trust your nose and to be prepared
for the inevitable changes that are coming
43 / 46