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

Curmudgeon: An Opinionated Look at an Opinionated Framework

Curmudgeon: An Opinionated Look at an Opinionated Framework

Rails has opinions about how we should organize code, interact with a database, write tests, format URLs... just about everything. These conventions, the wisdom goes, free us up to focus on the specifics of our application. "Convention over configuration" becomes our mantra as development hurtles forward with startling speed.

At some point, we stop. We take stock of what we've built, and it's a mess. How did we get here?

Turns out, the decisions that Rails made for us had consequences.

Ernie Miller

April 25, 2014
Tweet

More Decks by Ernie Miller

Other Decks in Programming

Transcript

  1. curmudgeon: a person (especially an old man) who is easily

    annoyed or angered and who often complains Source: Merriam-Webster
  2. 8 8.bytes 8 * 1024 8.kilobytes 8 * 1024 **

    2 8.megabytes 8 * 1024 ** 3 8.gigabytes 8 * 1024 ** 4 8.terabytes 8 * 1024 ** 5 8.petabytes 8 * 1024 ** 6 8.exabytes Time.now + 5 * 86400 5.days.from_now Time.now - 5 * 86400 5.days.ago Time.now + 14 * 86400 1.fortnight.from_now Ruby Rails
  3. 8 8.bytes 8 * 1024 8.kilobytes 8 * 1024 **

    2 8.megabytes 8 * 1024 ** 3 8.gigabytes 8 * 1024 ** 4 8.terabytes 8 * 1024 ** 5 8.petabytes 8 * 1024 ** 6 8.exabytes Time.now + 5 * 86400 5.days.from_now Time.now - 5 * 86400 5.days.ago Time.now + 14 * 86400 1.fortnight.from_now Ruby Rails
  4. array[2..-1] || 0 array.from(2) array.first(3) array.to(2) array[1] array.second array[2] array.third

    array[3] array.fourth array[4] array.fifth array << obj array.append(obj) array.unshift obj array.prepend(obj) !array.include?(obj) array.exclude?(obj) array.include?(obj) obj.in?(array) Ruby Rails
  5. def beginning_of_day change(:hour => 0) end alias :midnight :beginning_of_day alias

    :at_midnight :beginning_of_day alias :at_beginning_of_day :beginning_of_day ! def middle_of_day change(:hour => 12) end alias :midday :middle_of_day alias :noon :middle_of_day alias :at_midday :middle_of_day alias :at_noon :middle_of_day alias :at_middle_of_day :middle_of_day active_support/core_ext/time/calculations.rb
  6. coffee brewed by forcing a small amount of nearly boiling

    water under pressure through finely ground coffee beans
  7. activerecord/README.rdoc “The prime directive for this mapping has been to

    minimize the amount of code needed to build a real-world domain model.”
  8. def inherited(child_class) child_class.initialize_generated_modules super end ! def initialize_generated_modules @generated_attribute_methods =

    Module.new { extend Mutex_m } @attribute_methods_generated = false include @generated_attribute_methods end active_record/attribute_methods.rb
  9. def inherited(child_class) child_class.initialize_generated_modules super end ! def initialize_generated_modules @generated_attribute_methods =

    Module.new { extend Mutex_m } @attribute_methods_generated = false include @generated_attribute_methods end active_record/attribute_methods.rb
  10. def inherited(child_class) child_class.initialize_generated_modules super end ! def initialize_generated_modules @generated_attribute_methods =

    Module.new { extend Mutex_m } @attribute_methods_generated = false include @generated_attribute_methods end active_record/attribute_methods.rb
  11. def method_missing(method, *args, &block) self.class.define_attribute_methods if respond_to_without_attributes?(method) if attribute_method =

    self.class.find_generated_attribute_method(method) attribute_method.bind(self).call(*args, &block) else send(method, *args, &block) end else super end end active_record/attribute_methods.rb
  12. def method_missing(method, *args, &block) self.class.define_attribute_methods if respond_to_without_attributes?(method) if attribute_method =

    self.class.find_generated_attribute_method(method) attribute_method.bind(self).call(*args, &block) else send(method, *args, &block) end else super end end active_record/attribute_methods.rb
  13. def define_attribute_methods generated_attribute_methods.synchronize do return false if @attribute_methods_generated superclass.define_attribute_methods unless

    self == base_class super(column_names) @attribute_methods_generated = true end true end active_record/attribute_methods.rb
  14. def define_attribute_methods generated_attribute_methods.synchronize do return false if @attribute_methods_generated superclass.define_attribute_methods unless

    self == base_class super(column_names) @attribute_methods_generated = true end true end active_record/attribute_methods.rb
  15. def define_attribute_methods generated_attribute_methods.synchronize do return false if @attribute_methods_generated superclass.define_attribute_methods unless

    self == base_class super(column_names) @attribute_methods_generated = true end true end active_record/attribute_methods.rb
  16. def column_names @column_names ||= columns.map { |column| column.name } end

    ! def columns @columns ||= connection.schema_cache.columns(table_name).map do |col| col = col.dup col.primary = (col.name == primary_key) col end end active_record/model_schema.rb
  17. def column_names @column_names ||= columns.map { |column| column.name } end

    ! def columns @columns ||= connection.schema_cache.columns(table_name).map do |col| col = col.dup col.primary = (col.name == primary_key) col end end active_record/model_schema.rb
  18. def column_names @column_names ||= columns.map { |column| column.name } end

    ! def columns @columns ||= connection.schema_cache.columns(table_name).map do |col| col = col.dup col.primary = (col.name == primary_key) col end end active_record/model_schema.rb
  19. def table_name reset_table_name unless defined?(@table_name) @table_name end ! def reset_table_name

    self.table_name = if abstract_class? superclass == Base ? nil : superclass.table_name elsif superclass.abstract_class? superclass.table_name || compute_table_name else compute_table_name end end active_record/model_schema.rb
  20. def table_name reset_table_name unless defined?(@table_name) @table_name end ! def reset_table_name

    self.table_name = if abstract_class? superclass == Base ? nil : superclass.table_name elsif superclass.abstract_class? superclass.table_name || compute_table_name else compute_table_name end end active_record/model_schema.rb
  21. def table_name reset_table_name unless defined?(@table_name) @table_name end ! def reset_table_name

    self.table_name = if abstract_class? superclass == Base ? nil : superclass.table_name elsif superclass.abstract_class? superclass.table_name || compute_table_name else compute_table_name end end active_record/model_schema.rb
  22. def compute_table_name base = base_class if self == base if

    parent < Base && !parent.abstract_class? contained = parent.table_name contained = contained.singularize if parent.pluralize_table_names contained += '_' end "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" else base.table_name end end active_record/model_schema.rb
  23. def compute_table_name base = base_class if self == base if

    parent < Base && !parent.abstract_class? contained = parent.table_name contained = contained.singularize if parent.pluralize_table_names contained += '_' end "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" else base.table_name end end active_record/model_schema.rb
  24. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore pluralize_table_names ? table_name.pluralize

    : table_name end active_record/model_schema.rb Conferences::RailsConf -> RailsConf -> rails_conf -> rails_confs
  25. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore pluralize_table_names ? table_name.pluralize

    : table_name end active_record/model_schema.rb Conferences::RailsConf -> RailsConf -> rails_conf -> rails_confs Animals::Moose -> Moose -> moose -> mooses
  26. module ActiveSupport Inflector.inflections(:en) do |inflect| inflect.plural(/$/, 's') inflect.plural(/s$/i, 's') inflect.plural(/^(ax|test)is$/i,

    '\1es') inflect.plural(/(octop|vir)us$/i, '\1i') inflect.plural(/(octop|vir)i$/i, '\1i') inflect.plural(/(alias|status)$/i, '\1es') inflect.plural(/(bu)s$/i, '\1ses') inflect.plural(/(buffal|tomat)o$/i, '\1oes') inflect.plural(/([ti])um$/i, '\1a') inflect.plural(/([ti])a$/i, '\1a') inflect.plural(/sis$/i, 'ses') inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves') inflect.plural(/(hive)$/i, '\1s') inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies') inflect.plural(/(x|ch|ss|sh)$/i, '\1es') inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices') inflect.plural(/^(m|l)ouse$/i, '\1ice') inflect.plural(/^(m|l)ice$/i, '\1ice') inflect.plural(/^(ox)$/i, '\1en') inflect.plural(/^(oxen)$/i, '\1') inflect.plural(/(quiz)$/i, '\1zes') ! inflect.singular(/s$/i, '') inflect.singular(/(ss)$/i, '\1') inflect.singular(/(n)ews$/i, '\1ews') inflect.singular(/([ti])a$/i, '\1um') inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis') inflect.singular(/(^analy)(sis|ses)$/i, '\1sis') inflect.singular(/([^f])ves$/i, '\1fe') active_support/inflections.rb
  27. inflect.singular(/(s)eries$/i, '\1eries') inflect.singular(/(m)ovies$/i, '\1ovie') inflect.singular(/(x|ch|ss|sh)es$/i, '\1') inflect.singular(/^(m|l)ice$/i, '\1ouse') inflect.singular(/(bus)(es)?$/i, '\1')

    inflect.singular(/(o)es$/i, '\1') inflect.singular(/(shoe)s$/i, '\1') inflect.singular(/(cris|test)(is|es)$/i, '\1is') inflect.singular(/^(a)x[ie]s$/i, '\1xis') inflect.singular(/(octop|vir)(us|i)$/i, '\1us') inflect.singular(/(alias|status)(es)?$/i, '\1') inflect.singular(/^(ox)en/i, '\1') inflect.singular(/(vert|ind)ices$/i, '\1ex') inflect.singular(/(matr)ices$/i, '\1ix') inflect.singular(/(quiz)zes$/i, '\1') inflect.singular(/(database)s$/i, '\1') ! inflect.irregular('person', 'people') inflect.irregular('man', 'men') inflect.irregular('child', 'children') inflect.irregular('sex', 'sexes') inflect.irregular('move', 'moves') inflect.irregular('zombie', 'zombies') ! inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police)) end end active_support/inflections.rb
  28. class Post < ImaginaryRecord::Record self.table_name 'posts' ! attribute :id, Attr::Integer

    attribute :title, Attr::String attribute :body, Attr::String attribute :created_at, Attr::Timestamp attribute :updated_at, Attr::Timestamp end in/your/imagination.rb
  29. class MagicRecord < ImaginaryRecord::Record def method_missing(method, *args, &block) self.class.define_attribute_methods #

    ... end ! def self.define_attribute_methods # ... klass.table_name(compute_table_name) columns_from_table(table_name).each do |column| klass.attribute column.name, column.type end # ... end end in/your/imagination.rb
  30. ActiveRecord Attribute typecasting Serialization Macro reflection Transactions Nested attributes Associations

    Secure passwords Timestamps Lifecycle callbacks Dirty tracking Validations Mass assignment sanitation
  31. ActiveRecord Attribute typecasting Serialization Macro reflection Transactions Nested attributes Associations

    Secure passwords Timestamps Lifecycle callbacks Dirty tracking Validations Mass assignment sanitation Querying Persisting
  32. MyModel #<Module:0x007fc42ad68878> ActiveRecord::Base ActiveRecord::Store ActiveRecord::Serialization ActiveModel::Serializers::Xml ActiveModel::Serializers::JSON ActiveModel::Serialization ActiveRecord::Reflection ActiveRecord::Transactions

    ActiveRecord::Aggregations ActiveRecord::NestedAttributes ActiveRecord::AutosaveAssociation ActiveModel::SecurePassword ActiveRecord::Associations ActiveRecord::Timestamp ActiveModel::Validations::Callbacks
  33. Class.new (2.1.1) Model View Controller Ancestors 4 56 50 63

    Public/ Protected Methods 99 575 244 351 Private Methods 90 126 101 105 Public/ Protected Methods 55 311 405 333 Private Methods 72 160 132 115 Class Instance
  34. module PostsHelper def summary(post) # excerpt and tags end end

    ! module ReviewsHelper def summary(review) # number of stars and excerpt end end
  35. <h1> <%= summarizable.title %> </h1> <p> <%= summary(summarizable) %> </p>

    app/views/shared/_summarizable_with_summary.html.erb
  36. module Foo def self.included(base) base.class_eval do def self.method_injected_by_foo ... end

    end end end ! module Bar def self.included(base) base.method_injected_by_foo end end ! class Host include Foo # We need to include this dependency for Bar include Bar # Bar is the module that Host really needs end
  37. require 'active_support/concern' ! module Foo extend ActiveSupport::Concern included do def

    self.method_injected_by_foo ... end end end ! module Bar extend ActiveSupport::Concern include Foo ! included do self.method_injected_by_foo end end ! class Host include Bar # works, Bar takes care now of its dependencies end
  38. +

  39. You

  40. You

  41. unless Array::filter Array::filter = (callback) -> element for element in

    this when callback(element) ! array = [1..10] ! filtered_array = array.filter (x) -> x % 2 == 0 ! gt_five = (x) -> x > 5 filtered_array = array.filter gt_five CoffeeScript
  42. Turbolinks “With Turbolinks pages will change without a full reload,

    so you can't rely on DOMContentLoaded or jQuery.ready() to trigger your code.” — Turbolinks README
  43. class Order < ActiveRecord::Base belongs_to :customer has_many :line_items, :dependent =>

    :destroy ! validates :customer, :presence => true validates :refnum, :uniqueness => true validates_associated :line_items end
  44. IDD

  45. describe Monster do let(:teeth) { nil } let(:claws) { nil

    } subject { Monster.new(teeth, claws) } ! describe 'with no teeth or claws' do it 'cannot bite' do proc { subject.bite('Ernie') }.must_raise NoMethodError end ! it 'cannot scratch' do proc { subject.bite('Ernie') }.must_raise NoMethodError end end end class Monster def initialize(teeth = nil, claws = nil) @teeth, @claws = teeth, claws end ! def bite(target) @teeth.bite(target) end ! def scratch(target) @claws.scratch(target) end end
  46. class Monster < ActiveRecord::Base include ::Monster::Biting include ::Monster::Scratching ! belongs_to

    :teeth belongs_to :claws validates :teeth, :claws, :presence => true end
  47. describe 'Monster::Biting' do let(:biter) { Class.new { attr_reader :teeth include

    ::Monster::Biting def initialize(teeth) @teeth = teeth end } } ! it ‘inflicts puncture damage' do # ... end end spec/behaviors/monster/biting_spec.rb
  48. MVP

  49. The art of writing software is getting the desired result

    while making the fewest decisions possible.