building_generic_software.pdf

 building_generic_software.pdf

05fdba1ae381f24512e977f8fe2697b4?s=128

Chris Salzberg

November 15, 2018
Tweet

Transcript

  1. Building Generic Software c h r i s s a

    L z b e r g
  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.
  3. Plan 1. Generic Software 2. Finding the Problem 3. Going

    Generic 4. Lessons
  4. 1 Generic software

  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 “
  6. None
  7. None
  8. Your App

  9. None
  10. 2Finding the problem

  11. يبرع english Suomi 日本語 español italiano deutsch македонски français Swahili

    繁體中文 한국의 हिहन्दी یسراف
  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 #=> " 汎用ソフトウェア開ソフトウェア開発開発 "
  13. Translate API class Talk translates :title, :abstract end

  14. Storage Patterns

  15. Translatable columns

  16. Translation Tables

  17. Json Translations { "en": "Building Generic Software", "ja": " 汎用ソフトウェア開ソフトウェア開発開発

    " }
  18. Access Patterns

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

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

    return value if value.present? end end [I18n.locale, :en]
  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"]}
  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, ...>
  23. control Flows

  24. def translates(*attributes) attributes.each { |a| define_accessor(a) } end

  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
  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
  27. class Talk def title read_from_storage(:title) end def title=(value) write_to_storage(:title, value)

    end end translates :title
  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
  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
  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"
  31. Talk application code

  32. Talk translates application code high-level interface

  33. Talk translates read_from_storage application code high-level interface low-level implementation

  34. Talk translates read_from_storage application code high-level interface low-level implementation

  35. None
  36. How do you make something pluggable?

  37. 3 Going Generic

  38. Talk translates read_from_storage application code high-level interface low-level implementation

  39. Talk translates read_from_storage application code high-level interface low-level implementation

  40. Talk translates application code high-level interface ?

  41. Talk translates application code high-level interface ? Inversion of control

  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)
  43. Inversion of control The Principle

  44. Inversion of control The Principle Don’t call us, we’ll call

    you.
  45. Translate API v2 class Talk translates :title, backend: ColumnBackend end

  46. def translates(*attributes, backend:) attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend) end

    end ColumnBackend
  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
  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
  49. class Talk def title_backend @backends[:title] ||= ColumnBackend.new(self, :title) end end

    define_accessor(:title)
  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
  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
  52. class ColumnBackend

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

    end
  54. class ColumnBackend def initialize(model, attribute) @model, @attribute = model, attribute

    end def read(locale) end def write(locale, value) end end
  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
  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}"
  57. Talk title

  58. Talk ColumnBackend title ColumnBackend.new(self, :title)

  59. Talk read(:en) ColumnBackend title ColumnBackend.new(self, :title)

  60. Talk read(:en) read_attribute("title_en") ColumnBackend "Building Generic Software" title ColumnBackend.new(self, :title)

  61. Talk read(:en) read_attribute("title_en") ColumnBackend "Building Generic Software" "Building Generic Software"

    title "Building Generic Software" ColumnBackend.new(self, :title)
  62. Talk read(:en) backend_class "Building Generic Software" title "Building Generic Software"

    “pluggable” storage logic
  63. Talk read(:en) translations TableBackend [#<Talk::Translation>, ...] "Building Generic Software" title

    "Building Generic Software" “plug in” translation table backend
  64. def translates(*attributes, backend:) attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend) end

    end
  65. def translates(*attributes, backend:) attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend) end

    backend.setup_model(self, attributes) end custom setup logic
  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
  67. Core Core Column Column Table Table Json Json PROTOCOL read

    write setup_model Extensible Skeleton
  68. But wait...

  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
  70. Core Core PROTOCOL read write setup_model

  71. storage+access backend

  72. storage pattern access pattern backend plugin separation of concerns

  73. Translate API V3 class Talk translates :title, backend: ColumnBackend, plugins:

    [FallbacksPlugin] end
  74. def translates(*attributes, backend:) attributes.each do |attribute| define_accessor(attribute) define_backend(attribute, backend) end

    backend.setup_model(self, attributes) end
  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
  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
  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
  78. ColumnBackend FallbacksPlugin read(:'en-CA')

  79. ColumnBackend FallbacksPlugin read(:'en-CA') super(:'en-CA') nil

  80. ColumnBackend FallbacksPlugin read(:'en-CA') super(:'en-CA') nil super(:en) "Building Generic Software"

  81. ColumnBackend FallbacksPlugin read(:'en-CA') super(:'en-CA') nil super(:en) "Building Generic Software" "Building

    Generic Software"
  82. Putting it together Backends each solve generic problem Plugins each

    solve generic problem Core is the glue linking them together
  83. shioyama / mobility

  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
  85. 4LESSONS

  86. None
  87. “include entire frameworks”

  88. require "mobility"

  89. Core Core require "mobility"

  90. Core Core Column Column Table Table Json Json Backends require

    "mobility"
  91. Core Core Column Column Table Table Json Json Backends Plugins

    Fallbacks Fallbacks Dirty Dirty Query Query require "mobility"
  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
  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
  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
  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
  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
  97. It’s the protocols, dummy

  98. jeremyevans / roda shrinerb / shrine

  99. complexity of protocol reusability of software

  100. complexity of protocol reusability of software “maximally general interface”

  101. complexity of protocol reusability of software “maximally general interface” generic

    software
  102. Chris Salzberg @shioyama / chris@dejimata.com