Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Opening up to change

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.

Andy Pike

October 02, 2015
Tweet

More Decks by Andy Pike

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. 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
  4. 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
  5. class ProductRow def initialize(row) @row = row end def valid?

    @row.field("name").present? end end @andypike
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. image = Image.new.open(file_path) # instead of this: image.blur.greyscale # you

    can use this: image.apply(Blur.new, Greyscale.new) @andypike
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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