Opening up to change

196ab25f16dcfd37518a41ceb15e0da0?s=47 Andy Pike
October 02, 2015

Opening up to change

The video for this talk is here: http://andypike.com/blog/conferences/arrrrcamp-2015/

The one constant in software is change.

Your code will need to change and adapt over time. We should try to write code that is easy to change in the future. If possible, change without having to edit existing code.

This talk is all about the O in SOLID: the Open-Closed principle. I'll explain what the Open-Closed principle is, why this is important and how to structure and refactor code to make it "open for extension but closed for modification".

In this talk I'll show you, by example, how to make your code more changeable. In fact, so changeable that you will be able to extend what your program does and how it behaves without modifying a single line of existing code. Starting with a real world example that is painful to extend, I’ll refactor it over many iterations until it truly is Open-Closed. I'll show techniques, trade-offs and some gotchas from real world experience which will help you write more flexible programs in the future.

If you’ve never heard of the Open-Closed principle or are unsure how to put it into practice then this talk is for you. I would love to help open the door of this technique and give you the ability to alter your system in a painless way by opening it up to change.

This talk was given at Arrrrcamp 2015 http://2015.arrrrcamp.be.

196ab25f16dcfd37518a41ceb15e0da0?s=128

Andy Pike

October 02, 2015
Tweet

