Skip to content

Instantly share code, notes, and snippets.

@monzou
Last active December 20, 2024 17:34
Show Gist options
  • Save monzou/8eaf32f1f8fda92e18e3 to your computer and use it in GitHub Desktop.
Save monzou/8eaf32f1f8fda92e18e3 to your computer and use it in GitHub Desktop.
「『DCI なんて面倒なだけで Service 使えばいい』への返答」を読んだ感想とポエム

@rosylilly さんの『DCI なんて面倒なだけで Service 使えばいい』への返答 を読んで, 余り Service を使った場合の実装を見たことが無さそうに感じたので参考までに「自分ならこう書くだろうな」という例を挙げてみることにしました。@rosylilly さんの悩みを解決するための手助けになると良いなと思います。

ちなみに個人的なスタンスとしては Service とか DCI とかどうでも良くて, DCI も単に Ruby のような表現力のある言語と Rails のような軽量なアーキテクチャを使っている場合にはこんな方法もあるんじゃないの、という提案なのかなと思っています。Ruby や Rails を使っている場合に古めかしい PofEAA のような重量級のアーキテクチャに囚われる必要なんて無いと僕も思いますし, きっとこれから Rails も洗練されていくんでしょう。ここではあくまで @rosylilly さん用に「Service を使った場合はこんな実装になるよ」「Service と DCI は対立するものではないのでは?」という例を挙げたいだけで, 特にアーキテクチャを語りたいわけではありません(そもそも PofEAA なんてもう何年も読んですらいないので感覚的なものになってしまいますし、アーキテクチャ論みたいなものは所謂 SIer みたいなところに勤めている方々の方が詳しいかなと思うので)。というわけでいきなり擬似コードで書きます。Service といえば Java かなと思うので Java で考えます(帰省中でマトモなマシンが無いので実際に動かしてません。タイポとかあったらスミマセン)。


前置きはこれぐらいにしてはじめましょう。まず Actor は Entity でしょう。年齢や性別, say メソッドを持ちます(ここでは元コード同様、単純に System.out.println しているだけですが、普通は出力先を取るでしょうね)。

// Entity
package model.play;

public class Actor {

  public void say(String words) {
    System.out.println(words);
  }

}

Scene03Service というものは違和感を持ちました。Scene 毎の振る舞いというのはドメインロジックであってアプリケーションロジックではありません(元記事で書かれているドメインロジックとアプリケーションロジックという用語の指すところが不明ですが)。PofEAA の正確な定義は覚えていませんし興味もありませんが、基本的に Service はクライアントからの要求に対する公開 API のようなものだと考えると、このアプリケーションに Service レイヤがあると仮定すると恐らく「シーン 03 を再生する」といった要求になるのではないでしょうか。

// Service
package service.play;

public interface PlayService {

  // 指定されたシーンを再生する
  void play(SceneId id);

}

実際はシナリオやシーン番号を指定するのだと思いますが、ここではとりあえずシーンには ID が振られていると仮定してこれを受け取るようにしました。

ドメインロジック単位で Service を切ってしまうと @rosylilly さんの仰る通り手続き型なコードになってしまいます。一般にトランザクションスクリプトと呼ばれるパターンを採るとこうなるのだと思いますが、個人的にはこれは非常に大規模なアプリケーション・もしくは開発メンバーの大多数のレベルが一定以下の場合にのみ採用すべきパターンだと考えます。通常はより再利用性が高く保守しやすい構造化された実装にしたいと思うはずなので, 自分の場合はシーン毎の振る舞いはドメインに寄せます。

ここからは実際に書いてみると色々と変わると思いますが、一例としてはざっくりこんな感じになるでしょう。

// Domain
package model.scene;

// 役割に応じた Actor を取得するプロバイダ
public interface ActorProvider {

  Actor get(Role role);

}

// シーン
public interface Scene {

  void play(ActorProvider provider);

}

// シーンのリポジトリ
public interface SceneRepository {

  Scene get(SceneId id);

}

