プログラミング

CircleCIでcheckoutするならGit入りイメージを使おう

CircleCIのジョブが下記のエラーで失敗していた。

Either git or ssh (required by git to clone through SSH) is not installed in the image. Falling back to CircleCI's native git client but the behavior may be different from official git. If this is an issue, please use an image that has official git and ssh installed.

Enumerating objects: 208, done.
Counting objects: 100% (208/208), done.
Compressing objects: 100% (135/135), done.
Total 1015 (delta 130), reused 134 (delta 61), pack-reused 807

reference not found

git cloneで「reference not found」になっている。イメージにGitクライアントがインストールされておらず、CircleCIネイティブのGitクライアントにフォールバックしたものの、おそらくその実装に問題があるためエラーになったのだろう。

エラーメッセージにあるとおり、公式のGitが入ったイメージを使えば解決しそうだ。

これまでは下記のように「docker」イメージを指定していた。

    docker:
      - image: docker

これを、Git入りのイメージに変えたところ、ジョブが成功した。

    docker:
      - image: docker:git

今まで失敗しなかったのは、たまたま運がよかったのだろう。

Intent-ResponseパターンでFat Controllerをリファクタリングしてみる

Fat Controllerをコードで防ぐ方法はないか?」という記事で、まさにFat Controllerを防ぐ私案として、Intent-Responseパターンを提示した。分かりづらいと思うので、もう少し説明してみる。

Fat Controlerの実例

ぼくが思うFat Controlerは、こういうものだ。

  def diff
    journal = Journal::AggregatedJournal.containing_journal(@journal)
    field = params[:field].parameterize.underscore.to_sym

    unless valid_diff?
      return render_404
    end

    unless journal.details[field].is_a?(Array)
      return render_400 message: I18n.t(:error_journal_attribute_not_present, attribute: field)
    end

    from = journal.details[field][0]
    to = journal.details[field][1]

    @diff = Redmine::Helpers::Diff.new(to, from)
    @journable = journal.journable
    respond_to do |format|
      format.html
      format.js do
        render partial: 'diff', locals: { diff: @diff }
      end
    end
  end

OpenProjectというRails製のOSSに含まれているコードである。JournalsControllerのアクションだ。

それほどFatに見えないかもしれないが、ビジネスロジックもプレゼンテーションロジックも含まれていて、コントローラが責務を担いすぎているようにぼくには思える。

たとえば、@journable の扱いがおかしい。JS用のテンプレートでは @journable を参照しない。だから、下記のように、HTMLが要求されたときだけ設定されるべきだ。

    respond_to do |format|
      format.html do
        @journable = journal.journable
        render
      end
      format.js do
        render partial: 'diff', locals: { diff: @diff }
      end
    end

このような混乱が生じるのは、コントローラがプレゼンテーションロジックまで担ってしまっているからだ。

どうしたら防げるだろうか。

Intent

まず、ビジネスロジックについて考えてみよう。

JournalsController#diff のビジネスロジックは「diffの取得」である。HTMLが要求されたときでもJSが要求されたときでもレスポンスに含まれるのはdiffだけだ。

こうしたビジネスロジックを「Intent」と呼ぶことにする。JournalsController#diff にアクセスするユーザの意図、すなわちIntentは「diffの取得」ということである。

Response

一方、プレゼンテーションロジックについてはどうか。

JournalsController#diff のレスポンスは、表現形式(HTML or JS)を無視すれば、3種類考えられる。ステータスコードが404、400、200の場合に対応するものだ。

3種類のレスポンスに必要なロジックはそれぞれ異なる。404の場合は固定のレスポンスボディを返すだろうし、200の場合は表現形式に応じたレスポンスボディを組み立てることになるだろう。

こうしたレスポンスは、そのまま「Response」と呼ぶことにする。Viewと呼ばないのは、たとえば「204 No Content」のようにレスポンスボディのない応答もありえるからだ。Responseの呼ぶのがふさわしいだろう。

Fat Controlerのリファクタリング

ここまででビジネスロジックをIntentに、プレゼンテーションロジックをResponseに、それぞれ任せる方針が見えた。個々のIntentやResponseはオブジェクトとして扱うのが自然だろう。

