[KRUG] Architecture. The reclaimed years.

[KRUG] Architecture. The reclaimed years.

A short intro into clean architecture for Ruby apps based on dry-system gem.

E864e5088627498df8f9b911a9bc3219?s=128

Piotr Solnica

March 20, 2018
Tweet

Transcript

  1. ARCHITECTURE THE RECLAIMED YEARS 1

  2. PIOTR SOLNICA > rom-rb creator > dry-rb co-founder > github.com/solnic

    > @_solnic_ > solnic.eu 2
  3. ARCHITECTURE THE LOST YEARS 3

  4. "The web is a delivery mechanism, the web is a

    detail" — Uncle Bob 4
  5. "The top level architecture of my rails application, did not

    scream its intent at you, it screamed the framework at you, it screamed Rails at you" — Uncle Bob 5
  6. "It’s good for DHH, not so good for you" —

    Uncle Bob 6
  7. "Database is a detail" — Uncle Bob 7

  8. RAILS IS YOUR ARCHITECTURE EMBRACE IT OR LEAVE IT 8

  9. FAST TESTS 9

  10. > Boundaries > Data structures > Dependencies 10

  11. BOUNDARIES 11

  12. 12

  13. 13

  14. DATA STRUCTURES 14

  15. > HTTP Request > Application response > View-specific data >

    Domain-specific data 15
  16. DEPENDENCIES 16

  17. 17

  18. 18

  19. class CreateUser end 19

  20. class CreateUser attr_reader :user_repo, :validator, :mailer def initialize(user_repo:, validator:, mailer:)

    @user_repo = user_repo @validator = validator @mailer = mailer end end 20
  21. > Classes > Modules > Singleton methods 21

  22. PROBLEM WITH CLASSES 22

  23. CLASSES IN RUBY ARE GLOBAL, STATEFUL, MUTABLE VARIABLES 23

  24. 24

  25. > Minimize state in classes > Don't rely on class

    state at runtime > Don't rely on monkey-patching 25
  26. PROBLEM WITH MODULES 26

  27. MODULES IN RUBY IS A FORM OF MULTIPLE INHERITANCE 27

  28. IT'S HARD TO ACHIEVE A COHERENT SYSTEM WHEN MODULES ARE

    USED EXTENSIVELY 28
  29. FAVOR COMPOSITION OVER INHERITANCE 29

  30. SINGLETON METHODS 30

  31. > Using singleton methods couple your code to class/ module

    constants > Singleton methods easily lead to awkward, procedural code 31
  32. > Minimize usage of singleton methods, they are only good

    as "builder" methods, or top-level configuration APIs > Don't use them at runtime, objects are 10 x better and more flexible 32
  33. DRY-SYSTEM 33

  34. > An architecture for Ruby applications > Based heavily on

    lightweight dependency injection > Allows you to compose an application from isolated components 34
  35. 35

  36. RUBY APPLICATION COMES FIRST 36

  37. app |-lib |-system |-spec 37

  38. app |-lib |-system |- app.rb <= your app |- import.rb

    <= DI extension |-spec 38
  39. app |-lib |- users/create_user.rb |- repos/user_repo.rb |-system |-spec 39

  40. # app/system/app.rb require 'dry/system/container' class App < Dry::System::Container configure do

    |config| config.auto_register = %w(lib) end load_paths! 'lib', 'system' end 40
  41. SIMPLE OBJECT COMPOSITION require 'import' module Users class CreateUser include

    Import['repos.user_repo'] def call(params) user_repo.create(params) end end end 41
  42. YOUR APP IS THE ENTRY POINT TO YOUR SYSTEM ∞

    pry -r ./system/app [1] pry(main)> App['users.create_user'] => #<Users::CreateUser:0x00007ff3a1b2e520..> [2] pry(main)> App['repos.user_repo'] => #<Repos::UserRepo:0x00007ff3a11b0890..> 42
  43. 43

  44. TESTING IN ISOLATION require 'users/create_user' RSpec.describe Users::CreateUser do subject(:create_user) do

    Users::CreateUser.new end describe '#call' do it 'returns created user' do user = create_user.call(id: 1, name: 'Jane') expect(user).to eql(id: 1, name: 'Jane') end end end 44
  45. USER INTERFACE AS AN EXTENSION 45

  46. > Web UI based on HTML/CSS/JS > JSON API >

    CLI interface > ... 46
  47. LET'S ADD A WEB INTERFACE ON TOP USING RODA 47

  48. # system/web.rb require_relative 'app' require 'roda' class Web < Roda

    opts[:api] = App plugin :json route do |r| r.post 'users' do api['users.create_user'].call(r[:user]) end end def api self.class.opts[:api] end end 48
  49. ∞ curl -X POST http://localhost:9292/users -d "user[id]=1&user[name]=Jane" {"id":"1","name":"Jane"} 49

  50. LET'S ADD A CLI ON TOP USING HANAMI-CLI 50

  51. #!/usr/bin/env ruby require "bundler/setup" require "hanami/cli" require "json" require_relative '../system/boot'

    module Commands extend Hanami::CLI::Registry class CreateUser < Command desc "Creates a user" argument :user, desc: "User data" def call(user: nil, **) params = JSON.parse(user) output = App['users.create_user'].call(params) puts "Created #{output.inspect}" end end register "create_user", CreateUser end Hanami::CLI.new(Commands).call 51
  52. ∞ bin/app create_user '{"id":1,"name":"Jane"}' Created {"id"=>1, "name"=>"Jane"} 52

  53. DID YOU NOTICE THE BOUNDARIES HERE? 53

  54. WEB INPUT AS PRE-PROCESSED RACK PARAMS r[:user] # { "id"

    => 1, "name" => "Jane" } CLI INPUT AS A PLAIN JSON STRING '{"id":1,"name":"Jane"}' 54
  55. THIS IS NOT A CONCERN OF YOUR APPLICATION 55

  56. YOUR APPLICATION IS AN API WITH OBJECTS ACCEPTING SPECIFIC DATA

    STRUCTURES AS INPUT 56
  57. # user creation end-point App['users.create_user'] # expected data structure schema

    { id: Integer, name: String } 57
  58. 58

  59. CLEAN ARCHITECTURE > Ruby app comes first > Respecting boundaries

    > Object composition > User interface as an extension of your Ruby app 59
  60. THANK YOU 60

  61. MORE THINGS TO CHECK OUT > Boundaries talk by Gary

    Bernhardt > dry-system on GitHub > sample app from slides on GitHub 61