Skip to content

Instantly share code, notes, and snippets.

@jonnyarnold
Last active June 15, 2017 15:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jonnyarnold/703cf482fa46ba1533e0 to your computer and use it in GitHub Desktop.
Save jonnyarnold/703cf482fa46ba1533e0 to your computer and use it in GitHub Desktop.
SOLID Workshops

Workshop 1

What is SOLID?

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.

Single Responsibility Principle

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?

Exercise

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.

Dependency Inversion Principle

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?

If Time Allows

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.

Reading List

http://confreaks.tv/videos/goruco2009-solid-object-oriented-design http://confreaks.tv/videos/rubyconf2009-solid-ruby

Workshop 2

Recap of Workshop 1

Interface Segregation Principle

Xerox: https://en.wikipedia.org/wiki/Interface_segregation_principle

More useful in typed languages with concepts of interfaces.

In Ruby, we don't have interface concepts, but there are still places you can use it: (Gear Example) if you don't have to put an argument into a method call, it can probably be separated.

In Go, most interfaces have a single method. DynamoDB on Tracker

Liskov Substitution Principle

You should be able to use a subclass anywhere that you would use the parent class.

Google Feeds example: https://github.com/reevoo/revieworld/tree/master/lib/google_feeds

We don't use subclasses very often. Maybe we should use them more? Useful for plugin design (e.g. ActiveRecord), avoids multiple inheritance problems e.g. The Deadly Diamond of Death.

Open/Closed Principle

https://en.wikipedia.org/wiki/Open/closed_principle

Software entities should be open for extension, but closed for modification.

https://youtu.be/dKRbsE061u4?t=14m45s until 19:26

Exercise:

class UsageFileParser
  def initialize(client, usage_file)
    @client = client
    @usage_file = usage_file
  end

  def parse
    case @client.usage_file_format
      when :xml
        parse_xml
      when :csv
        parse_csv
    end

    @client.last_parse = Time.now
    @client.save!
  end

  private

  def parse_xml
    # parse xml
  end

  def parse_csv
    # parse csv
  end
end

https://robots.thoughtbot.com/back-to-basics-solid

SOLID EXTRA!

Law of Demeter: https://en.wikipedia.org/wiki/Law_of_Demeter

Avoids hidden coupling.

http://devblog.avdi.org/2011/07/05/demeter-its-not-just-a-good-idea-its-the-law/

  • Method chaining is a smell, but don't worry too much.
  • Not good for expressive interfaces, e.g. Enumerable.

Example/Exercise: Bank Accounts.

# A Bank holds a list of accounts.
class Bank
  attr_accessor :accounts

  def initialize(accounts)
    @accounts = accounts
  end

  # We need to send the bailiffs round.
  # Where do they go?
  def postcodes_of_overdrawn_accounts
    accounts
      .select { |account| account.balance < 0 }
      .map { |account| account.person.address.postcode }
  end
end

class Account
  attr_accessor :person, :balance

  def initialize(person)
    @person = person
    @balance = 0
  end
end

class Person
  attr_reader :name, :address

  def initialize(name, address)
    @name = name
    @address = address
  end
end

class Address
  attr_reader :postcode

  def initialize(postcode)
    @postcode = postcode
  end
end

Real-life example: https://github.com/reevoo/revieworld/pull/1008#discussion-diff-44141363

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment