2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

サービス地獄とは何か 〜依存関係がツリーからツタに変わる瞬間〜

Posted at

はじめに

Webアプリケーションの設計レビューをしていると、
誰もが一度はこんな構造に出会います。

Controller
 └ OrderService
     ├ UserService
     │   └ AuthService
     │       └ RoleService
     ├ CampaignService
     │   └ DiscountService
     └ MailService
         └ TemplateService

一見すると

  • Controller は薄い
  • Service に責務を分離している
  • 再利用も意識している

―― ように見えます。

しかし実際には、

どこから読めばいいか分からない

状態に陥っていることが多いです。

この記事では、この状態を 「サービス地獄」 と呼び、

  • なぜ起きるのか
  • なぜ辛いのか
    どうすれば避けられるのか

を、図とコードで整理します。


サービス地獄とは

Service が Service を呼び始め、
依存関係がツリーではなくツタになる状態

です。

本来期待していた姿(ツリー)

Controller
 └ OrderService
     ├ OrderRepository
     └ MailSender
  • 上から下に読める
  • 依存の向きが一方向
  • 流れが一目で分かる

現実に起きがちな姿(ツタ)

OrderService
 ├ UserService
 │   └ AuthService
 │       └ RoleService
 ├ CampaignService
 │   └ DiscountService
 └ MailService
     └ TemplateService
  • Service が Service を呼ぶ
  • 依存が横にも斜めにも伸びる
  • 読むたびに別ファイルへジャンプする

なぜ「地獄」なのか

①処理の流れが追えない

OrderService を読んでいるはずなのに、

  • UserService に飛び
  • AuthService に飛び
  • RoleService に飛ぶ

本筋が見えなくなります。


②変更影響範囲が分からない

「この Service、どこから呼ばれてる?」

  • IDE の参照検索が前提
  • 安全に直せない
  • 結果、誰も触らなくなる

③テストが重くなる

new OrderService(
    new UserService(
        new AuthService(
            new RoleService(...)
        )
    ),
    new CampaignService(
        new DiscountService(...)
    )
)
  • モックが大量
  • DI が複雑
  • テストが本体より難しい

なぜ起きるのか

原因① Service の定義が曖昧

多くの現場で Service はこう定義されます。

Controller にロジックを書かないための層

これは 否定形の定義 です。

  • Controller に書かない
  • Repository に書かない
  • Entity にも書かない

結果、

じゃあ全部 Service に置くしかない

となります。


原因② 再利用=Service という誤解

_userService.CanOrder(user);

便利です。 しかしこの一行が、

  • Service → Service 依存
  • 責務の混在

を生みます。


よくあるコード例

public class OrderService
{
    private readonly UserService _userService;

    public void PlaceOrder(int userId)
    {
        var user = _userService.Get(userId);

        if (!_userService.CanOrder(user))
            throw new Exception();

        // 注文登録
    }
}

この時点で、

  • 取得
  • ルール
  • フロー

が Service に混在しています。


抜け出す方向性

① Service は「流れ」だけにする

public void PlaceOrder()
{
    // ①取得
    // ②判定
    // ③登録
    // ④通知
}

意味を持たせないのがポイントです。


② ルールは Service の外へ

public class User
{
    public bool CanPlaceOrder()
    {
        return IsActive && !IsBanned;
    }
}

if (!user.CanPlaceOrder())
    throw new Exception();
  • Service 依存が消える
  • 意味がコードに残る

③ 再利用したくなったら警戒する

再利用したくなった瞬間は、

Service から出す合図

  • Entity
  • Policy
  • Specification

を検討します。


まとめ

サービス地獄は 設計を真面目にやった結果、誰もが踏む

  • 問題は Service の数ではない
  • 問題は Service に意味を詰め込みすぎたこと

ツリーで描ける設計は、頭でも追える

もしツタになっていたら、

  • Service を疑う
  • ルールの置き場所を疑う

そこが改善のスタート地点です。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?