Upgrade to Pro — share decks privately, control downloads, hide ads and more …

実践!HanamiでCleanArchitecutre

21d5f61fba6b2d522dadc5888d403a21?s=47 kawada
June 15, 2021

 実践!HanamiでCleanArchitecutre

Fusic Tech Live vol2 「Rubyistの集い」での発表資料です。

21d5f61fba6b2d522dadc5888d403a21?s=128

kawada

June 15, 2021
Tweet

Transcript

  1. 実践! Hanamiで Clean Architecture 2021年6⽉15⽇ 藤澤 亮平 1

  2. ⾃⼰紹介 藤澤 亮平 - ID - Github︓fujisawaryohei - Twitter︓@potaku_dev -

    Work at - 株式会社 Fusic (フュージック) 第⼆技術部⾨ 所属 - ソフトウェアエンジニア - Skill - Rails/Vue.js/Nuxt.js/TypeScript/ AWS SAA - SNS - Github︓fujisawaryohei - Twitter︓@potaku_dev - Qiita︓fujisawaryohei 2
  3. 01 なぜHanamiを学ぶのか

  4. クリーンアーキテクチャの 設計思想や変更に強いコード の書き⽅を学ぶことができる

  5. 02 クリーンアーキテクチャとは

  6. プロダクトのコアロジックやFW, DB, UI等の ソフトウェアを構成する様々な要素間の 関⼼を分離することによってソフトウェアを 構成するFW,DB,UI等の変更から製品のコアロ ジックを守ることを⽬的とするアーキテクチャ

  7. 03 クリーンアーキテクチャの メリット

  8. 1. FW⾮依存・DB⾮依存・UI ⾮依存な設計を⾏う事ができる

  9. 2. 独⽴性が⾼まり テスタビリティが向上する

  10. 04 クリーンアーキテクチャ 概要

  11. タイトル

  12. DIを⽤いてSOLID原則の1つである 依存関係逆転の原則を満たす事ができる。 これによって依存関係の制御を⾏うことができる。 どうやって依存しないようにするの︖︖

  13. Entites

  14. タイトル この部分

  15. エンティティ層は最重要ビジネスデータと最重要ビジネス ルールを持つクラスが存在するレイヤー Entities層とは

  16. 最重要ビジネスデータ: システム化する以前から存在するビジネスデータ 最重要ビジネスルール: システム化する以前から存在するビジネスルール 最重要ビジネスルール, 最重要ビジネスデータとは

  17. Use Cases

  18. タイトル この部分

  19. アプリケーション固有のビジネスルール システム化するに辺り⽣じるルールを記述するレイヤー UseCases層とは

  20. Interface Adpters

  21. タイトル この部分

  22. ユースケースやエンティティに最も便利な形式から、 DBやWebのUIのような外部の機能に 最も便利な形式にデータを変換処理する場所 Controllerとは

  23. Frameworks&Drivesからのデータを抽象化する Gatewayとは

  24. ユースケースから返ってきたデータを適切な形に変 換してViewに渡すロジックが集約する場所 Presenterとは

  25. Interface Adapter層を 要約すると・・

  26. 1. 外側のレイヤーから受け取ったデータを 適切なデータ形式に変換しUseCase層へ渡す処理 2. UseCase層で処理して返ってきたデータを 外側のレイヤーに適切なデータ形式に再度変換して 渡す処理 これらの責務を担うレイヤーとなります。 Interface Adapter層

  27. タイトル つまりこれ

  28. Framework and Drivers

  29. タイトル この部分

  30. 05 Hanami

  31. Clean Architectureの設計思想が反映された Rubyのフレームワーク Hanamiとは

  32. Clean Architecure The main purpose of this architecture is to

    enforce a separation of concerns between the core of our product and the delivery mechanisms. The first is expressed by the set of use cases that our product implements, while the latter are interfaces to make these features available to the outside world. このHanamiのアーキテクチャの目的は、製品のコアロジック の関心とその製品を配信するメカニズムの関心の分離を強化 することです。前者は実装する一連のユースケースによって 表現され、後者はこれらの機能を外部の世界で利用できるよ うにするためのインターフェースです。
  33. ・hanami-model(モデルで使⽤するパッケージ) ・hanami-controller(コントローラーで使⽤するパッケージ) ・hanami-view(ビューで使⽤するパッケージ) ・hanami-validation(バリデーションで使⽤するパッケージ) ・hanami-util(便利なパッケージが⼀括してパッケージングされている) Hanamiパッケージ

  34. Hanamiのアーキテクチャ

  35. ディレクトリ構成 ・controller, Viewは apps/webディレクトリ ・Entity, Repository, Interactor, Presenterは lib/project_nameディレクトリ

  36. 06 Hanamiで実践︕ クリーンアーキテクチャ

  37. 本の登録フォームから「 タイトル 」「 作者 」「単価」 これらの⼊⼒を⾏い登録するユースケース Web上で登録した本の「 タイトル 」「 作者

    」「単価」 に基づいた税込価格の確認ができる 今回のユースケース
  38. # Hanamiのプロジェクト作成 ▶ bundle exec hanami new project_name # hanamiのモデル作成

    ▶ bundle exec hanami g model model_name # controlleとaction⽣成 ▶ bundle exec hanami g action web controller_name#action_name # マイグレーション (HANAMI_ENV=testを先頭につける事でテスト⽤DBを実⾏できます) ▶ bundle exec hanami hanami db prepare # テストコード実⾏ ▶ bundle exec hanami exec rake Hanami コマンド⼀覧
  39. Entity # /lib/bookshelf/entities/book.rb # Table name: books # # id

    :bigint not null, primary key # author :string not null # title :string not null # created_at :datetime not null # updated_at :datetime not null class Book < Hanami::Entity def total_price self.unit_price * 1.08 end end
  40. Repository # /lib/bookshelf/repositories/book_repository.rb class BookRepository < Hanami::Repository # create(data) –

    Create a record for the given data (or entity) # update(id, data) – Update the record corresponding to the given id by setting the given data (or entity) # delete(id) – Delete the record corresponding to the given id # all - Fetch all the entities from the relation # find - Fetch an entity from the relation by primary key # first - Fetch the first entity from the relation # last - Fetch the last entity from the relation # clear - Delete all the records from the relation end
  41. Repository 実装例 # ORMやSQLの使用が可能 class ArticleRepository < Hanami::Repository def most_recent_by_author(author,

    limit: 8) articles.where(author_id: author.id) .order(:published_at) .limit(limit) end end article_repository = ArticleRepository.new article_repository.most_recent_by_author
  42. Repositoryアンチパターン # ダメなケース(例: interactor層でRepositoryを下記のように呼び出す場合) class MostRecentByAuthorGetter attr_reader :repository def call(params)

    article_repository = ArticleRepository.new article_repository.where(author_id: params[:author][:id]) .order(:published_at) .limit(8) end end
  43. タイトル MostRecentBy AuthorGetter ArticleRepository

  44. Repositoryの呼び出し⽅ # 詳細の実装はRepositoryに隠蔽する事で呼び出し元のInteractorに # DIして使用できるようになる。 class MostRecentByAuthorGetter attr_reader :repository def

    initialize(repository) @repository = repository end def call(params) repository.most_recent_by_author(params[:id]) end End article_repository = ArticleRepository.new MostRecentByAuthorGetter.new(article_repository).call(params[:id])
  45. None
  46. Interactor # /lib/bookshelf/interactors/book_interactors/add_book.rb require 'hanami/interactor' module BookInteractor class AddBook include

    Hanami::Interactor attr_reader :params attr_reader :repository expose :book def initialize(repository = BookRepository.new) @repository = repository end def call(params) @book = repository.create(Book.new(params)) end private # 参考: https://www.rubydoc.info/gems/hanami-utils/Hanami/Interactor/LegacyInterface # valid?はHanami::Interactorをincludeするとcall呼び出し前に実行するフックメソッド # valid? を実行する # valid? の戻り値が真値であれば、クラスに定義された #call を実行する # valid? の戻り地が偽値であれば、何もしない # Hanami::Interactor::ResultクラスはExposeで指定したキーをインスタンス変数とGetterとして持つ # callでHanami::Interactor::Result のインスタンスを返す def valid?(params) validate_result = Validations::Create.new(params).validate if validate_result.failure? error(validate_result.messages) end validate_result.success? end end end
  47. Interactor # /lib/bookshelf/interactors/book_interactors/add_book.rb require 'hanami/interactor' module BookInteractor class AddBook include

    Hanami::Interactor attr_reader :params attr_reader :repository expose :book def initialize(repository = BookRepository.new) @repository = repository end def call(params) @book = repository.create(Book.new(params)) end private # 参考: https://www.rubydoc.info/gems/hanami-utils/Hanami/Interactor/LegacyInterface # valid?はHanami::Interactorをincludeするとcall呼び出し前に実行するフックメソッド # valid? を実行する # valid? の戻り値が真値であれば、クラスに定義された #call を実行する # valid? の戻り地が偽値であれば、何もしない # Hanami::Interactor::ResultクラスはExposeで指定したキーをインスタンス変数とGetterとして持つ # callでHanami::Interactor::Result のインスタンスを返す def valid?(params) validate_result = Validations::Create.new(params).validate if validate_result.failure? error(validate_result.messages) end validate_result.success? end end end
  48. Interactor バリデーション成功時 # /lib/bookshelf/interactors/book_interactors/add_book.rb require 'hanami/interactor' module BookInteractor class AddBook

    include Hanami::Interactor attr_reader :params attr_reader :repository expose :book def initialize(repository = BookRepository.new) @repository = repository end def call(params) @book = repository.create(Book.new(params)) end private # 参考: https://www.rubydoc.info/gems/hanami-utils/Hanami/Interactor/LegacyInterface # valid?はHanami::Interactorをincludeするとcall呼び出し前に実行するフックメソッド # valid? を実行する # valid? の戻り値が真値であれば、クラスに定義された #call を実行する # valid? の戻り地が偽値であれば、何もしない # Hanami::Interactor::ResultクラスはExposeで指定したキーをインスタンス変数とGetterとして持つ # callでHanami::Interactor::Result のインスタンスを返す def valid?(params) validate_result = Validations::Create.new(params).validate if validate_result.failure? error(validate_result.messages) end validate_result.success? end end end
  49. Interactor バリデーション成功時 # /lib/bookshelf/interactors/book_interactors/add_book.rb require 'hanami/interactor' module BookInteractor class AddBook

    include Hanami::Interactor attr_reader :params attr_reader :repository expose :book def initialize(repository = BookRepository.new) @repository = repository end def call(params) @book = repository.create(Book.new(params)) end private # 参考: https://www.rubydoc.info/gems/hanami-utils/Hanami/Interactor/LegacyInterface # valid?はHanami::Interactorをincludeするとcall呼び出し前に実行するフックメソッド # valid? を実行する # valid? の戻り値が真値であれば、クラスに定義された #call を実行する # valid? の戻り地が偽値であれば、何もしない # Hanami::Interactor::ResultクラスはExposeで指定したキーをインスタンス変数とGetterとして持つ # callでHanami::Interactor::Result のインスタンスを返す def valid?(params) validate_result = Validations::Create.new(params).validate if validate_result.failure? error(validate_result.messages) end validate_result.success? end end end
  50. Interactor バリデーション失敗時 # /lib/bookshelf/interactors/book_interactors/add_book.rb require 'hanami/interactor' module BookInteractor class AddBook

    include Hanami::Interactor attr_reader :params attr_reader :repository expose :book def initialize(repository = BookRepository.new) @repository = repository end def call(params) @book = repository.create(Book.new(params)) end private # 参考: https://www.rubydoc.info/gems/hanami-utils/Hanami/Interactor/LegacyInterface # valid?はHanami::Interactorをincludeするとcall呼び出し前に実行するフックメソッド # valid? を実行する # valid? の戻り値が真値であれば、クラスに定義された #call を実行する # valid? の戻り地が偽値であれば、何もしない # Hanami::Interactor::ResultクラスはExposeで指定したキーをインスタンス変数とGetterとして持つ # callでHanami::Interactor::Result のインスタンスを返す def valid?(params) validate_result = Validations::Create.new(params).validate if validate_result.failure? error(validate_result.messages) end validate_result.success? end end end
  51. Validation # /lib/bookshelf/interactors/book_interactor/validation/create.rb require 'hanami/validations' module BookInteractor::Validations class Create include

    Hanami::Validations validations do required(:title).filled(:str?) required(:author).filled(:str?) required(:unit_price).filled(:int?) end end end
  52. Interactorのテスト # /lib/bookshelf/interactors/book_interactors/add_book.rb RSpec.describe BookInteractor::AddBook, type: :interactor do let(:book) {

    Book.new(params) } let(:repository) { double('BookRepository', create: book)} let(:interactor) { described_class.new(repository) } context 'with valid params' do let(:params) { Hash[ { title: 'TDD', author: 'Kent Beck', unit_price: 3500 } ]} it 'create a book' do interactor_result = interactor.call(params) expect(interactor_result.book.title).to eq params[:title] expect(interactor_result.book.author).to eq params[:author] expect(interactor_result.book.unit_price).to eq params[:unit_price] end # 永続化しているかどうかのテストはBookRepositoryが # createメソッドを呼んでいるかどうかをテストする context 'persistence' do # instance_double は 未定義のインスタンスメソッドをスタブした際にエラーを吐いてくれる let(:repository) { instance_double('BookRepository') } it 'persist a book' do expect(repository).to receive(:create) BookInteractor::AddBook.new(repository).call(params) end end end context 'with invalid params' do let(:params) { Hash[title: '', author: '', unit_price: -1]} it 'create a book' do interactor_result = interactor.call(params) expect(interactor_result.errors.first).not_to be_nil expect(interactor_result.errors.first[:title]).to eq ["must be filled"] expect(interactor_result.errors.first[:author]).to eq ["must be filled"] end end end
  53. Controller # /apps/web/controllers/books/create.rb module Web::Controllers::Books class Create include Web::Action attr_reader

    :interactor before { |params| cast_unit_price(params) } expose :book, :errors def initialize(interactor = BookInteractor::AddBook.new) @interactor = interactor end def call(params) interactor_result = interactor.call(params[:book]) if interactor_result.success? @book = interactor_result.book redirect_to '/books’ else @errors = interactor_result.error_messages self.status = 422 end end private def cast_unit_price(params) return unless params[:book].key?(:unit_price) unit_price = params[:book][:unit_price] params[:book][:unit_price] = unit_price.to_i unless unit_price.empty? end end end
  54. Controller # /apps/web/controllers/books/create.rb module Web::Controllers::Books class Create include Web::Action attr_reader

    :interactor before { |params| cast_unit_price(params) } expose :book, :errors def initialize(interactor = BookInteractor::AddBook.new) @interactor = interactor end def call(params) interactor_result = interactor.call(params[:book]) if interactor_result.success? @book = interactor_result.book redirect_to '/books’ else @errors = interactor_result.error_messages self.status = 422 end end private def cast_unit_price(params) return unless params[:book].key?(:unit_price) unit_price = params[:book][:unit_price] params[:book][:unit_price] = unit_price.to_i unless unit_price.empty? end end end
  55. Controller # /apps/web/controllers/books/create.rb module Web::Controllers::Books class Create include Web::Action attr_reader

    :interactor before { |params| cast_unit_price(params) } expose :book, :errors def initialize(interactor = BookInteractor::AddBook.new) @interactor = interactor end def call(params) interactor_result = interactor.call(params[:book]) if interactor_result.success? @book = interactor_result.book redirect_to '/books’ else @errors = interactor_result.error_messages self.status = 422 end end private def cast_unit_price(params) return unless params[:book].key?(:unit_price) unit_price = params[:book][:unit_price] params[:book][:unit_price] = unit_price.to_i unless unit_price.empty? end end end
  56. Controller # /apps/web/controllers/books/create.rb module Web::Controllers::Books class Create include Web::Action attr_reader

    :interactor before { |params| cast_unit_price(params) } expose :book, :errors def initialize(interactor = BookInteractor::AddBook.new) @interactor = interactor end def call(params) interactor_result = interactor.call(params[:book]) if interactor_result.success? @book = interactor_result.book redirect_to '/books’ else @errors = interactor_result.error_messages self.status = 422 end end private def cast_unit_price(params) return unless params[:book].key?(:unit_price) unit_price = params[:book][:unit_price] params[:book][:unit_price] = unit_price.to_i unless unit_price.empty? end end end
  57. Controller # /apps/web/controllers/books/create.rb module Web::Controllers::Books class Create include Web::Action attr_reader

    :interactor before { |params| cast_unit_price(params) } expose :book, :errors def initialize(interactor = BookInteractor::AddBook.new) @interactor = interactor end def call(params) interactor_result = interactor.call(params[:book]) if interactor_result.success? @book = interactor_result.book redirect_to '/books’ else @errors = interactor_result.error_messages self.status = 422 end end private def cast_unit_price(params) return unless params[:book].key?(:unit_price) unit_price = params[:book][:unit_price] params[:book][:unit_price] = unit_price.to_i unless unit_price.empty? end end end
  58. Controllerのテスト # /spec/web/controllers/books/crete_spec.rb RSpec.describe Web::Controllers::Books::Create, type: :action do let(:book) {

    Book.new(title: 'Confident Ruby', author: 'Avdi Grimm', unit_price: '3500') } let(:repository) { double('BookRepository', create: book) } let(:interactor) { BookInteractor::AddBook.new(repository) } # スタブしたInteractorをControllerにDI let(:action) { described_class.new(interactor) } context 'with valid params' do let(:params) { Hash[book: { title: 'Confident Ruby', author: 'Avdi Grimm', unit_price: '3500' }] } it 'create a book' do response = action.call(params) expect(action.book.title).to eq params[:book][:title] expect(action.book.author).to eq params[:book][:author] expect(action.book.unit_price).to eq params[:book][:unit_price].to_i expect(action.exposures[:book]).to eq Book.new(params[:book]) end it 'redirect the user to the books listing' do response = action.call(params) expect(response[0]).to eq 302 expect(response[1]['Location']).to eq('/books’) end end context 'with invalid params' do let(:params){ Hash[book:{ hoge: 'fuga' }] } it 'returns HTTP Client error' do response = action.call(params) expect(response[0]).to eq(422) expect(action.errors).not_to be_nil end end end
  59. Controllerのテスト # /spec/web/controllers/books/crete_spec.rb RSpec.describe Web::Controllers::Books::Create, type: :action do let(:book) {

    Book.new(title: 'Confident Ruby', author: 'Avdi Grimm', unit_price: '3500') } let(:repository) { double('BookRepository', create: book) } let(:interactor) { BookInteractor::AddBook.new(repository) } let(:action) { described_class.new(interactor) } context 'with valid params' do let(:params) { Hash[book: { title: 'Confident Ruby', author: 'Avdi Grimm', unit_price: '3500' }] } it 'create a book' do response = action.call(params) expect(action.book.title).to eq params[:book][:title] expect(action.book.author).to eq params[:book][:author] expect(action.book.unit_price).to eq params[:book][:unit_price].to_i expect(action.exposures[:book]).to eq Book.new(params[:book]) end it 'redirect the user to the books listing' do response = action.call(params) expect(response[0]).to eq 302 expect(response[1]['Location']).to eq('/books’) end end context 'with invalid params' do let(:params){ Hash[book:{ hoge: 'fuga' }] } it 'returns HTTP Client error' do response = action.call(params) expect(response[0]).to eq(422) expect(action.errors).not_to be_nil end end end
  60. ViewとTemplate require 'hanami/view' module Articles class Index include Hanami::View end

    class AtomIndex < Index format :atom end end Hanami::View.configure do root 'app/templates' end Hanami::View.load! articles = ArticleRepository.new.all Articles::Index.render(format: :html, articles: articles) # => This will use Articles::Index # and "articles/index.html.erb" Articles::Index.render(format: :atom, articles: articles) # => This will use Articles::AtomIndex # and "articles/index.atom.erb" Articles::Index.render(format: :xml, articles: articles) # => This will raise a Hanami::View::MissingTemplateError
  61. ViewとTemplate(登録失敗時: 登録フォーム画⾯) # apps/web/template/books/new.html.erb <h2>Add book</h2> <% unless error_messages.empty? %>

    <div class="errors"> <h3>There was a problem with your submission</h3> <ul> <% error_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <%= form_for :book, '/books' do div class: 'input' do label :title text_field :title end div class: 'input' do label :author text_field :author end div class: 'input' do label :unit_price number_field :unit_price end div class: 'controls' do submit 'Create Book’ end end %> # apps/web/views/books/create.rb module Web::Views::Books class Create include Web::View template 'books/new’ def error_messages error_messages = [] errors.each do |error| error_messages = error.map{|k, v| error_format(k, v) } end error_messages end private def error_format(key, values) "#{key} #{values.join(",")}" end end end
  62. ViewとTemplate(登録失敗時: 登録フォーム画⾯) # apps/web/template/books/new.html.erb <h2>Add book</h2> <% unless error_messages.empty? %>

    <div class="errors"> <h3>There was a problem with your submission</h3> <ul> <% error_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <%= form_for :book, '/books' do div class: 'input' do label :title text_field :title end div class: 'input' do label :author text_field :author end div class: 'input' do label :unit_price number_field :unit_price end div class: 'controls' do submit 'Create Book’ end end %> # apps/web/views/books/create.rb module Web::Views::Books class Create include Web::View template 'books/new’ def error_messages error_messages = [] errors.each do |error| error_messages = error.map{|k, v| error_format(k, v) } end error_messages end private def error_format(key, values) "#{key} #{values.join(",")}" end end end
  63. ViewとTemplate(登録失敗時: 登録フォーム画⾯) # apps/web/template/books/new.html.erb <h2>Add book</h2> <% unless error_messages.empty? %>

    <div class="errors"> <h3>There was a problem with your submission</h3> <ul> <% error_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <%= form_for :book, '/books' do div class: 'input' do label :title text_field :title end div class: 'input' do label :author text_field :author end div class: 'input' do label :unit_price number_field :unit_price end div class: 'controls' do submit 'Create Book’ end end %> # apps/web/views/books/create.rb module Web::Views::Books class Create include Web::View template 'books/new’ def error_messages error_messages = [] errors.each do |error| error_messages = error.map{|k, v| error_format(k, v) } end error_messages end private def error_format(key, values) "#{key} #{values.join(",")}" end end end
  64. ViewとTemplate(登録成功時: 本⼀覧画⾯) # apps/web/template/books/index.html.erb <h2>All books</h2> <% if books.any? %>

    <div id="books"> <% books.each do |book_presenter| %> <div class="book"> <h2><%= book_presenter.title %></h2> <p><%= book_presenter.author %></p> <p><%= book_presenter.total_price %></p> </div> <% end %> </div> <% else %> <p class="placeholder">There are no books yet.</p> <% end %> <a href="/books/new">New book</a> # apps/web/views/books/index.rb module Web::Views::Books class Index include Web::View # SystemStackError (stack level too deep). # def books # books.map{|book| BookPresenter.new(book) } # end def books locals[:books].map{|book| BookPresenter.new(book) } end end end
  65. ViewとTemplate(登録成功時: 本⼀覧画⾯) # apps/web/template/books/index.html.erb <h2>All books</h2> <% if books.any? %>

    <div id="books"> <% books.each do |book_presenter| %> <div class="book"> <h2><%= book_presenter.title %></h2> <p><%= book_presenter.author %></p> <p><%= book_presenter.total_price %></p> </div> <% end %> </div> <% else %> <p class="placeholder">There are no books yet.</p> <% end %> <a href="/books/new">New book</a> # lib/bookshelf/presenters /book_presenter.rb require 'hanami/view’ class BookPresenter include Hanami::Presenter attr_reader :book def initialize(book) @book = book end def title book.title end def author book.author end def total_price "税込価格: #{book.total_price.round}円" end end
  66. 07 Hanamiを 使⽤してみての感想

  67. 感想1 Hanamiは各レイヤーそれぞれにmoduleが用意されており、 このModuleが提供してくれるインターフェースはイニシ ャライザとCallの2つというシンプルさとシンプルさ故の カスタマイズ性の高さ、そしてカスタマイズ性が高いが故 に開発者の設計思想をしっかり反映できるのがメリットだ と感じた

  68. Callの⼀択である事で感じたメリット CallメソッドのみがPublicに呼び出せる共通の インターフェースであるため、DIのみを意識しておけば抽 象化も行いやすい (RepositoryとInteractorについては工夫が必要そう)

  69. 参考⽂献 ・ Clean Architecture 達人に学ぶソフトウェアの構造と設計 https://www.amazon.co.jp/dp/B07FSBHS2V/ref=dp-kindle- redirect?_encoding=UTF8&btkr=1 ・HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか https://magazine.rubyist.net/articles/0056/0056-hanami.html ・Laravelで実践クリーンアーキテクチャ

    https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22 ・クリーンアーキテクチャ完全に理解した https://gist.github.com/mpppk/609d592f25cab9312654b39f1b357c60
  70. ご清聴いただきありがとうございました Thank You We are Hiring ! https://recruit.fusic.co.jp/