0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Service Object・Facade・Adapterの違いと使いどころを、1つの実装から理解する

0
Posted at

パターンを個別に覚えていってもなかなか腑に落ちない

Service ObjectとFacadeについて調べていたとき、途中で違いが分からなくなりました。

どちらも、

  • 複数の処理をまとめる
  • 呼び出し側をシンプルにする
  • 内部の処理を意識しなくてよくする

というところで一緒じゃない??と思いました。

例えば、

class UpdateUserAvatarService
  def call(user, file)
    validate_image(file)
    save_image(file)
    user.update!(avatar_path: ...)
    notify(user)
  end
end

のようなクラスを見たとき、

これはService Objectなのか、Facadeなのか?

と考え始めると、だんだん境界が分からなくなってきました。

さらに調べていくと、外部APIやSDKをラップする話ではAdapterも出てきます。

すると今度は、

S3 SDKを隠して save(file) だけ公開するクラスは、FacadeなのかAdapterなのか?

という疑問も出てきます。

ここで一度、

そもそも、これらをきれいに分類することが目的なのか?

と考えるようになりました。

実際のコードでは、参考書のように、

これはService Object
これはFacade
これはAdapter

と明確に分かれているとは限りません。

1つのクラスが、

  • 一連の業務処理をまとめるService Objectとして振る舞い
  • 内部の複雑さを隠すFacade的な役割を持ち
  • 外部APIの違いを吸収するAdapter的な役割も持つ

ということもあるみたいです。

なので、コードを見て、

「これは何パターンか」

がわかることよりも、

大事なのは、

今のコードにはどんな設計上の問題があり、その問題を解消する方法を知っている・引き出しをいくつか持っている

ことだと思いました。

この記事では、Service Object・Facade・Adapterの定義を個別に覚えるのではなく、

「画像を保存する」という1つの実装を少しずつ改善していく

という流れで、それぞれがどんな問題を解決する考え方なのかを整理していきます。

なお、Service ObjectはGoFのデザインパターンではありませんが、

責務や依存関係をどう整理するかを考えるうえで、実務で一緒に登場しやすい設計上の考え方

としてこの記事では扱います。

その過程で、

  • なぜService Objectとして処理を切り出すのか
  • Facadeは何を隠しているのか
  • Adapterは何を変換しているのか
  • 同じクラスに複数の役割が混ざるのは問題なのか
  • どこまで分ければよく、どこからが分けすぎなのか

を見ていきます。

この記事の目的は、パターン名を正しく分類できるようになることではありません。

設計上の問題に気づき、その問題に対して適切な考え方を選べるようになること

を目指します。

まずは素直に実装する

Railsで、ユーザーがプロフィール画像を更新する機能を実装するとします。

最初はControllerに直接書いても動きます。

class UsersController < ApplicationController
  def update_avatar
    key = "avatars/#{current_user.id}.png"

    s3 = Aws::S3::Client.new

    s3.put_object(
      bucket: ENV["S3_BUCKET"],
      key: key,
      body: params[:file]
    )

    current_user.update!(
      avatar_path: key
    )

    render json: current_user
  end
end

この時点では、必ずしも問題ではありません。

数行程度の小さな処理であれば、無理にクラスを増やす方が理解しづらくなることもあります。

ただ、実務では要件が少しずつ増えていきます。

例えば、

  • ファイル形式を検証する
  • ファイルサイズを検証する
  • 画像をS3に保存する
  • Userを更新する
  • 変更履歴を残す
  • 更新通知を送る

といった処理が必要になったとします。

def update_avatar
  validate_image(params[:file])

  key = "avatars/#{current_user.id}.png"

  s3 = Aws::S3::Client.new

  s3.put_object(
    bucket: ENV["S3_BUCKET"],
    key: key,
    body: params[:file]
  )

  current_user.update!(
    avatar_path: key
  )

  AuditLog.create!(
    user: current_user,
    action: "avatar_updated"
  )

  AvatarUpdatedMailer
    .with(user: current_user)
    .deliver_later

  render json: current_user
end

ここまで来ると、Controllerがかなり多くのことを知っています。

