2020年04月

開発用サーバもOracle Cloud Free Tierに乗り換えた

リリースチェッカーむびりすなどの開発用サーバを、さくらのVPSから、Oracle Cloud Free TierのVMインスタンスに移行しました。さくらのVPSに不満があったわけではなく、節約目的です。年間17,055円が浮くことになります。

リリースチェッカーの本番用サーバは昨秋に移行済みです。開発用サーバも早く移行したかったのですが、Oracle Cloudの空きがなかなか出ず、時間がかかってしまいました。やはり人気なのでしょう。


以下、Oracle Cloudとは無関係な話です。今回の移行を機に、むびりすのRailsを5.2.2から6.0.2.2に上げました。「rails app:update」して「config.hosts」を追加しただけで、あっさり動きました。ついでに、Rubyも2.6.0から2.7.1に上げています。

さらに無関係な話ですが、昨秋の記事に書いたリリースチェッカーの手動デプロイは、その後、DockerのSwarmモードに変更しました。「docker service update」コマンドひとつでデプロイできて便利です。


そんなふうに最近は過ごしています。映画館に行くのが趣味だったのですが、今は無理なので、あいた時間を自作サービスの改善にあてている感じです。

本業は以前からフルリモートで変わりありません。今は、ぼくにとっては初物尽くしのGo+GitHub Actions+ECS(Fargate)でマイクロサービスを作っています。仕事のあるうちは、やるべきことを淡々とやるのがよいと思っています。

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初学者なので、焦らず進めていく。

プロフィール

ENECHANGE株式会社VPoT兼CTO室マネージャー。AWS Community Builder (Cloud Operations)。前職はAWS Japan技術サポート。社内外を問わず開発者体験の向上に取り組んでいます

カテゴリ別アーカイブ
月別アーカイブ
ブログ内検索