Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: 私の好きなコード(2)フラクタルな旅(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: 私の好きなコード(2)フラクタルな旅(翻訳)

フラクタルとは、より小さなスケールに進んでも同じようなパターンが繰り返される現象です。私にとって、優れたコードはフラクタルなコードです。すなわち、抽象化レベルが高い場所でも低い場所でも品質が同じに保たれている様子が観察できるということです。

これは別段驚くような話ではありません。優れたコードとは理解がたやすいコードのことであり、複雑さに対処するベストなメカニズムは、すなわち抽象化の構築です。インターフェイスを私たち人間が理解しやすいものにするために、抽象化の構築によって複雑さのやりとりが行われます。しかし抽象化によって背後に押しやられた複雑性については引き続き処理が必要です。そのために、同じ手順を全体で繰り返すことになります。詳細を隠蔽する抽象を新たに構築し、それらを処理する上位のメカニズムを提供するわけです。

私は、巨大サブシステムから内部クラスの末尾にあるprivateメソッドにいたるまで、あらゆるものを抽象化によって参照します。しかし抽象化はどのように構築すればよいのでしょうか?これは100万ドルに値する質問であり、無数の書籍の主題にもなっています。本記事では、コードを理解しやすくするために必要だと私が考える4つの性質に注目していきたいと思います。

  • ドメイン駆動(Domain-Driven): 問題のドメインを言語化する
  • カプセル化(Encapsulation): 非常に明確なインターフェイスを公開し、細部を隠蔽する
  • 凝集性(Cohesiveness): 呼び出し元から見て1つの処理だけを行う
  • 対称性(Symmetry): 同じ抽象化レベルで動作する

失礼、本記事そのものの抽象化が早くもやりすぎになりつつあるので、Basecampで使われている実際のコードでわかりやすく説明したいと思います。

Basecampは多くの場面でアクティビティタイムラインという情報を提供しています。このタイムラインは動的に更新され、ユーザーが表示中に誰かが何らかの操作を行うと即座に更新されます。

ドメインレベルでは、Basecampでユーザーが何らかの操作(ToDoを完了、ドキュメント作成、コメント投稿など)を行うと、システムは"イベント"を作成し、それらのイベントはさまざまな場所(アクティビティタイムライン、Webフックなど)に"中継"(relay)されます。このコードを見ていくことにしましょう。

最初にEventモデルがあり、Relayingというconcernがincludeされています(関係ないコードは省略しています)。

class Event < ApplicationRecord
  include Relaying
end

このconcernは、relays関連付けと、イベント作成時にイベントを非同期に中継するフックを追加します。

module Event::Relaying extend ActiveSupport::Concern

  included do
    after_create_commit :relay_later, if: :relaying?
    has_many :relays
  end

  def relay_later
    Event::RelayJob.perform_later(self)
  end

  def relay_now
    ...
  end
end

class Event::RelayJob < ApplicationJob
  def perform(event)
    event.relay_now
  end
end

ここで注目したいのはEvent#relay_nowメソッドです。このメソッドを見るといくつかの点に気づきます。

  • このメソッド名はドメインの言葉になっている
  • メソッドを呼び出すジョブ側から見て1つの処理だけを行っている
  • イベントの中継に関するすべてがこの場所で隠蔽されている

このメソッドを深掘りしてみましょう。

module Event::Relaying
  def relay_now
    relay_to_or_revoke_from_timeline

    relay_to_webhooks_later
    relay_to_customer_tracking_later

    if recording
      relay_to_readers
      relay_to_appearants
      relay_to_recipients
      relay_to_schedule
    end
  end
end

このメソッドは、より低レベルのいくつかのメソッド呼び出しを取りまとめています。呼び出されるのはすべて中継に関するメソッドなので、凝集性は保たれています。「どこに中継するか」という行き先も、ドメインに基づいたメソッド名として明確に示されています。詳細も引き続き隠蔽されています。さらに、他の抽象化レベルを見に行かなくても、このメソッドが何をしているかを理解できるようになっているので、対称性も実現されています。

以下の#relay_to_or_revoke_from_timelineメソッドが、どうやら私たちが探しているもののようです。

module Event::Relaying private
    def relay_to_or_revoke_from_timeline
      if bucket.timelined?
        ::Timeline::Relayer.new(self).relay
        ::Timeline::Revoker.new(self).revoke
      end
    end
end

ここでも、ドメインに基づいた良い命名がなされています。あるbucketがタイムラインに乗っているかどうかをtimelined?でチェックし、Timeline::Relayerオブジェクトを作成してイベントをタイムラインにrelayしています。イベントをrevokeするクラス名(Timeline::Revoker)はrelayと対になっているので、対称性も達成されています。このメソッドは中継とタイムラインに専念しているので凝集性も達成されていますし、実装の詳細も引き続き隠蔽されています。

では、そのクラスを見てみましょう。

class Timeline::Relayer def initialize(*event*)
    @event = event end

  def relay
    if relaying?
      record
      broadcast
    end
  end

  private
    attr_reader :event
    delegate :bucket, to: :event

    def record
      bucket.record Relay.new(event: event), parent: timeline_recording, visible_to_clients: visible_to_clients?
    end

    def broadcast
      TimelineChannel.broadcast_event(event, to: recipients)
    end
end

今度は純粋なRubyクラスによる抽象化であり、メソッドによる抽象化ではありませんが、同じ特徴を観察できます。

公開されている#relay publicメソッドは詳細を隠蔽しています。中を見てみると、中継をデータベースに記録(record)する操作と、イベントをAction Cableでブロードキャストする操作(なお、このコードはHotwire登場よりずっと前に書かれました)という2つの操作を行っています。さらに、2つの操作はどちらも1行のコードを呼び出しているだけですが、どちらもより上位レベルのメソッドとして切り出されているので、対称性も達成されています。

ついに低レベルの詳細部分に到達しました。
#recordメソッドは中継をデータベースで永続化します。中継(relay)は録画としてレコーディング可能であり、Railsのdelegated type(#393411の元にもなった典型的なユースケースでもあります。
#broadcastメソッドは、イベントを受信者たちにブロードキャストする場所であり、本記事で最初に知りたいと思っていた部分です。

このコード例では、イベント作成の瞬間から、Action Cableチャネル経由でプッシュされるまでの中継ロジックをスムーズに理解できました。これが可能になったのは、コードをジャンプするたびにそこで注目すべき内容が常に「1つだけ」に保たれていたおかげです。どこにジャンプしようと責務は1つだけで、抽象化レベルをまたがらずに1つの抽象化レベルに収まっています。メソッド名やクラス名は、私たちが考えている問題を的確に反映しています。もちろん、コードが優れているかどうかは主観に基づくものですし、他にも多くの概念と関連していますが、ある程度以上の規模のシステムでコードを楽に追いかけられるように書けるスキルは、私が最も愛する特性です。

本記事は、『私の好きなコード』シリーズの記事です。

随分前の記事になりますが、take on the composed method implementation patternもご覧ください。同記事では2冊の書籍を参照していて、そこが記事で最も有用な部分です。このトピックにご興味がおありでしたら、ぜひこれらの書籍も読むことをおすすめします。

関連記事

Rails: 私の好きなコード(1)大胆かつ的確なドメイン駆動開発(翻訳)

Rails: 私の好きなコード(3)"正しく書かれた" concerns(翻訳)

素のRailsは十分に豊かである(翻訳)

Rails: 私の好きなコード(5)永続化とロジックを絶妙にブレンドするActive Record(翻訳)

Rails API: ActiveSupport::ConcernとModule::Concerning(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。