構造にすると、次のような状態です。

UsersController から複数の依存が放射状に伸びています。

ここで大切なのは、

「どのデザインパターンを使うか」

を先に考えることではありません。

まずは、

このControllerは何を担当するべきなのか

を考えます。


Step 1: ユースケースを切り出す

Controllerの主な役割は、

  • HTTPリクエストを受け取る
  • アプリケーションの処理を呼び出す
  • レスポンスを返す

ことです。

しかし現在は、

  • 画像の検証
  • ストレージ保存
  • DB更新
  • 監査ログ
  • メール送信

まで担当しています。

これらをまとめると、

プロフィール画像を変更する

という1つのユースケースとして表現できます。

そこで、この一連の処理をクラスに切り出します。

class UpdateUserAvatarService
  def call(user, file)
    validate_image(file)

    key = "avatars/#{user.id}.png"

    s3 = Aws::S3::Client.new

    s3.put_object(
      bucket: ENV["S3_BUCKET"],
      key: key,
      body: file
    )

    user.update!(
      avatar_path: key
    )

    AuditLog.create!(
      user: user,
      action: "avatar_updated"
    )

    AvatarUpdatedMailer
      .with(user: user)
      .deliver_later
  end
end

Controllerは次のようになります。

def update_avatar
  UpdateUserAvatarService.new.call(
    current_user,
    params[:file]
  )

  render json: current_user
end

構造も変わります。

Controllerから直接伸びていた依存が、UpdateUserAvatarService に集まりました。

これが、RailsでよくいうService Objectです。


Service Objectは何を解決しているのか

Service Objectで大切なのは、Service という名前ではありません。

重要なのは、

一連の処理に名前をつけ、その処理の責務を切り出すこと

です。

今回であれば、

画像を検証する
↓
画像を保存する
↓
Userを更新する
↓
ログを残す
↓
通知する

という一連の処理を、

プロフィール画像を変更する

という意味のある単位で切り出しました。

つまりService Objectは、

ユースケース単位で処理をまとめる

ための手段として使えます。


Service Objectを作るべきかどうか

何でもService Objectにすればよいわけではありません。

例えば、

class UpdateUserNameService
  def call(user, name)
    user.update!(name: name)
  end
end

程度であれば、

user.update!(name: name)

で十分かもしれません。

Service Objectを検討したくなるのは、例えば次のようなときです。

  • 複数のModelを更新する
  • 複数の外部サービスを呼び出す
  • 処理順序に意味がある
  • トランザクション境界を作りたい
  • ControllerやJobに処理が増えている
  • 一連の処理に業務上の名前を付けられる

特に、

この一連の処理を一言で表せるか

は、ひとつの判断材料になります。


Step 2: Service ObjectにS3の知識が漏れている

Controllerから処理を切り出せたので、一見きれいになりました。

しかし、UpdateUserAvatarService を見ると、まだ気になる部分があります。

s3 = Aws::S3::Client.new

s3.put_object(
  bucket: ENV["S3_BUCKET"],
  key: key,
  body: file
)

ここで考えたいのは、

このService Objectは本当にS3について知る必要があるのか

ということです。

このクラスが実現したいのは、

プロフィール画像を保存すること

です。

別に、

S3 SDKの put_object を実行すること

が目的ではありません。

しかし現状では、

  • AWSを利用している
  • S3を利用している
  • put_object というAPIがある
  • bucket
  • key
  • body

といったインフラ側の事情まで知っています。

つまり、

業務処理のクラスにインフラの知識が漏れている

状態です。


Step 3: 画像保存の窓口を作る

そこで、画像保存の処理を別クラスに切り出します。

class ImageStorage
  def save(file, path)
    s3.put_object(
      bucket: ENV["S3_BUCKET"],
      key: path,
      body: file
    )

    path
  end

  private

  def s3
    @s3 ||= Aws::S3::Client.new
  end
end

Service Object側は次のようになります。

