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

Introduction to ClassyEnum and Friends

Introduction to ClassyEnum and Friends

Presented at Burlington Ruby's June 2013 meet up

Peter Brown

June 26, 2013

More Decks by Peter Brown

Other Decks in Programming


  1. An Introduction to ClassyEnum and Friends For Indubitably Classy Ruby

  2. Classy Indeed. @beerlington beerlington.com Sir Beerlington III

  3. Burlington Ruby Conference August 3 - 4 @btvrubyconf burlingtonruby.com

  4. ClassyEnum

  5. What is an Enum? aka “enumerated type” or “enumeration” Data

    type consisting of named values: ‘elements’, ‘members’ or ‘enumerators’ Collection of related constants Each value is different, represents one thing
  6. Pseudo Examples Deck of cards Chess pieces Color palette Priority

  7. Why use Enums? Makes code more self-documenting Easy to change

    values in the future Reduces errors by mistyping values
  8. Exploring Enums in Ruby

  9. class States NEW_ENGLAND = [ :connecticut, :maine, :massachusetts, :new_hampshire, :rhode_island,

    :vermont ] end
  10. States::NEW_ENGLAND.last => :vermont States::NEW_ENGLAND.first => :connecticut States::NEW_ENGLAND.size => 6

  11. Enumerable Similar name, but not an enumerated type Can extend

    classes to mimic enumerable, or “collection” behavior [1,2,3].first => 1
  12. class States extend Enumerable NEW_ENGLAND = [:connecticut, :maine, :massachusetts, :new_hampshire,

    :rhode_island, :vermont] def self.each NEW_ENGLAND.each {|state| yield state } end end Must yield each successive element!
  13. States.first => :connecticut States.count => 6 States.select {|s| s.to_s.start_with? 'm'

    } => [:maine, :massachusetts]
  14. Also similar name, but not an enumerated type Adds convenience

    methods to iterate over a collection Enumerator [1,2,3].each.class => Enumerator
  15. states = States.to_enum => #<Enumerator: States:each> states.next => :connecticut states.next

    => :maine ...skip a few... states.next => :vermont states.next StopIteration: iteration reached an end
  16. Enums in Active Record Typically used as validation or database

    lookup tables Need to persist a value that represents a member String or built-in enum Integer
  17. class User < ActiveRecord::Base validates_inclusion_of :state, in: States end user

    = User.new(state: :vermont) user.valid? => true user.state = 'OMG TEXAS' user.valid? # => false
  18. Database Lookup Tables Painful to manage data Usually don’t really

    need to be in the database joins, joins, joins
  19. ClassyEnum

  20. Classes Object Oriented Programming Polymorphism Composition Factory Patterns

  21. Adding Logic class User < ActiveRecord::Base def lives_in_awesome_state? state ==

    'vermont' end end user = User.new(state: 'maine') user.lives_in_awesome_state? # => false user = User.new(state: 'vermont') user.lives_in_awesome_state? # => true
  22. class User < ActiveRecord::Base def lives_in_awesome_state? state == 'vermont' ||

    state == 'maine' end end user = User.new(state: 'maine') user.lives_in_awesome_state? # => true user = User.new(state: 'vermont') user.lives_in_awesome_state? # => true
  23. Should a user know if a state is awesome?

  24. What if a state knew if it was awesome?

  25. What if a state knew if it was awesome? user.state.awesome?

    # => true
  26. class User < ActiveRecord::Base def state State.new(super) end end class

    State def initialize(name) @name = name end def awesome? @name == 'vermont' || @name == 'maine' end end get value from database
  27. What else can be Improved? An individual state doesn’t know

    whether it’s awesome or not Conditional logic is harder to test and change
  28. Polymorphism Objects with the same interface, but different functionality (contract)

    Expect all objects to behave differently when given the same instructions Circle.draw Square.draw Triangle.draw
  29. Polymorphism Inheritance Single Table Inheritance in Active Record

  30. class User < ActiveRecord::Base def state super.camelize.constantize.new end end class

    State def awesome? false end end class Vermont < State def awesome? true end end false by default override in subclasses composition + factory
  31. user = User.new(state: 'vermont') user.state.awesome? # => true user.state =

    'maine' user.state.awesome? # => false
  32. Takeaways Reduce branching (if/else, conditions) Easier to test and change

    Statistically less bugs Pain to reproduce this logic everywhere you need it
  33. Polymorphism + Composition + Simple Factory + Validations

  34. Introducing Classy Enum

  35. Introducing Classy Enum

  36. What Does it Do? Everything I just discussed Wraps your

    enum values in Ruby classes Isolates behavior and properties Tons of other fun stuff...
  37. class State < ClassyEnum::Base end class State::Vermont < State end

    class User < ActiveRecord::Base classy_enum_attr :state end declaration in AR model enum definitions
  38. class State < ClassyEnum::Base def awesome? false end end class

    State::Vermont < State def awesome? true end end
  39. A state still knows if it is awesome. user =

    User.new(state: 'vermont') user.state.awesome? # => true
  40. Active Record Validation alarm = Alarm.new alarm.priority = 'low' alarm.valid?

    => true alarm.priority = 'lwo' alarm.valid? => false alarm.errors[:priority] => ["is not included in the list"]
  41. Classy Enumerables State.count => 6 State.first => connecticut State.max =>

    vermont State.max.text => "Vermont" State.select_options => [["Connecticut", "connecticut"], ["Maine", "maine"], ["Massachusetts", "massachusetts"]...]
  42. Internationalization es: classy_enum: priority: low: 'Bajo' medium: 'Medio' high: 'Alto'

    @alarm.priority = :low @alarm.priority.text # => 'Low' I18n.locale = :es @alarm.priority.text # => 'Bajo'
  43. Forms <%= form_for(@alarm) do |f| %> <div class="field"> <%= f.label

    :priority %><br> <%= f.select :priority, Priority.select_options %> </div> <div class="actions"> <%= f.submit %> </div> <% end %>
  44. Enum Generator $ rails g classy_enum State connecticut maine massachusetts...

    create app/enums create app/enums/state.rb class State < ClassyEnum::Base end class State::Connecticut < State end class State::Maine < State end class State::Massachusetts < State end ...
  45. Live code!

  46. Live code!

  47. Alternatives to Classy Enum Non-classy enums State machines

  48. Enumerize gem class User < ActiveRecord::Base extend Enumerize enumerize :role,

    in: [:user, :admin], default: :user end user = User.new user.role.user? # => true simple interface, but benefit of classes is lost
  49. State Machines Preferred over static values for states Provides hooks

    between/around state changes
  50. class Vehicle attr_accessor :seatbelt_on, :time_used, :auto_shop_busy state_machine :state, :initial =>

    :parked do after_transition :on => :crash, :do => :tow after_transition :on => :repair, :do => :fix after_transition any => :parked do |vehicle, transition| vehicle.seatbelt_on = false end end end
  51. None
  52. None
  53. Conclusion Worldwide Happiness # of projects using Classy Enum