Transcript

  1. Opening'up'to'change Andy%Pike @andypike

  2. Hello @andypike @andypike

  3. Say$hi @andypike

  4. @andypike

  5. @andypike

  6. @andypike

  7. @andypike

  8. @andypike

  9. @andypike

  10. @andypike

  11. @andypike

  12. @andypike

  13. Changing'so*ware @andypike

  14. O.C.P. @andypike

  15. @andypike

  16. @andypike

  17. Open/closed+principle @andypike

  18. What%is%it? @andypike

  19. @andypike

  20. Open%for%extension, but%closed%for%modifica6on @andypike

  21. Classes&can&only&be&modified&to&correct& errors.&New&or&changed&features&would& require&that&a&different&class&be& created. @andypike

  22. S.O.L.I.D. @andypike

  23. Robert'Mar*n (Uncle'Bob) @andypike

  24. Why$is$OCP$important? @andypike

  25. Risk%&%Cost @andypike

  26. Simple'vs'Easy @andypike

  27. A"good"design"is"easier"to"change" than"a"bad"design —"Dave"Thomas @andypike

  28. Embrace(change @andypike

  29. Stop%your%whining! @andypike

  30. Slight'Sidetrack @andypike

  31. What%is%the%number%one%thing%that% makes%our%programs%harder%to% change?* (*#a%er#extensive#research#and#polling#of#exactly#one#person#7#me) @andypike

  32. if @andypike

  33. if book_loan.due_on < Date.today && book_loan.returned == false # Do

    something end # If you can't avoid it, at least name the concept: if book_loan.overdue? # Do something end @andypike
  34. Using&OCP @andypike

  35. Inheritance @andypike

  36. class Car def accelerate end def change_gear end end @andypike

  37. class Convertible < Car def open_roof end end # Then

    we can do this: my_car = Convertible.new my_car.open_roof my_car.accelerate @andypike
  38. Basis%of%original open/closed%thinking @andypike

  39. Dependency(Injec+on @andypike

  40. class Library def loan(book) # Do something interesting log("The book

    #{book.title} was loaned out") end def log(message) # Write line to file... end end @andypike
  41. class FileLogger def write(message) # Write line to file... end

    end @andypike
  42. class Library def initialize(logger = FileLogger.new) @logger = logger end

    def loan(book) log("The book #{book.title} was loaned out") end def log(message) @logger.write(message) end end @andypike
  43. Library.new.loan(book) Library.new(FileLogger.new).loan(book) Library.new(DatabaseLogger.new).loan(book) Library.new(SlackLogger.new).loan(book) @andypike

  44. class LogEverywhere def write(message) FileLogger.new.write(message) DatabaseLogger.new.write(message) SlackLogger.new.write(message) end end @andypike

  45. When%a%role%needs to%change%just%create a%class%and%pass%it%in @andypike

  46. Decorator(Pa*ern @andypike

  47. def import_products(file_path) CSV.read(file_path, :headers => true).each do |row| if row.field("name").present?

    # do something interesting end end end @andypike
  48. class ProductRow def initialize(row) @row = row end def valid?

    @row.field("name").present? end end @andypike
  49. require "delegate" class ProductRow < SimpleDelegator def valid? field("name").present? end

    end @andypike
  50. def products_in(file_path) CSV.read(file_path, :headers => true).map do |r| ProductRow.new(r) end

    end def import_products(file_path) products_in(file_path).each do |row| if row.valid? # do something interesting end end end @andypike
  51. def valid_products_in(file_path) CSV.read(file_path, :headers => true) .map { |p| ProductRow.new(p)

    } .select(&:valid?) end def import_products(file_path) valid_products_in(file_path).each do |row| # do something interesting end end @andypike
  52. Useful'when'separa.ng concerns,'or'when'not'in control'of'object'crea.on @andypike

  53. Command'Pa)ern @andypike

  54. class Image def open(file_path) # Load image file end def

    blur # Blur the image, return new version end def greyscale # Greyscale the image, return new version end end @andypike
  55. # Blur and greyscale an image image = Image.new.open(file_path) image.blur.greyscale

    @andypike
  56. class Blur def apply_to(image) # Blur the image, return new

    version end end class Greyscale def apply_to(image) # Greyscale the image, return new version end end @andypike
  57. class Image def open(file_path) # Load image file end def

    apply(*commands) commands.reduce(self) do |image, command| command.apply_to(image) end end end @andypike
  58. image = Image.new.open(file_path) # instead of this: image.blur.greyscale # you

    can use this: image.apply(Blur.new, Greyscale.new) @andypike
  59. Encapsulate+behaviour+in a+class+and+pass+it+around @andypike

  60. Service'Locator'Pa-ern @andypike

  61. class NotificationCentre def send_welcome_to(user) send_email(user.email, :welcome) end def send_email(email_address, content)

    # Uses some service to send emails end end @andypike
  62. class NotificationCentre def send_welcome_to(user) if user.notification_method == :sms send_sms(user.mobile, :welcome)

    else send_email(user.email, :welcome) end end def send_sms(mobile_number, content) # Uses some service to send sms end def send_email(email_address, content) # ... @andypike
  63. class NotificationCentre def send_welcome_to(user) if user.notificaton_method == :webhook send_webhook(user.webhook_url, :welcome)

    elsif user.notificaton_method == :sms send_sms(user.mobile, :welcome) else send_email(user.email, :welcome) end end def send_webhook(url, content) # Make HTTP call to url end @andypike
  64. @andypike

  65. class EmailNotifier def deliver(user, content) # Send an email to

    `user.email`... end end @andypike
  66. class NotificationCentre def send_welcome_to(user) if user.notificaton_method == :webhook WebhookNotifier.new.deliver(user, :welcome)

    elsif user.notificaton_method == :sms SmsNotifier.new.deliver(user, :welcome) else EmailNotifier.new.deliver(user, :welcome) end end end @andypike
  67. class NotificationCentre def send_welcome_to(user) case user.notificaton_method when :webhook WebhookNotifier.new.deliver(user, :welcome)

    when :sms SmsNotifier.new.deliver(user, :welcome) else EmailNotifier.new.deliver(user, :welcome) end end end @andypike
  68. class NotificationCentre def send_welcome_to(user) notifier_for(user).new.deliver(user, :welcome) end def notifier_for(user) case

    user.notification_method when :webhook WebhookNotifier when :sms SmsNotifier else EmailNotifier end end end @andypike
  69. class NotificationCentre def send_welcome_to(user) notifier_for(user).new.deliver(user, :welcome) end def notifier_for(user) NotifierRegistry.notifier_for(user.notification_method)

    end end @andypike
  70. class NotifierRegistry def self.notifier_for(notification_method) case notification_method when :webhook WebhookNotifier when

    :sms SmsNotifier else EmailNotifier end end end @andypike
  71. class NotifierRegistry def self.notifier_for(notification_method) registry.fetch(notification_method) { EmailNotifier } end def

    self.registry { :webhook => WebhookNotifier, :sms => SmsNotifier, :email => EmailNotifier } end end @andypike
  72. method_options = NotifierRegistry.methods.map { |m| [m.titleize, m] } select :user,

    :notification_method, method_options @andypike
  73. Popula'ng*the*registry @andypike

  74. Configura)on @andypike

  75. NotifierRegistry.registry = { :webhook => WebhookNotifier, :sms => SmsNotifier, :email

    => EmailNotifier } @andypike
  76. Auto%Register: By%Conven2on @andypike

  77. class NotifierRegistry @registry = {} def self.register(notification_method, notifier) @registry[notification_method] =

    notifier end def self.notifier_for(notification_method) @registry.fetch(notification_method.to_sym) { EmailNotifier } end end @andypike
  78. def self.load Dir["./notifiers/*.rb"].each { |file| require file } Module.constants .map(&:to_s)

    .select { |name| name.end_with?("Notifier") } .each do |name| method = name.chomp("Notifier").downcase.to_sym notifier = Object.const_get(name) register(method, notifier) # => :email, EmailNotifier end end @andypike
  79. Auto%Register: Self%Registra1on @andypike

  80. class NotifierRegistry @registry = {} def self.register(notifier) @registry[notifier.notification_method] = notifier

    end def self.notifier_for(notification_method) @registry.fetch(notification_method.to_sym) { EmailNotifier } end end @andypike
  81. module SelfRegisteringNotifier def self.included(base) base.send(:extend, ClassMethods) end module ClassMethods attr_accessor

    :notification_method def send_notifications_via(notification_method) self.notification_method = notification_method NotifierRegistry.register(self) end end end @andypike
  82. class EmailNotifier include SelfRegisteringNotifier send_notifications_via :email def deliver(user, content) #

    Sends an email... end end @andypike
  83. Rails&Autoload&Gotcha @andypike

  84. # Do not eager load code on boot. config.eager_load =

    false @andypike
  85. def self.notifier_for(notification_method) ensure_loaded @registry.fetch(notification_method.to_sym) { EmailNotifier } end def ensure_loaded

    return if already_loaded? Dir["./notifiers/*.rb"].each { |file| require_dependency(file) } loaded! end def self.already_loaded? loaded? || Rails.configuration.eager_load end @andypike
  86. Demo @andypike

  87. @andypike

  88. @andypike

  89. We#extended#behaviour without#a#single#modifica7on! @andypike

  90. Popula'ng*the*Registry @andypike

  91. Look$up$behaviour$at$run0me. When$change$comes,$register a$new$class. @andypike

  92. Balance @andypike

  93. It#may#seem#like#overkill#in#the# beginning,#but#as#the#codebase#ages# you#will#reap#the#rewards#of#good# object#design. —"Andy"Pike @andypike

  94. Yes,%I%just%quoted%myself @andypike

  95. More%than%just%design%pa1erns @andypike

  96. Code%that%is%easier%to understand%is%easier%to%change @andypike

  97. Code%is%read%many%more -mes%than%it%is%wri1en @andypike

  98. We#follow#exis+ng#code#styles @andypike

  99. Did$you$spot$a$theme? @andypike

  100. Rails:'Err,'where'do'I'put'them? @andypike

  101. Summary @andypike

  102. Things'to'consider • Create'Classes • SOLID • Simple'vs'Easy • Balance @andypike

  103. My#Guidelines • Name&things&well • Make&things&small • Avoid&condi6onals • Avoid&nils @andypike

  104. We're%hiring! @andypike

  105. @andypike @andypike

  106. Credits • Bertrand_Meyer,Photo:,By,David.Monniaux,(Own,work),[CC,BY? SA,3.0,(hDp:/ /creaHvecommons.org/licenses/by?sa/3.0),or,GFDL, (hDp:/ /www.gnu.org/copyleQ/fdl.html)],,via,Wikimedia, Commons • Simple,vs,Easy,graph:,hDps:/

    /blog.wearewizards.io/some?a? priori?good?qualiHes?of?soQware?development • Robert,MarHn,photo:,By,Tim?bezhashvyly,(Own,work),[CC,BY?SA, 4.0,(hDp:/ /creaHvecommons.org/licenses/by?sa/4.0)],,via, @andypike