class UpdateUserAvatarService
  def initialize(image_storage: ImageStorage.new)
    @image_storage = image_storage
  end

  def call(user, file)
    validate_image(file)

    path = "avatars/#{user.id}.png"

    image_storage.save(file, path)

    user.update!(
      avatar_path: path
    )

    AuditLog.create!(
      user: user,
      action: "avatar_updated"
    )

    AvatarUpdatedMailer
      .with(user: user)
      .deliver_later
  end

  private

  attr_reader :image_storage
end

構造は次のようになります。

ここで着目したいのは、

UpdateUserAvatarService → AwsS3Client

という直接依存がなくなったことです。

代わりに、

UpdateUserAvatarService
        ↓
ImageStorage
        ↓
AwsS3Client

という形になりました。

依存そのものが消えたわけではありません。

どのクラスがその依存を引き受けるかを変えた

ということです。


ここでFacadeの考え方が出てくる

この ImageStorage は、Facade的な役割を持っています。

Facadeは、

内部の複雑な仕組みに対して、利用側が使いやすい入口を提供する

ための考え方です。

利用側は、

image_storage.save(file, path)

と呼ぶだけです。

内部では、

  • S3 Clientを生成する
  • Bucketを指定する
  • Keyを指定する
  • AWS SDKを呼ぶ

といった処理をしていますが、利用側はそれを知る必要がありません。

つまり、

画像を保存したいなら、この入口を使えばよい

という状態です。

これがFacade的な設計です。


Facadeの「隠す」とは何か

Facadeを理解するときに重要なのが、

複雑さを隠す

という表現です。

この「隠す」は、単に private にするという意味ではありません。

重要なのは、

利用側が知る必要のある概念を減らすこと

です。

Facadeを置く前は、利用側が、

AWS
S3
Client
Bucket
put_object

といった概念を知る必要がありました。

Facadeを置いた後は、

画像を保存する

だけ知っていればよくなります。

つまりFacadeは、

利用側の認知負荷を下げる

役割も持っています。


Step 4: S3 SDKのインターフェースをアプリ側に合わせる

ここまででも十分な設計かもしれません。

ただし、もう少し変更可能性を考えてみます。

例えば今後、

  • 開発環境ではローカルストレージ
  • 本番環境ではS3
  • 将来的には別のクラウドストレージ

を使う可能性があるとします。

S3 SDKでは、

put_object(
  bucket: ...,
  key: ...,
  body: ...
)

というAPIを使います。

一方で別のサービスでは、

upload(
  filename: ...,
  data: ...
)

のようなAPIかもしれません。

外部サービスごとにインターフェースは異なります。

しかしアプリケーション側としては、

storage.save(file, path)

という形で統一して扱いたい。

そこでAdapterという考え方が出てきます。


Adapterで外部APIの違いを吸収する

例えば次のようなクラスを用意します。

class S3StorageAdapter
  def save(file, path)
    s3.put_object(
      bucket: ENV["S3_BUCKET"],
      key: path,
      body: file
    )

    path
  end

  private

  def s3
    @s3 ||= Aws::S3::Client.new
  end
end

Adapterは、

アプリケーション側
save(file, path)

を、

S3 SDK側
put_object(bucket:, key:, body:)

へ変換しています。

クラス図にすると、次のようになります。

つまりAdapterは、

外部のインターフェースを、自分たちが使いたい形に合わせる

ためのパターンです。


保存先が増えるとAdapterの役割がより分かりやすい

例えば保存先として、

  • S3
  • ローカルファイル

の2種類を使うとします。

アプリケーション側はどちらでも、

storage.save(file, path)

と呼びたい。

その場合は、次のような構造にできます。

S3なら、

put_object(...)

ローカルなら、

File.write(...)

と内部実装は違っても、アプリケーション側は、

save(file, path)

だけ知っていればよい状態になります。


FacadeとAdapterは何が違うのか

ここが混乱しやすいところです。

実際には、同じクラスが両方の役割を持つことがあります。

例えば、

class ImageStorage
  def save(file, path)
    Aws::S3::Client.new.put_object(
      bucket: ENV["S3_BUCKET"],
      key: path,
      body: file
    )
  end
end

このクラスは、

  • S3の細かい使い方を利用側から隠している
  • S3 SDKのAPIを save という形に変換している

