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

building_generic_software.pdf

 building_generic_software.pdf

Chris Salzberg

November 15, 2018
Tweet

More Decks by Chris Salzberg

Other Decks in Programming

Transcript

  1. About Me • My handle is @shioyama. • I live

    in Tokyo, Japan. • I’m a Canadian from Montréal. • I work at a company called Degica. • I’m the author of a gem called Mobility. • I blog at dejimata.com.
  2. Jeremy Evans One of the best ways to write flexible

    software is to write generic software. Instead of designing a single API that completely handles a specific case, you write multiple APIs that handle smaller, more generic parts of that use case and then handling the entire case is just gluing those parts together. When you approach code like that, designing APIs that solve generic problems, you can more easily reuse those APIs later, to solve other problems.” “ The Development of Sequel, May 2012 “
  3. The Translated Attribute I18n.locale = :en talk = Talk.new talk.title

    = "Building Generic Software" talk.title #=> "Building Generic Software" I18n.locale = :ja talk.title #=> nil talk.title = " 汎用ソフトウェア開ソフトウェア開発開発 " talk.title #=> " 汎用ソフトウェア開ソフトウェア開発開発 "
  4. Fallbacks talk = Talk.new I18n.locale = :en talk.title = "Building

    Generic Software" I18n.locale = :'en-CA' talk.title #=> "Building Generic Software"
  5. Fallbacks def title fallback_locales.each do |locale| value = fetch_value(:title, locale)

    return value if value.present? end end [I18n.locale, :en]
  6. Dirty Tracking I18n.locale = :en talk.title = "Building Generic Software"

    talk.save talk.title = "Building Specific Software" talk.changes #=> {"title_en"=> ["Building Generic Software", "Building Specific Software"]}
  7. quering Talk.create( title_en: "Building Generic Software", title_ja: " 汎用ソフトウェア開ソフトウェア開発開発 ")

    Talk.find_by(title: "Building...", locale: :en) #=> #<Talk id: 1, ...> Talk.find_by(title: " 汎用ソフトウェア開 ...", locale: :ja) #=> #<Talk id: 1, ...>
  8. def translates(*attributes) attributes.each { |a| define_accessor(a) } end def define_accessor(attribute)

    define_method(attribute) do read_from_storage(attribute) end define_method("#{attribute}=") do |value| write_to_storage(attribute, value) end end
  9. def translates(*attributes) attributes.each { |a| define_accessor(a) } end def define_accessor(attribute)

    define_method(attribute) do read_from_storage(attribute) end define_method("#{attribute}=") do |value| write_to_storage(attribute, value) end end
  10. module InstanceMethods def read_from_storage(attribute) fallback_locales.each do |locale| value = column_value(attribute,

    locale) return value if value.present? end nil end def column_value(attribute, locale) read_attribute("#{attribute}_#{locale}") end end
  11. module InstanceMethods def read_from_storage(attribute) fallback_locales.each do |locale| value = column_value(attribute,

    locale) return value if value.present? end nil end def column_value(attribute, locale) read_attribute("#{attribute}_#{locale}") end end fallbacks
  12. module InstanceMethods def read_from_storage(attribute) fallback_locales.each do |locale| value = column_value(attribute,

    locale) return value if value.present? end nil end def column_value(attribute, locale) read_attribute("#{attribute}_#{locale}") end end column storage "title_en"
  13. Inversion of control One important characteristic of a framework is

    that the methods defined by the user to tailor the framework will often be called from within the framework itself, rather than from the user's application code. The framework often plays the role of the main program in coordinating and sequencing application activity. This inversion of control gives frameworks the power to serve as extensible skeletons. The methods supplied by the user tailor the generic algorithms defined in the framework for a particular application.” “ - Ralph E. Johnson & Brian Foote, Designing Reusable Classes (1988)
  14. def translates(*attributes, backend:) attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend) end

    end def define_backend(attribute, backend_class) define_method "#{attribute}_backend" do @backends[attribute] ||= backend_class.new(self, attribute) end end
  15. def translates(*attributes, backend:) attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend) end

    end def define_backend(attribute, backend_class) define_method "#{attribute}_backend" do @backends[attribute] ||= backend_class.new(self, attribute) end end ColumnBackend
  16. class Talk def title title_backend.read(I18n.locale) end def title=(value) title_backend.write(I18n.locale, value)

    end def title_backend @backends[:title] ||= ColumnBackend.new(self, :title) end end
  17. class Talk def title title_backend.read(I18n.locale) end def title=(value) title_backend.write(I18n.locale, value)

    end def title_backend @backends[:title] ||= ColumnBackend.new(self, :title) end end protocol
  18. class ColumnBackend def initialize(model, attribute) @model, @attribute = model, attribute

    end def read(locale) end def write(locale, value) end end
  19. class ColumnBackend def initialize(model, attribute) @model, @attribute = model, attribute

    end def read(locale) @model.read_attribute(column(locale)) end def write(locale, value) @model.write_attribute(column(locale), value) end end
  20. class ColumnBackend def initialize(model, attribute) @model, @attribute = model, attribute

    end def read(locale) @model.read_attribute(column(locale)) end def write(locale, value) @model.write_attribute(column(locale), value) end end "#{@attribute}_#{locale}"
  21. Talk read(:en) translations TableBackend [#<Talk::Translation>, ...] "Building Generic Software" title

    "Building Generic Software" “plug in” translation table backend
  22. class TableBackend def self.setup_model(model_class, _) translation = get_translation_class(model_class) model_class.has_many :translations,

    class_name: translation.name translation.belongs_to :translated_model, class_name: model_class.name end end
  23. Core Core Column Column Table Table Json Json PROTOCOL read

    write setup_model Extensible Skeleton
  24. class ColumnWithFallbacksBackend def read(locale) fallback_locales.each do |locale| value = column_value(locale)

    return value if value.present? end nil end private def column_value(locale) @model.read_attribute(column(locale)) end end fallbacks
  25. def translates(*attributes, backend:, plugins: []) backend_subclass = Class.new(backend) plugins.each do

    |plugin| backend_subclass.include plugin end attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend_subclass) end backend_subclass.setup_model(self, attributes) end plugins
  26. class ColumnBackend def read(locale) @model.read_attribute(column(locale)) end end module FallbacksPlugin def

    read(locale) fallback_locales.each do |locale| value = super(locale) return value if value.present? end nil end end
  27. class ColumnBackend def read(locale) @model.read_attribute(column(locale)) end end module FallbacksPlugin def

    read(locale) fallback_locales.each do |locale| value = super(locale) return value if value.present? end nil end end
  28. Putting it together Backends each solve generic problem Plugins each

    solve generic problem Core is the glue linking them together
  29. Core Core Column Column Table Table Json Json BACKEND PROTOCOL

    read write Storage Logic Plugin Logic Fallbacks Fallbacks Dirty Dirty Query Query ATTRIBUTES PROTOCOL initialize included setup_model
  30. Core Core Column Column Table Table Json Json Backends Plugins

    Fallbacks Fallbacks Dirty Dirty Query Query require "mobility"
  31. module Translates def translates(*attributes, backend:, plugins: []) backend_subclass = Class.new(backend)

    plugins.each { |plugin| backend_subclass.include plugin } attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend_subclass) end backend_subclass.setup_model(self, attributes) end def define_backend(attribute, backend_class) define_method "#{attribute}_backend" do @backends ||= {} @backends[attribute] ||= backend_class.new(self, attribute) end end def define_accessor(attribute) define_method(attribute) do send("#{attribute}_backend").read(I18n.locale) end define_method("#{attribute}=") do |value| send("#{attribute}_backend").write(I18n.locale, value) end end end
  32. module Translates def translates(*attributes, backend:, plugins: []) backend_subclass = Class.new(backend)

    plugins.each { |plugin| backend_subclass.include plugin } attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend_subclass) end backend_subclass.setup_model(self, attributes) end def define_backend(attribute, backend_class) define_method "#{attribute}_backend" do @backends ||= {} @backends[attribute] ||= backend_class.new(self, attribute) end end def define_accessor(attribute) define_method(attribute) do send("#{attribute}_backend").read(I18n.locale) end define_method("#{attribute}=") do |value| send("#{attribute}_backend").write(I18n.locale, value) end end end No ActiveRecord
  33. module Translates def translates(*attributes, backend:, plugins: []) backend_subclass = Class.new(backend)

    plugins.each { |plugin| backend_subclass.include plugin } attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend_subclass) end backend_subclass.setup_model(self, attributes) end def define_backend(attribute, backend_class) define_method "#{attribute}_backend" do @backends ||= {} @backends[attribute] ||= backend_class.new(self, attribute) end end def define_accessor(attribute) define_method(attribute) do send("#{attribute}_backend").read(I18n.locale) end define_method("#{attribute}=") do |value| send("#{attribute}_backend").write(I18n.locale, value) end end end No ActiveRecord No ActiveSupport
  34. module Translates def translates(*attributes, backend:, plugins: []) backend_subclass = Class.new(backend)

    plugins.each { |plugin| backend_subclass.include plugin } attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend_subclass) end backend_subclass.setup_model(self, attributes) end def define_backend(attribute, backend_class) define_method "#{attribute}_backend" do @backends ||= {} @backends[attribute] ||= backend_class.new(self, attribute) end end def define_accessor(attribute) define_method(attribute) do send("#{attribute}_backend").read(I18n.locale) end define_method("#{attribute}=") do |value| send("#{attribute}_backend").write(I18n.locale, value) end end end No ActiveRecord No ActiveSupport No Persisted Storage
  35. module Translates def translates(*attributes, backend:, plugins: []) backend_subclass = Class.new(backend)

    plugins.each { |plugin| backend_subclass.include plugin } attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend_subclass) end backend_subclass.setup_model(self, attributes) end def define_backend(attribute, backend_class) define_method "#{attribute}_backend" do @backends ||= {} @backends[attribute] ||= backend_class.new(self, attribute) end end def define_accessor(attribute) define_method(attribute) do send("#{attribute}_backend").read(I18n.locale) end define_method("#{attribute}=") do |value| send("#{attribute}_backend").write(I18n.locale, value) end end end No ActiveRecord No ActiveSupport No Persisted Storage only references to i18n