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. Building
    Generic
    Software
    c h r i s s a L z b e r g

    View Slide

  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.

    View Slide

  3. Plan
    1. Generic Software
    2. Finding the Problem
    3. Going Generic
    4. Lessons

    View Slide

  4. 1
    Generic
    software

    View Slide

  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

    View Slide

  6. View Slide

  7. View Slide

  8. Your App

    View Slide

  9. View Slide

  10. 2Finding the
    problem

    View Slide

  11. يبرع
    english
    Suomi
    日本語 español
    italiano
    deutsch
    македонски
    français
    Swahili
    繁體中文
    한국의
    हिहन्दी
    یسراف

    View Slide

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

    View Slide

  13. Translate API
    class Talk
    translates :title, :abstract
    end

    View Slide

  14. Storage
    Patterns

    View Slide

  15. Translatable columns

    View Slide

  16. Translation Tables

    View Slide

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

    View Slide

  18. Access
    Patterns

    View Slide

  19. Fallbacks
    talk = Talk.new
    I18n.locale = :en
    talk.title = "Building Generic Software"
    I18n.locale = :'en-CA'
    talk.title
    #=> "Building Generic Software"

    View Slide

  20. Fallbacks
    def title
    fallback_locales.each do |locale|
    value = fetch_value(:title, locale)
    return value if value.present?
    end
    end
    [I18n.locale, :en]

    View Slide

  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"]}

    View Slide

  22. quering
    Talk.create(
    title_en: "Building Generic Software",
    title_ja: " 汎用ソフトウェア開ソフトウェア開発開発 ")
    Talk.find_by(title: "Building...", locale: :en)
    #=> #
    Talk.find_by(title: " 汎用ソフトウェア開 ...", locale: :ja)
    #=> #

    View Slide

  23. control
    Flows

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  27. class Talk
    def title
    read_from_storage(:title)
    end
    def title=(value)
    write_to_storage(:title, value)
    end
    end
    translates :title

    View Slide

  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

    View Slide

  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

    View Slide

  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"

    View Slide

  31. Talk application code

    View Slide

  32. Talk
    translates
    application code
    high-level interface

    View Slide

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

    View Slide

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

    View Slide

  35. View Slide

  36. How do you make something pluggable?

    View Slide

  37. 3
    Going
    Generic

    View Slide

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

    View Slide

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

    View Slide

  40. Talk
    translates
    application code
    high-level interface
    ?

    View Slide

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

    View Slide

  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)

    View Slide

  43. Inversion of control
    The
    Principle

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  49. class Talk
    def title_backend
    @backends[:title] ||=
    ColumnBackend.new(self, :title)
    end
    end
    define_accessor(:title)

    View Slide

  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

    View Slide

  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

    View Slide

  52. class ColumnBackend

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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}"

    View Slide

  57. Talk
    title

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  62. Talk
    read(:en)
    backend_class
    "Building Generic Software"
    title
    "Building
    Generic
    Software"
    “pluggable” storage logic

    View Slide

  63. Talk
    read(:en)
    translations
    TableBackend
    [#, ...]
    "Building Generic Software"
    title
    "Building
    Generic
    Software"
    “plug in” translation table backend

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  67. Core
    Core
    Column
    Column Table
    Table Json
    Json
    PROTOCOL
    read
    write setup_model
    Extensible Skeleton

    View Slide

  68. But wait...

    View Slide

  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

    View Slide

  70. Core
    Core
    PROTOCOL
    read
    write setup_model

    View Slide

  71. storage+access backend

    View Slide

  72. storage pattern
    access pattern
    backend
    plugin
    separation of concerns

    View Slide

  73. Translate API V3
    class Talk
    translates :title, backend: ColumnBackend,
    plugins: [FallbacksPlugin]
    end

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  78. ColumnBackend
    FallbacksPlugin
    read(:'en-CA')

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  82. Putting it together
    Backends each solve generic problem
    Plugins each solve generic problem
    Core is the glue linking them together

    View Slide

  83. shioyama / mobility

    View Slide

  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

    View Slide

  85. 4LESSONS

    View Slide

  86. View Slide

  87. “include entire frameworks”

    View Slide

  88. require "mobility"

    View Slide

  89. Core
    Core
    require "mobility"

    View Slide

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

    View Slide

  91. Core
    Core
    Column
    Column Table
    Table Json
    Json
    Backends Plugins
    Fallbacks
    Fallbacks Dirty
    Dirty Query
    Query
    require "mobility"

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  97. It’s the
    protocols,
    dummy

    View Slide

  98. jeremyevans / roda
    shrinerb / shrine

    View Slide

  99. complexity of protocol
    reusability of software

    View Slide

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

    View Slide

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

    View Slide

  102. Chris Salzberg
    @shioyama / [email protected]

    View Slide