ActorProvider は役割に応じた Actor を取得します(追記:配役を取得する機能なので Casting と命名した方が自然ですね)。これは DB から取得するのか、それとも設定ファイルからロードするのかも問いません。単にドメインロジック(Scene#play)としては Actor が必要であるということを要求しているだけです。Actor に依存していますがそれすら嫌であれば interface を切り出して Actor がそれを実装すれば良いでしょう。SceneActorProvider に応じて所定のシーンを再生します。あとは要不要はさておき一応 Scene の Repository を用意してみました。これは ID に応じた Scene を提供するクラスです。これも実装は問いません。いちいち Repository を作ったのは Scene がもし別のコンポーネントに依存している場合にスムーズに受け渡しを行えるようにするためです。例えばある Scene では大道具が必要になる場合なんかには便利かもしれませんね。こんな感じです。

class SceneRepositoryImpl implements SceneRepository {

  // 大道具のプロバイダを DI する
  private final StageSettingProvider settingProvider;

  @Inject
  public SceneRepositoryImpl(StageSettingProvider settingProvider) {
    this.settingProvider = settingProvider;
  }

  @Override
  public Scene get(SceneId id) {
    // 例えばシーン 04 では大道具が必要かも ...?
    return new Scene04(settingProvider);
  }


}

まぁこの辺りは本物のアプリケーションであればもう少し色々工夫が必要だと思いますが、今回の例では Scene には特に依存関係が無いので余り意味はありません。

Scene03 の実装はこんな感じになるでしょう。

// シーン 03 の実装
package model.scene.impl;

class Scene03 implements Scene {

  @Override
  public void play(ActorProvider provider) {

    // Romeo 役と Juliette 役の Actor を取得して役に割り当て
    Romeo romeo = new Romeo(provider.get(Role.ROMEO));
    Juliette juliette = new Juliette(provider.get(Role.JULIETTE));

    juliette.ask();
    romeo.hasitate();
    juliette.beg();

  }
  
  // Scene03 におけるロミオ役の振る舞い
  private static class Romeo {
  
    private final Actor actor;

    Romeo(Actor actor) {
      this.actor = actor;
    }

    void hasitate() {
      actor.say("Shall ...");
    }

  }

  // Scene03 におけるジュリエット役の振る舞い
  private static class Juliette {

    private final Actor actor;

    Juliette(Actor actor) {
      this.actor = actor;
    }

    void ask() {
      actor.say("O Romeio, ...");
    }

    void beg() {
      actor.say("Tis but ...");
    }

  }

}

Java だと Ruby のようにミックスイン出来ないため、残念ながら Actor に delegate することになります。ここでは分かりやすく「Scene03 用の Romeo」という風に実装しましたが、「全シーンを通じた Romeo」というクラスを作っても良いと思います。

そろそろ Service を実装しましょう。一般的に Java では Service は Stateless な DI コンポーネントになります。今回は設定ファイルに応じて Actor がロードされるようにしてみます。ActorProviderImpl を作っても良いですが面倒なので Service で実装してみましょう。

package service.play.impl;

@Transactional
public class PlayServiceImpl implements PlayService, ActorProvider {

  // ここではコンフィギュレーションファイルに配役が定義されていると仮定します
  private static final Configuration CONFIGURATION;
  static {
    CONFIGURATION = ConfigurationLoader.load("romeo-and-juliette.yml");
  }

  // SceneRepository を DI する
  private final SceneRepository sceneRepository;

  private final Dao<Actor> actorDao;

  @Inject
  public PlayServiceImpl(SceneRepository sceneRepository, DaoFactory daoFactory) {
    this.sceneRepository = sceneRepository;
    this.actorDao = daoFactory.create(Actor.class);
  }

  // ActorProvider としての振る舞い
  @Override
  Actor get(Role role) {
    Long actorId = CONFIGURATION.getActorIdFor(role);
    return actorDao.findByPk(actorId);
  }

  @Override
  void play(SceneId id) {
    Scene scene = sceneRepository.get(id);
    scene.play(this);
  }


}

ここで Service はあくまでクライアントの要求に応じてドメインロジックを実行しているだけであることに注目して下さい。Service は DAO を介してインフラストラクチャから Actor をロードしたりしていますが, 以降は model.* 以下の実装を呼び出しているだけです。トランザクションスクリプトを採用せず, ドメイン側にロジックを寄せた場合は通常このように Service は非常に薄い層になります。

例えばここに(少々変な仮定ですが)「シーンの再生が終わったら会員に対してメール通知を行う」という機能を追加してみるとしましょう。これは明らかにドメインロジックではなくアプリケーションの振る舞いなので, Service 側にある方が自然ですよね。Service はこんな感じになるでしょう。

@Transactional
public class PlayServiceImpl implements PlayService, ActorProvider {

  private final SceneRepository sceneRepository;

  private final NotificationService notificationService;

  private final Dao<Actor> actorDao;

  @Inject
  public PlayServiceImpl(SceneRepository sceneRepository, NotificationService notificationService, DaoFactory daoFactory) {
    this.sceneRepository = sceneRepository;
    this.notificationService = notificationService;
    this.actorDao = daoFactory.create(Actor.class);
  }

  @Override
  void play(SceneId id) {
    Scene scene = sceneRepository.get(id);
    scene.play(this);
    notificationService.notify(new ScenePlayedEvent(id));
  }


}

さて, もしこのアプリケーションが Web アプリケーションなのであれば PlayService#play は所謂 Controller や Resource と呼ばれる HTTP リクエストのディスパッチャから呼ばれることになるでしょう。もしリッチクライアントアプリケーションなのであれば, クライアント側の要求に応じて RMI を通じて呼び出されることになることもあるでしょう。あるいはバッチアプリケーションから呼び出されることもあるかもしれません。ここでは JAX-RS を使った Web アプリケーションだと考えて Resource を作ってみましょう。

package resource.play;

@Path("/play")
public interface PlayResource {

  @PUT
  @Path("/scene/{id}")
  Response play(@PathParam("id") SceneId id);

}
package resource.play.impl;

@Transactional
public class PlayResourceImpl implements PlayResource {

  private final PlayService service;

  @Inject
  public PlayResourceImpl(PlayService service) {
    this.service = service;
  }

  @Override
  Response play(SceneId id) {
    service.play(id);
    return Response.ok(); // 今回はとりあえず常に OK を返しておく
  }

}

コード例としてはこんな感じです。あくまで一例なのでもっと良く出来るとは思いますが, 何となく Service を使ったらこうなるというイメージが伝わると良いのですが……。

@monzou
Copy link
Author

monzou commented Jan 2, 2014

追記ですが Play と ActorProvider という名前はあんまり良くないですね。Play は演劇ぐらいの名前がちょうど良いですね。ActorProvider に関しては単に配役のことなので Casting とかかな。あと Romeo と Juliette の台詞がベタ書きですがもしこれが本当のアプリケーションならコンフィギュレーションに切り出すでしょう。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment