building_generic_software.pdf

 building_generic_software.pdf

05fdba1ae381f24512e977f8fe2697b4?s=128

Chris Salzberg

November 15, 2018
Tweet

Transcript

  1. 2.

    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. 5.

    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. 6.
  4. 7.
  5. 9.
  6. 12.

    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 #=> " 汎用ソフトウェア開ソフトウェア開発開発 "
  7. 19.

    Fallbacks talk = Talk.new I18n.locale = :en talk.title = "Building

    Generic Software" I18n.locale = :'en-CA' talk.title #=> "Building Generic Software"
  8. 20.

    Fallbacks def title fallback_locales.each do |locale| value = fetch_value(:title, locale)

    return value if value.present? end end [I18n.locale, :en]
  9. 21.

    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"]}
  10. 22.

    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, ...>
  11. 25.

    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
  12. 26.

    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
  13. 28.

    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
  14. 29.

    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
  15. 30.

    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"
  16. 35.
  17. 42.

    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)
  18. 47.

    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
  19. 48.

    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
  20. 50.

    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
  21. 51.

    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
  22. 54.

    class ColumnBackend def initialize(model, attribute) @model, @attribute = model, attribute

    end def read(locale) end def write(locale, value) end end
  23. 55.

    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
  24. 56.

    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}"
  25. 63.

    Talk read(:en) translations TableBackend [#<Talk::Translation>, ...] "Building Generic Software" title

    "Building Generic Software" “plug in” translation table backend
  26. 66.

    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
  27. 67.

    Core Core Column Column Table Table Json Json PROTOCOL read

    write setup_model Extensible Skeleton
  28. 69.

    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
  29. 75.

    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
  30. 76.

    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
  31. 77.

    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
  32. 82.

    Putting it together Backends each solve generic problem Plugins each

    solve generic problem Core is the glue linking them together
  33. 84.

    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
  34. 85.
  35. 86.
  36. 91.

    Core Core Column Column Table Table Json Json Backends Plugins

    Fallbacks Fallbacks Dirty Dirty Query Query require "mobility"
  37. 92.

    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
  38. 93.

    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
  39. 94.

    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
  40. 95.

    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
  41. 96.

    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