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.

E864e5088627498df8f9b911a9bc3219?s=128

Piotr Solnica

June 27, 2014
Tweet

Transcript

  1. HELLO!

  2. I’m Piotr Solnica :)

  3. None
  4. None
  5. Ruby developer github.com/solnic @_solnic_

  6. powow.no gitorious.org evofitness.no

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

  8. Convenience

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

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

  11. Simplicity

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

    or do”
  13. Separation of Concerns

  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])
  15. It’s all so simple, right? :)

  16. None
  17. It is all CONVENIENT (but not simple)

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

  19. • Input data coercion • Setting default values • Input

    data validation • Interaction with the database
  20. • Business logic hidden in life-cycle hooks • Handling nested-data

    structures (via accepts_nested_attributes_for) • Data normalization through custom attribute writers • …
  21. Separation of Concerns

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

    data coercion data validation persistence
  23. def create(params) tuple = params_processor.call(params[:user]) errors = validator.call(tuple) repository.insert(tuple) if

    errors.empty? end
  24. Data & Behavior

  25. # CONVENIENCE ! class User < ActiveRecord::Base ! def full_name

    "#{first_name} #{last_name}" end ! end
  26. # SIMPLICITY ! ! data = { first_name: "Jade", last_name:

    "Doe", email: "jade@doe.org" } class Presenter def initialize(data) @data = data end def full_name "#{@data[:first_name]} #{@data[:last_name]}" end end
  27. Presenter.new(data).full_name => "Jade Doe"

  28. Convenience

  29. Simplicity

  30. Focus on what’s essential

  31. Try to be minimalistic

  32. Immutability

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

  34. Oh You Mutable State!

  35. require "ice_nine" IceNine.deep_freeze(data) presenter = Presenter.new(data) data[:first_name].upcase! # BOOM! RuntimeError:

    can't modify frozen String
  36. Adamantium github.com/dkubb/adamantium

  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
  38. Relational Model

  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”
  40. Structure

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

    ! tasks = Axiom::Relation.new([ [:user_id, Integer], [:title, String] ])
  42. Manipulation

  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"}]
  44. users.join(tasks).insert([[1, "Jade", "Jade's Task"]]).to_a # => [{:user_id=>1, :name=>"Jade", :title=>"Jade's Task"}]

  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
  46. Relations as 1st class citizens

  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
  48. Let’s put relations in front

  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
  50. Composability & Encapsulation

  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 })
  52. Composing queries gives very little confidence

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

  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
  55. Ruby Object Mapper github.com/rom-rb

  56. Relations (aka data) Mapping to objects Simplicity

  57. Logical Schema

  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
  59. Public “external” relations

  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)
  61. Mapping “data” to objects

  62. # basic attribute mapping ! rom.mapping do relation(:users_tasks) do map

    :user_id, :user_name, :title end end
  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
  64. Simplicity!

  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")
  66. Embrace simplicity

  67. Convenience can come later

  68. Thank You! <3

  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
  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/