となると、JournalsController#diff は下記のようにリファクタリングするのがよさそうだ。

  def diff
    status_code, outcome = Journal::Intent::Diff.new.execute(self)
    responses = {
      404: Journal::Response::FieldNotFound,
      400: Journal::Response::AttributeNotPresent,
      200: Journal::Response::Diff
    }
    responses[status_code].new.build(self, status_code, outcome)
  end

こうすれば、コントローラはIntentの実行と、実行結果に応じたResponseの組み立てを制御するだけでよくなる。outcomeはIntentの結果を意味し、200時にはdiffが、400時にはエラーメッセージが含まれる。

(IntentやResponseにコントローラ自身を渡しているのは、渡した先でparamsやrenderなどを呼び出すと想定したからだ。他に良い手があるかもしれない)

Intent-Responseパターンのメリット

Intent-Responseパターンが絶対の正解だとは言わない。が、コントローラの可読性の低さに起因するエンバグは減るだろう。それだけでも、ぼくにはメリットが大きい。

プログラマ人生後半戦のテーマ

最近考えているのは、Webアプリケーションフレームワークとの付き合い方とか、移植性の高いWebアプリのアーキテクチャパターンとか、そんなことである。先日公開した flask-skinny はその一環だ。定年まで20年を切ったプログラマ人生の後半戦、まずはこのテーマに自分なりの結論を出したいと思っている。

なぜそんなことを考えているかといえば、これまで勤めてきたいくつかの企業で、フレームワークのアップグレードに追従できなくなっているアプリを見てきたからである。2015年に入社した企業ではRails 2が使われていた。Rails 4がリリースされたのは2013年である。テストコードはなかったから、真面目にアップグレードするとすれば、まずテストコードを書き、Rails 3への移行を試すことになる。当時は他のタスクに追われ、そんな余裕はなかった。2020年の今でも、似たような状況に陥っているWebプログラマは少なくないのではないか。

もういい加減、そんなWebアプリ、そんな負債は生み出したくない。フレームワークのアップグレードに追従しやすい、もしくは別のフレームワークに乗り換えやすい、あるいは他のプログラミング言語に移植しやすい、そういうWebアプリが書きたい。

では、そのためにはどうしたらよいのか。それをプログラマ人生の後半戦の最初のテーマに据えようというわけである。


今のところ、なんとなく考えているのは以下のようなことだ。

  • フルスタックフレームワークは避ける
    • いずれ心中することになるから
  • ルーティング処理は自前で書くか、フレームワークに任せる
    • ルートをYAMLなりJSONなりで定義し、自前で処理するのが理想的
    • とはいえ、移植時にルート定義を書き直すのはそこまで負担にはならなさそう
  • リクエストやレスポンスの抽象化はプログラミング言語かフレームワークに任せる
    • WSGIやらRackやらは抽象度が低すぎ、そのまま扱うのは厳しい
    • とはいえ、抽象化処理を自前で書くのは車輪の再発明すぎる
    • Goの net/http がよさそうに見える
  • データアクセスレイヤへの依存性をビジネスロジックに注入するのはコントローラの役目
    • そこまでしてくれるフレームワークはないから
  • ビューテンプレートには Mustache を使うHTTPレイヤのテストを書く
    • いずれも、他の言語に移植しやすくなるから
  • Intent-Responseパターンを活用する
    • flask-skinnyで提案したアーキテクチャパターン。詳細は近日公開予定の別記事で

以上のようにまとめてみると、Goなら、超うすうすフレームワークを自前で書けば、移植性の高いWebアプリが書けそうな気がしてきた。フレームワークに必要な機能は下記のみ。

  1. JSONなりYAMLなりでルートが定義できるルーティング機能
  2. 外部への依存性(データアクセス、テンプレートエンジン、etc.)をWebアプリに注入できるDI機能
  3. Intent-ResponseパターンでWebアプリを呼び出す機能

1と2は、いい感じのライブラリがすでにあるかもしれない。3は、flask-skinnyで実現したとおりで、さほど難しいものではない。

というわけで、直近のタスクは上記フレームワークを作ることとなった。Go初学者なので、焦らず進めていく。

プロフィール
クラウドサポートアソシエイト at AWSジャパン(試用期間中)/リリースチェッカー運営(https://a.noare.net/)/情報処理技術者試験マニア(https://didit.iwamot.com/iwamot)/JAPAN MENSA会員/投稿は所属先の見解ではありません。文責は個人にあります
カテゴリ別アーカイブ
記事検索