Slide 1

Slide 1 text

Building Generic Software c h r i s s a L z b e r g

Slide 2

Slide 2 text

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.

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

1 Generic software

Slide 5

Slide 5 text

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 “

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

Your App

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

2Finding the problem

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Translate API class Talk translates :title, :abstract end

Slide 14

Slide 14 text

Storage Patterns

Slide 15

Slide 15 text

Translatable columns

Slide 16

Slide 16 text

Translation Tables

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Access Patterns

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

control Flows

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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"

Slide 31

Slide 31 text

Talk application code

Slide 32

Slide 32 text

Talk translates application code high-level interface

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

How do you make something pluggable?

Slide 37

Slide 37 text

3 Going Generic

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Talk translates application code high-level interface ?

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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)

Slide 43

Slide 43 text

Inversion of control The Principle

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

class ColumnBackend

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Talk title

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

But wait...

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Core Core PROTOCOL read write setup_model

Slide 71

Slide 71 text

storage+access backend

Slide 72

Slide 72 text

storage pattern access pattern backend plugin separation of concerns

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

ColumnBackend FallbacksPlugin read(:'en-CA')

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

shioyama / mobility

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

4LESSONS

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

“include entire frameworks”

Slide 88

Slide 88 text

require "mobility"

Slide 89

Slide 89 text

Core Core require "mobility"

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

It’s the protocols, dummy

Slide 98

Slide 98 text

jeremyevans / roda shrinerb / shrine

Slide 99

Slide 99 text

complexity of protocol reusability of software

Slide 100

Slide 100 text

complexity of protocol reusability of software “maximally general interface”

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

Chris Salzberg @shioyama / [email protected]