ので、

Facade的でもあり、Adapter的でもあります。

違いは、クラスの形ではありません。

見ている問題が違います。

Facadeは、

内部の複雑さを利用側から隠したい

という問題を解決します。

Adapterは、

外部のインターフェースをアプリ側に合わせたい

という問題を解決します。

整理すると、

Facade
→ 複雑さを隠す

Adapter
→ インターフェースを合わせる

です。


Service Objectとの違い

ここでService Objectも含めて整理してみます。

今回の例では、

UpdateUserAvatarService

は、

プロフィール画像を変更する

というユースケースをまとめています。

一方、

ImageStorage

は、

画像保存の内部構造を隠す

役割を持っています。

さらに、

S3StorageAdapter

は、

S3 SDKのAPIをアプリ側の形に合わせる

役割を持っています。

つまり、

Service Object
→ 何の処理をまとめるか

Facade
→ 何を利用側から隠すか

Adapter
→ どのインターフェースを合わせるか

という違いがあります。


すべて分けるとこうなる

3つを明示的に分けると、例えば次のような構造になります。

役割を言葉にすると、

Controller
「プロフィール画像を変更したい」

        ↓

Service Object
「プロフィール画像変更という処理を実行する」

        ↓

Facade
「画像を保存したいならここを使う」

        ↓

Adapter
「S3 SDKの形式に変換する」

        ↓

AWS SDK
「put_object(...)」

となります。


ただし、ここまで分ける必要があるとは限らない

ここは実務では特に重要です。

デザインパターンを学んでいると、

Service Object
↓
Facade
↓
Adapter
↓
SDK

と、きれいにクラスを分けたくなることがあります。

しかし、必ずしもそれが良い設計とは限りません。

例えば、

class ImageStorage
  def save(file, path)
    Aws::S3::Client.new.put_object(
      bucket: ENV["S3_BUCKET"],
      key: path,
      body: file
    )

    path
  end
end

だけで十分なら、それで問題ありません。

この1クラスが、

  • FacadeとしてS3の詳細を隠す
  • AdapterとしてS3 SDKの形式を変換する

という2つの役割を持っていても、困っていなければ分割する必要はありません。


パターンを知ると分けすぎることもある

例えば、パターンをすべて別クラスとして表現しようとすると、こんな構造になるかもしれません。

S3へ画像を保存するだけの機能に対して、ここまで必要でしょうか。

もちろん、規模や要件によっては必要になることもあります。

ただ、現時点で、

  • 保存先はS3だけ
  • SDKの変更頻度も高くない
  • 複数実装を切り替える予定もない

のであれば、次のくらいで十分かもしれません。

こちらの方が、構造を理解するコストは低くなります。


「責務が2つあるから分ける」は単純すぎる

設計を学ぶと、

1クラス1責務

という言葉をよく見ます。

これを機械的に適用すると、

S3を隠す責務
S3 APIを変換する責務

だから、

ImageStorageFacade
S3StorageAdapter

に分けよう、と考えたくなります。

ただ、実務では、

この2つは別々の理由で変更されるのか

を見る方が重要です。

例えば、

class S3ImageStorage
  def save(file, path)
    ...
  end
end

というクラスがあり、

S3の扱いが変わればこのクラスを変更する。

保存先が変われば別の実装を作る。

それで十分なら、無理にさらに分ける必要はありません。

責務は、

日本語で何個説明できるか

ではなく、

何を理由に変更されるか

で考える方が実践的です。


抽象化にはコストがある

「将来変わるかもしれないから抽象化する」

という考え方も注意が必要です。

例えば、

将来的にS3からGoogle Cloud Storageへ移行するかもしれない

という理由だけで、

StorageInterface
↓
StorageFacade
↓
StorageFactory
↓
S3StorageAdapter

まで用意する必要があるとは限りません。

抽象化すると、

  • クラスが増える
  • 処理を追いにくくなる
  • 名前を考える必要がある
  • テスト対象が増える
  • 構造を理解するコストが増える

という負担もあります。

抽象化そのものが目的になると、かえって設計が複雑になることがあります。


