🌎

フラクタルな旅(翻訳)

2023/02/01に公開

英語記事: Fractal journeys
原文公開日: 2022/09/18
原著者: Jorge Manrubia


フラクタルとは、同じようなパターンが徐々に小さなスケールへ向かいながら繰り返されることです。私にとって、優れたコードはフラクタルです。異なる抽象レベルにおいて同じ品質が繰り返されるのです。

これは驚くべきことではありません。良いコードとは、理解しやすいコードであり、複雑さに対処するための最良の仕組みは、抽象化することなのです。抽象化は、複雑な要素を私たち人間にとって理解しやすいインターフェースへ置き換えます。しかし、私たちは依然として下へ押しやられた複雑性に対処する必要があります。そのために、同じプロセスを繰り返します。詳細を隠蔽する新しい抽象を構築し、それらを扱うより高いレベルの仕組みを提供するのです。

ここで言う「抽象」とは、大規模なサブシステムから、ある内部クラスの隅っこのプライベートメソッドまで、全てを対象にしています。しかし、そのような抽象をどのように構築するのでしょうか?これは大変難しい質問であり、数え切れないほど多くの書籍が題材にしています。この記事では、コードを理解しやすくするために不可欠だと私が考える4つの資質に焦点を当てたいと思います。

  • ドメイン駆動:問題のドメインを表すこと。
  • カプセル化:明瞭なインターフェースを公開し、詳細を隠蔽すること。
  • 凝集性:呼び出し元から見て、一つのことのみ行うこと。
  • 対称性:同じ抽象レベルで動作すること。

ここまでの話は少し抽象的になりすぎているので、Basecampで実際に使用されているコードで説明します。Basecampでは、いくつかの場所でアクティビティのタイムラインが表示されます。このタイムラインは動的に更新されます。すなわち、あなたが見ている間に誰かが何かをすれば、リアルタイムに更新されます。

ドメインレベルでは、Basecampにおいて、TODOの完了やドキュメントの作成、コメントの投稿などのアクションを行うと、システムが「イベント」を作成し、それらのイベントはアクティビティタイムラインやWebhooksなどの宛先へ「リレー(中継)」されます。それでは、コードを見てみましょう。

まず最初に、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がここで注目したいメソッドです。このメソッドは、ドメイン言語を話し、それを呼び出すジョブの視点から見ると一つのことを行い、イベントをリレーすることに関わる全てがここに隠蔽されていることに注目してください。このメソッドを掘り下げてみましょう。

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が「タイムライン上にあるかどうか」を確認し、イベントをタイムラインにリレーする Timeline::Relayer オブジェクトを生成しています。Timeline::Relayerに対応したイベントを取り消すクラスTimeline::Revokerも存在し、これらは対称的です。このメソッドは凝集性が高くリレーとタイムラインに集中しており、そして実装の詳細は隠されたままです。それでは、Timeline::Relayerについて見てみましょう。

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を公開しており、実装の詳細は隠されています。その内部では、リレーをデータベースに保存し、Action Cableでブロードキャストするという2つの処理を行っています(このコードはHotwireより何年も前に書かれたものです)。対称性に注目してください。双方の操作が1行の呼び出しであっても、上位レベルのメソッドとしてそれぞれ抽出されています。

最後に、低レベルの詳細について説明します。#recordメソッドはリレーをデータベースへ永続化します。リレーは、Railsのdelegated typesの起源となった重要なユースケースであるrecordingのrecordableです。そして#broadcastは、イベントを受信者にブロードキャストするメソッドで、私たちが最初に興味を持ったメソッドです。

この例では、イベントが作成された瞬間から、それがAction Cableのチャネルでプッシュされるまでのリレーのロジックを簡単に理解することができます。なぜなら、コードジャンプしたそれぞれの箇所において、注意しなければならないことがたった一つに絞られているからです。すなわち、一つの責務、一つの抽象レベル、対象の問題を反映する命名が実現されているからです。もちろん、何が良いコードであるかというのは、主観的で、より多くの概念を含んでいます。しかし、自明でない複雑なシステムにおいて、容易な旅を可能にしてくれるこのフラクタルは、私が好きなコードの第一の品質です。


この記事は、Code I likeというRailsの設計技法に関する連載記事に属しています。

また、何年も前の記事ですが、composedメソッドの実装パターンに関する考察もご覧ください。この記事の一番の見どころは、記事の中で紹介している2冊の本です。これらの話題に興味がある場合はぜひ読んでみてください。

jorgemanrubia.com
写真:Martin RancourtUnsplash

脚注
  1. その他翻訳記事:
    第一弾: Domain driven boldness
    第三弾: Good concerns
    第四弾(TechRachoさん): Vanilla Rails is plenty
    第五弾: Active Record, nice and blended ↩︎

Discussion