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

[KRUG] Architecture. The reclaimed years.

[KRUG] Architecture. The reclaimed years.

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

Piotr Solnica

March 20, 2018
Tweet

More Decks by Piotr Solnica

Other Decks in Programming

Transcript

  1. ARCHITECTURE
    THE RECLAIMED YEARS
    1

    View Slide

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

    View Slide

  3. ARCHITECTURE
    THE LOST YEARS
    3

    View Slide

  4. "The web is a delivery mechanism,
    the web is a detail"
    — Uncle Bob
    4

    View Slide

  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

    View Slide

  6. "It’s good for DHH, not so good for
    you"
    — Uncle Bob
    6

    View Slide

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

    View Slide

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

    View Slide

  9. FAST TESTS
    9

    View Slide

  10. > Boundaries
    > Data structures
    > Dependencies
    10

    View Slide

  11. BOUNDARIES
    11

    View Slide

  12. 12

    View Slide

  13. 13

    View Slide

  14. DATA STRUCTURES
    14

    View Slide

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

    View Slide

  16. DEPENDENCIES
    16

    View Slide

  17. 17

    View Slide

  18. 18

    View Slide

  19. class CreateUser
    end
    19

    View Slide

  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

    View Slide

  21. > Classes
    > Modules
    > Singleton methods
    21

    View Slide

  22. PROBLEM WITH CLASSES
    22

    View Slide

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

    View Slide

  24. 24

    View Slide

  25. > Minimize state in classes
    > Don't rely on class state at runtime
    > Don't rely on monkey-patching
    25

    View Slide

  26. PROBLEM WITH MODULES
    26

    View Slide

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

    View Slide

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

    View Slide

  29. FAVOR COMPOSITION OVER INHERITANCE
    29

    View Slide

  30. SINGLETON METHODS
    30

    View Slide

  31. > Using singleton methods couple your code to class/
    module constants
    > Singleton methods easily lead to awkward, procedural
    code
    31

    View Slide

  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

    View Slide

  33. DRY-SYSTEM
    33

    View Slide

  34. > An architecture for Ruby applications
    > Based heavily on lightweight dependency injection
    > Allows you to compose an application from isolated
    components
    34

    View Slide

  35. 35

    View Slide

  36. RUBY APPLICATION COMES FIRST
    36

    View Slide

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

    View Slide

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

    View Slide

  39. app
    |-lib
    |- users/create_user.rb
    |- repos/user_repo.rb
    |-system
    |-spec
    39

    View Slide

  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

    View Slide

  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

    View Slide

  42. YOUR APP IS THE ENTRY POINT TO YOUR SYSTEM
    ∞ pry -r ./system/app
    [1] pry(main)> App['users.create_user']
    => #
    [2] pry(main)> App['repos.user_repo']
    => #
    42

    View Slide

  43. 43

    View Slide

  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

    View Slide

  45. USER INTERFACE AS AN EXTENSION
    45

    View Slide

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

    View Slide

  47. LET'S ADD A WEB INTERFACE ON TOP USING RODA
    47

    View Slide

  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

    View Slide

  49. ∞ curl -X POST http://localhost:9292/users -d "user[id]=1&user[name]=Jane"
    {"id":"1","name":"Jane"}
    49

    View Slide

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

    View Slide

  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

    View Slide

  52. ∞ bin/app create_user '{"id":1,"name":"Jane"}'
    Created {"id"=>1, "name"=>"Jane"}
    52

    View Slide

  53. DID YOU NOTICE THE BOUNDARIES HERE?
    53

    View Slide

  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

    View Slide

  55. THIS IS NOT A CONCERN OF YOUR
    APPLICATION
    55

    View Slide

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

    View Slide

  57. # user creation end-point
    App['users.create_user']
    # expected data structure schema
    { id: Integer, name: String }
    57

    View Slide

  58. 58

    View Slide

  59. CLEAN ARCHITECTURE
    > Ruby app comes first
    > Respecting boundaries
    > Object composition
    > User interface as an extension of your Ruby app
    59

    View Slide

  60. THANK YOU
    60

    View Slide

  61. MORE THINGS TO CHECK OUT
    > Boundaries talk by Gary Bernhardt
    > dry-system on GitHub
    > sample app from slides on GitHub
    61

    View Slide