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 full-size 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 full-size slide

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

    View full-size slide

  4. 1
    Generic
    software

    View full-size 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 full-size slide

  6. 2Finding the
    problem

    View full-size slide

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

    View full-size slide

  8. 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 full-size slide

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

    View full-size slide

  10. Storage
    Patterns

    View full-size slide

  11. Translatable columns

    View full-size slide

  12. Translation Tables

    View full-size slide

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

    View full-size slide

  14. Access
    Patterns

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  17. 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 full-size slide

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

    View full-size slide

  19. control
    Flows

    View full-size slide

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

    View full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

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

    View full-size slide

  24. 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 full-size slide

  25. 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 full-size slide

  26. 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 full-size slide

  27. Talk application code

    View full-size slide

  28. Talk
    translates
    application code
    high-level interface

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  31. How do you make something pluggable?

    View full-size slide

  32. 3
    Going
    Generic

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. Talk
    translates
    application code
    high-level interface
    ?

    View full-size slide

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

    View full-size slide

  37. 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 full-size slide

  38. Inversion of control
    The
    Principle

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  42. 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 full-size slide

  43. 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 full-size slide

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

    View full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. class ColumnBackend

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  50. 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 full-size slide

  51. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  59. 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 full-size slide

  60. 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 full-size slide

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

    View full-size slide

  62. 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 full-size slide

  63. Core
    Core
    PROTOCOL
    read
    write setup_model

    View full-size slide

  64. storage+access backend

    View full-size slide

  65. storage pattern
    access pattern
    backend
    plugin
    separation of concerns

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  68. 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 full-size slide

  69. 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 full-size slide

  70. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  76. shioyama / mobility

    View full-size slide

  77. 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 full-size slide

  78. “include entire frameworks”

    View full-size slide

  79. require "mobility"

    View full-size slide

  80. Core
    Core
    require "mobility"

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  83. 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 full-size slide

  84. 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 full-size slide

  85. 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 full-size slide

  86. 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 full-size slide

  87. 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 full-size slide

  88. It’s the
    protocols,
    dummy

    View full-size slide

  89. jeremyevans / roda
    shrinerb / shrine

    View full-size slide

  90. complexity of protocol
    reusability of software

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  93. Chris Salzberg
    @shioyama / [email protected]

    View full-size slide