$30 off During Our Annual Pro Sale. View Details »

Convenience vs Simplicity

Convenience vs Simplicity

Avoiding complexity is one of the greatest goals in programming. The tools we use, libraries and frameworks, must be helping us in achieving that goal. There must be a reasonable balance between convenience and simplicity as growing complexity is the price we pay for that convenience.

This talk is about seeking simplicity when dealing with data and behavior showing alternative approaches to object relational mapping and persistence concerns.

Piotr Solnica

June 27, 2014
Tweet

More Decks by Piotr Solnica

Other Decks in Programming

Transcript

  1. HELLO!

    View Slide

  2. I’m Piotr Solnica :)

    View Slide

  3. View Slide

  4. View Slide

  5. Ruby developer
    github.com/solnic
    @_solnic_

    View Slide

  6. powow.no
    gitorious.org
    evofitness.no

    View Slide

  7. Convenience
    vs
    Simplicity
    Piotr Solnica / RedDotRubyConf / Singapore 2014

    View Slide

  8. Convenience

    View Slide

  9. www.oxforddictionaries.com/definition/english/convenience
    “The state of being able to proceed with
    something without difficulty”

    View Slide

  10. Look at all the things
    I’M NOT doing!

    View Slide

  11. Simplicity

    View Slide

  12. www.oxforddictionaries.com/definition/english/simplicity
    “The quality or condition of being easy to
    understand or do”

    View Slide

  13. Separation
    of
    Concerns

    View Slide

  14. # simple!
    class User < ActiveRecord::Base
    # all the things we’re not doing
    end
    !
    # still very simple!
    user = User.find_by(name: "Jade")
    user.mutate_it_somehow
    user.save
    # again simple!
    User.create(params[:user])

    View Slide

  15. It’s all so simple,
    right?
    :)

    View Slide

  16. View Slide

  17. It is all
    CONVENIENT
    (but not simple)

    View Slide

  18. User.create(params[:user])

    View Slide

  19. • Input data coercion
    • Setting default values
    • Input data validation
    • Interaction with the database

    View Slide

  20. • Business logic hidden in life-cycle hooks
    • Handling nested-data structures (via
    accepts_nested_attributes_for)
    • Data normalization through custom attribute
    writers
    • …

    View Slide

  21. Separation
    of
    Concerns

    View Slide

  22. tuple = params_processor.call(params[:user])
    errors = validator.call(tuple)
    repository.insert(tuple) if errors.empty?
    input data coercion
    data validation
    persistence

    View Slide

  23. def create(params)
    tuple = params_processor.call(params[:user])
    errors = validator.call(tuple)
    repository.insert(tuple) if errors.empty?
    end

    View Slide

  24. Data & Behavior

    View Slide

  25. # CONVENIENCE
    !
    class User < ActiveRecord::Base
    !
    def full_name
    "#{first_name} #{last_name}"
    end
    !
    end

    View Slide

  26. # SIMPLICITY
    !
    !
    data = {
    first_name: "Jade",
    last_name: "Doe",
    email: "[email protected]"
    }
    class Presenter
    def initialize(data)
    @data = data
    end
    def full_name
    "#{@data[:first_name]} #{@data[:last_name]}"
    end
    end

    View Slide

  27. Presenter.new(data).full_name
    => "Jade Doe"

    View Slide

  28. Convenience

    View Slide

  29. Simplicity

    View Slide

  30. Focus on what’s essential

    View Slide

  31. Try to be minimalistic

    View Slide

  32. Immutability

    View Slide

  33. presenter = Presenter.new(data)
    data[:first_name].upcase!
    presenter.full_name
    # JADE Doe

    View Slide

  34. Oh You Mutable State!

    View Slide

  35. require "ice_nine"
    IceNine.deep_freeze(data)
    presenter = Presenter.new(data)
    data[:first_name].upcase!
    # BOOM! RuntimeError: can't modify frozen String

    View Slide

  36. Adamantium
    github.com/dkubb/adamantium

    View Slide

  37. class Money
    include Adamantium
    attr_reader :value, :currency
    def initialize(data)
    @value, @currency = data.values_at(:value, :currency)
    end
    end
    money = Money.new(value: 123.12, currency: "pln")
    money.currency.upcase!
    # BOOM! RuntimeError: can't modify frozen String

    View Slide

  38. Relational Model

    View Slide

  39. “Out of the Tar Pit” Ben Moseley, Peter Marks
    “The relational model has — despite its origins
    — nothing intrinsically to do with databases.
    Rather it is an elegant approach to structuring
    data, a means for manipulating such data, and
    a mechanism for maintaining integrity and
    consistency of state”

    View Slide

  40. Structure

    View Slide

  41. require "axiom"
    users = Axiom::Relation.new([
    [:user_id, Integer],
    [:name, String]
    ])
    !
    tasks = Axiom::Relation.new([
    [:user_id, Integer],
    [:title, String]
    ])

    View Slide

  42. Manipulation

    View Slide

  43. users = users.insert([[1, "Jade"]])
    tasks = tasks.insert([[1, "Task One”]])
    users_tasks = proc { |user_id|
    users.
    join(tasks).
    rename(name: :user_name).
    restrict(user_id: user_id).
    project([:user_name, :title])
    }
    users_tasks.call(1).to_a
    # [{:user_name=>"Jade", :title=>"Task One"}]

    View Slide

  44. users.join(tasks).insert([[1, "Jade", "Jade's Task"]]).to_a
    # => [{:user_id=>1, :name=>"Jade", :title=>"Jade's Task"}]

    View Slide

  45. “Data Independence - a clear separation is
    enforced between the logical data and its
    physical representation”
    “Out of the Tar Pit” Ben Moseley, Peter Marks

    View Slide

  46. Relations as
    1st class citizens

    View Slide

  47. class User < ActiveRecord::Base
    def self.active
    where(active: true)
    end
    end
    # returns a relation with *all the data*
    User.all
    # returns a relation with *a subset* of all the data
    User.active

    View Slide

  48. Let’s put relations in front

    View Slide

  49. class RelationRegistry
    def initialize
    @registry = {}
    # internal base relation, not exposed to the outside world
    @registry[:users] = Axiom::Relation.new(
    [[:name, String], [:active, Boolean]]
    )
    end
    # derived, public relation with active users
    def active_users
    @registry[:users].restrict(active: true)
    end
    end
    relations = RelationRegistry.new
    relations.active_users

    View Slide

  50. Composability
    &
    Encapsulation

    View Slide

  51. # Find all users without tasks
    # we need to resort to a hand-written partial SQL
    User.
    joins(
    "left outer join tasks on tasks.user_id = users.user_id”
    ).
    where(tasks: { id: nil })
    # won't work as it produces an inner join
    User.joins(:tasks).where(:tasks => { :id => nil })

    View Slide

  52. Composing queries
    gives very little confidence

    View Slide

  53. Rather than composing
    queries, let’s compose relations

    View Slide

  54. # Find all users without tasks
    users_with_tasks = users.join(tasks).project([:user_id, :name])
    # difference relational operator
    users_with_no_tasks = users.difference(users_with_tasks)
    # ...or just
    users_with_no_tasks = users - users_with_tasks

    View Slide

  55. Ruby Object Mapper
    github.com/rom-rb

    View Slide

  56. Relations (aka data)
    Mapping to objects
    Simplicity

    View Slide

  57. Logical Schema

    View Slide

  58. rom = ROM::Environment.setup(:memory)
    rom.schema do
    !
    relation(:users, internal: true) do
    attribute :id, Integer, rename: :user_id
    attribute :name, String
    key :user_id
    end
    relation(:tasks) do
    attribute :user_id, Integer
    attribute :title, String
    key :user_id, :title
    end
    !
    end
    that’s a base, internal
    relation

    View Slide

  59. Public “external” relations

    View Slide

  60. rom.schema do
    !
    relation(:users_tasks) do |user_id|
    users.
    join(tasks).
    rename(name: :user_name).
    restrict(user_id: user_id).
    project([:user_id, :user_name, :title])
    end
    !
    end
    # get all user's tasks
    rom.users_tasks(1)

    View Slide

  61. Mapping “data” to objects

    View Slide

  62. # basic attribute mapping
    !
    rom.mapping do
    relation(:users_tasks) do
    map :user_id, :user_name, :title
    end
    end

    View Slide

  63. # mapping embedded values
    !
    rom.mapping do
    relation(:users_tasks) do
    map :title
    wrap :user do
    map :id, from: :user_id
    map :name, from: user_name
    end
    end
    end

    View Slide

  64. Simplicity!

    View Slide

  65. # simple access to all relations defined in the schema
    rom.users_tasks(1).to_a
    # simple access to mapped objects
    rom.load(rom.users_tasks(1))
    # simple inserts using *just data*
    rom.tasks.insert(
    user_id: 1,
    user_name: “Jade",
    title: "Jade's First Task”
    )
    # simple updates using *just data*
    rom.users_tasks(1).
    restrict(id: 1).
    update(title: "Jade's Awesome Task")

    View Slide

  66. Embrace simplicity

    View Slide

  67. Convenience can come later

    View Slide

  68. Thank You!
    <3

    View Slide

  69. Resources
    • Out of the tar pit paper
    • Relations as the first class citizen blog post
    • Ruby Relational Algebra libraries
    • www.try-alf.org
    • github.com/dkubb/axiom
    • Ruby Object Mapper github.com/rom-rb/rom

    View Slide

  70. Photo credits
    • https://www.flickr.com/photos/ntrinkhaus/12748578494
    • https://www.flickr.com/photos/ntrinkhaus/12748578494
    • https://www.flickr.com/photos/scottlaird/31367273
    • https://www.flickr.com/photos/nanpalmero/
    14373506594
    • https://www.flickr.com/photos/georgiesharp/
    8547779958/

    View Slide