変更可能性に対して抽象化する

では、いつ抽象化するべきなのでしょうか。

例えば、

  • すでに複数の保存先が存在する
  • 環境ごとに保存先を切り替える
  • 外部SDKへの依存を局所化したい
  • テストで依存を差し替えたい
  • SDK変更の影響を限定したい

といった具体的な理由があるなら、Adapterや抽象化を置く価値があります。

一方で、

いつか変わるかもしれない

だけなら、今はシンプルな実装にしておく判断も十分あり得ます。

重要なのは、

想像上の変更ではなく、現実の変更可能性とコストのバランスを見ること

です。


テストのしやすさからも見てみる

依存関係を整理すると、テストもしやすくなります。

最初のService Objectでは、

Aws::S3::Client.new

を内部で直接生成していました。

この状態だと、Service ObjectのテストなのにS3の扱いまで意識する必要があります。

一方で、

class UpdateUserAvatarService
  def initialize(image_storage:)
    @image_storage = image_storage
  end
end

としておけば、

image_storage = instance_double(ImageStorage)

allow(image_storage)
  .to receive(:save)
  .and_return("avatars/1.png")

のように差し替えられます。

Service Objectのテストでは、

画像保存を依頼した後にUserが正しく更新されるか

に集中できます。

S3 SDKとの接続については、ImageStorage 側でテストすればよい。

このように、

責務を分けることと、テストの境界を分けること

はかなり近い関係にあります。


実務ではどこで気づけばいいか

今回の流れを、設計判断として整理すると次のようになります。

Controllerに処理が増えてきた

複数ステップの業務処理になっている

↓

ユースケースとして切り出せないか考える

↓

Service Object

業務処理にS3の詳細が出てきた

Aws::S3::Client
put_object
bucket

↓

このクラスは本当にS3を知る必要があるか考える

↓

画像保存という窓口を作る

↓

Facade的な設計

外部SDKのAPIがアプリ側に漏れている

put_object(bucket:, key:, body:)

↓

アプリ側では save(file, path) として扱いたい

↓

Adapter的な設計

パターンから設計するのではなく、問題から設計する

今回の実装は、最初から、

Service Objectを使おう

Facadeを置こう

Adapterを作ろう

と考えたわけではありません。

まず問題がありました。

Controllerが複雑になった
        ↓
ユースケースを切り出した
        ↓
Service Object

業務処理にS3の詳細が漏れていた
        ↓
画像保存の窓口を作った
        ↓
Facade的な設計

S3 SDKのAPIが漏れていた
        ↓
アプリ側の形式に合わせた
        ↓
Adapter的な設計

後から振り返ると、

これはService Objectという考え方に近い

ここではFacadeの考え方を使っている

ここはAdapterとして整理できる

と名前をつけられます。

この順番の方が、実務では使いやすいと思います。


パターン名を知る意味

では、パターン名を覚えることに意味がないかというと、そうではありません。

名前を知っていると、

「このSDK依存、Adapterで包めそうですね」

「ここはFacadeを1つ置けば呼び出し側をかなり単純化できそうです」

のように、設計上のアイデアを短い言葉で共有できます。

つまりデザインパターンの名前は、

設計の完成形を当てるための答え

というより、

設計上の問題と解決方法を共有するための共通言語

として使うと価値があります。

まとめ

今回扱った3つを整理すると、次のようになります。

考え方 主に解決する問題
Service Object 一連の業務処理をどこに置くか
Facade 内部の複雑さを利用側から隠す
Adapter 外部のインターフェースを自分たちの形に合わせる

ただし、「このクラスは何パターンなのか」

を正しく分類することが目的とせず、

「今、どんな設計上の問題を解決しようとしているのか」

を考えられるようにすることの方が大事な気がします。
パターン名だけを蓄積しても、使いどころが分からなければ実装にはつながらないので、

  • 責務をどこに置くか
  • 何を利用側から隠すか
  • どの依存を隔離するか
  • どの変更を局所化するか
  • どこまで抽象化するか

を考えられるようにするための引き出しとして、良い設計を考えるための言葉と視点として持っておこうと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?