SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Akkaで学ぶアクターモデル入門

アクターモデルによる、APIやデータベースに振り回されないアプリケーション設計と実装

Akkaで学ぶアクターモデル入門 第4回

設計をもとにソースコードに落とし込む

 この記事で紹介しているソースコードは、記述量を抑えるためチケット在庫アクターに関する部分のみ抜き出して書いています。完全な形でのソースコードはGitHubのレポジトリに置いてあります。アプリケーションを走らせるための案内を載せたREADMEもありますので、ご興味のある方はぜひそちらもご覧ください。

 最初にアクターが受け取るメッセージ型を定義します。チケット在庫アクターを表すクラス、TicketStockの中に、interface Messageとともに具体的なメッセージ型の1つ目としてStartSaleを定義しましょう。

import akka.actor.typed.*;
import akka.actor.typed.javadsl.*;

public class TicketStock {
  public interface Message {}

  //販売開始メッセージ
  //public: TicketStockの外からStartSaleメッセージを投げる
  //static: TicketStockクラスのインスタンスとは紐付かない
  //final:  メッセージ型を継承させない
  public static final class StartSale implements Message {
    //イミュータブルなメッセージなのでfinal
    public final int ticketId;
    public final int stockQuantity;

    public StartSale(int ticketId, int stockQuantity) {
      this.ticketId = ticketId;
      this.stockQuantity = stockQuantity;
    }
  }
  //... StartSale以外のメッセージ型は後ほど定義
}

 メッセージ型を表すクラスは全てメンバーにfinalがついてイミュータブルなメッセージとして実装されていることに注意してください。Akkaではメッセージをイミュータブルにするのは非常に重要で、そうしないと非同期処理の安全性を保てません。同様に他のメッセージ型も定義します。行数を減らして見やすくするため、コンストラクタは省略しています。

//販売期間終了メッセージ
public static final class EndSale implements Message {
  public final int ticketId;
  //... コンストラクタは省略
}

//チケット購入メッセージ
public static final class PurchaseTicket implements Message {
  public final int userId;
  public final int ticketId;
  public final int purchaseQuantity;
  //チケット購入リクエストの最初の送信元はレスポンスを期待する、
  //PurchaseResponse型の定義は後ほど
  public final ActorRef sender;
  //... コンストラクタは省略
}

PurchaseTicketメッセージにはsenderというメンバーが存在しますが、これはシーケンス図の左端にあった最初の送信元を意味します。チケット購入処理が終わったらsenderにチケット購入が成功したか失敗したかを表すレスポンスを送り返さなくてはなりません。

public interface PurchaseResponse {}

public static class PurchaseResponseFailure implements PurchaseResponse {
  public final String ticketId;
  public final int purchaseQuantity;
  public final String errorMessage;
  //... コンストラクタは省略
}

public static class PurchaseResponseSuccess implements PurchaseResponse {
  public final int purchaseId;
  public final String ticketId;
  public final int purchaseQuantity;
  //... コンストラクタは省略
}

 次にアクターの状態を型で表現します。これらもTicketStockクラスの中にinterface Stateとそれを実装する型として定義しましょう。状態はアクター外部には晒さないので、privateにしています。

public class TicketStock {
  //...メッセージの型のソースコード
  //...

  // 状態はアクター外部に晒さないのでprivate
  private interface State {}


  // 販売開始前状態を表す型
  private static class BeforeSale implements State {
    final int ticketId;

    public BeforeSale(int ticketId) {
      this.ticketId = ticketId;
    }
  }


  // 販売中状態を表す型
  private static class StockAvailable implements State {
    final int ticketId;
    final int stockQuantity;
    final int nextPurchaseId;
    final Map purchaseRecords;
    //... コンストラクタは省略
  }

  // 売り切れ状態を表す型
  private static class OutOfStock implements State {
    final int ticketId;
    final int nextPurchaseId;
    final Map purchaseRecords;
    //... コンストラクタは省略
  }

  // 販売期間終了状態を表す型
  private static class SaleEnded implements State {
    final int ticketId;
    final Map purchaseRecords;
    //... コンストラクタは省略
  }
}

 purchaseRecordsメンバーは、Mapを使って誰がどのチケットを購入したかという履歴を保存しています。1件ごとの購入履歴は以下のPurchaseRecordを使っています。

private static class PurchaseRecord {
  final int ticketId;
  final int userId;
  final int purchaseQuantity;

  public PurchaseRecord(int ticketId, int userId, int purchaseQuantity) {
    this.ticketId = ticketId;
    this.userId = userId;
    this.purchaseQuantity = purchaseQuantity;
  }
}

 メッセージと状態を定義し終わったら、状態遷移を定義します。ここからは少し慣れが必要な部分ですが、状態遷移図を事前に描いていれば、設計を素直にソースコードに落とし込めるはずです。BeforeSale、StockAvailable、OutOfStock……という状態の型ごとにそれぞれ状態遷移を定義するメソッドbeforeSale、stockAvailable、outOfStock……を書いていきます。

public class TicketStock {
  //...
  //...
  public static Behavior beforeSale(
    ActorContext context,
    BeforeSale beforeSaleState
  ) {//...実装は後ほど紹介
  }

  public static Behavior stockAvailable(
    ActorContext context,
    StockAvailable stockAvailableState
  ) { //...実装は後ほど紹介
  }

  public static Behavior outOfStock(
    ActorContext context,
    OutOfStock outOfStockState
  ) { //...実装は後ほど紹介
  }

  public static Behavior saleEnded(
    ActorContext context,
    SaleEnded saleEndedState
  ) { //...実装は後ほど紹介
  }
}

 まずはbeforeSaleについて、状態遷移図をよりソースコードに落とし込みやすくするため、分解してみましょう。

 これはBeforeSale状態での状態遷移を表すので「BeforeSale状態でStartSaleメッセージを受け取るとStockAvailable状態に遷移する」というのをコードにするとこうなります。

public static Behavior beforeSale(
  ActorContext context,
  BeforeSale beforeSaleState
) {
  return Behaviors.receive(Message.class)
    .onMessage(StartSale.class, message -> {
      return stockAvailable(
        ActorContext context,
        new StockAvailable(beforeSaleState.ticketId, message.stockQuantity, 1)
      );
    })
    .build();
}

 戻り値型になっているBehaviorというクラスがアクターの状態遷移を表します。このメソッドもstaticであり、TicketStockのインスタンスを生成していないことに注意してください。

 オブジェクト指向スタイルと関数型スタイル

 Akkaを用いたアクタークラスの定義方法はオブジェクト指向スタイルと関数型スタイルの2つがあり、公式ドキュメントでもその違いが解説されています。オブジェクト指向スタイルではAbstractBehaviorを継承する形でアクターを定義しますが、本記事で採用した関数型スタイルでは、Behavior型を返すstaticなメソッドにより状態遷移を定義する方法を用いています。

 stockAvailableで定義する状態は少し複雑で、PurchaseTicket、EndSaleという2つの異なる型メッセージを受け取る可能性があります。

public static Behavior stockAvailable(
  ActorContext context,
  StockAvailable stockAvailableState
) {
  return Behaviors.receive(Message.class)
    .onMessage(PurchaseTicket.class, message -> {
      //...実装は後ほど紹介
    })
    .onMessage(EndSale.class, message -> {
      //...実装は後ほど紹介
    })
    .build();
}

 最初にPurchaseTicketの処理について、チケット在庫数を超える販売はできないことに注意するとこのようになります。チケット購入が成功でも失敗でもsenderのtellメソッドを使ってレスポンスを返しています。

.onMessage(PurchaseTicket.class, message -> {
  if (!stockAvailableState.canPurchaseTicket(message.purchaseQuantity)) {
    message.sender.tell(
      new FailedPurchase(
        message.ticketId,
        message.purchaseQuantity,
        "在庫数を超えるチケットは購入できません"
      )
    );
    return Behaviors.same();
  } else {
    message.sender.tell(
      new SuccessfulPurchase(
        stockAvailableState.nextPurchaseId,
        message.ticketId,
        message.purchaseQuantity
      )
    );

    var nextState = stockAvailableState.purchaseTicket(
      message.userId,
      message.purchaseQuantity);

    if (nextState instanceof OutOfStock) {
      return outOfStock(
        context,
        (OutOfStock) nextState
      );
    } else { //OutOfStockではければ必ずStockAvailable
      return stockAvailable(
        context,
        (StockAvailable) nextState
      );
    }
  }
})

 StockAvailableのpurchaseTicketというメソッドを呼び出していますが、それは以下のようにチケット在庫量によって戻り値のStateが変わるメソッドです。

private static class StockAvailable implements State {
  //...
  public State purchaseTicket(int userId, int purchaseQuantity) {
    //実際にはpurchaseRecordsを更新する処理を書かなくてはいけない
    final Map updatedPurchaseRecords = ...;


    if (this.stockQuantity == purchaseQuantity) {
      return new OutOfStock(
        this.ticketId,
        this.nextPurchaseId + 1,
        updatedPurchaseRecords);
    } else {
      return new StockAvailable(
        this.ticketId,
        this.stockQuantity - purchaseQuantity,
        this.nextPurchaseId + 1,
        updatedPurchaseRecords);
    }
  }
}

 さらにチケット販売期間が終了しEndSaleメッセージを受け取ると、チケット在庫の量に関わらずSaleEnded状態に遷移します。

.onMessage(EndSale.class, message ->
  saleEnded(
    context,
    new SaleEnded(
      stockAvailableState.ticketId,
      stockAvailableState.purchaseRecords
    )
  )
)

 残りのoutOfStockとsaleEndedについてはこうなります。

private static Behavior outOfStock(
  ActorContext context,
  OutOfStock outOfStockState
) {
  return Behaviors.receive(Message.class)
    .onMessage(EndSale.class, message ->
      saleEnded(
        context,
        new SaleEnded(
          outOfStockState.ticketId,
          outOfStockState.purchaseRecords
        )
      )
    )
    .onMessage(PurchaseTicket.class, message -> {
      message.sender.tell(new PurchaseResponseFailure(
        message.ticketId,
        message.purchaseQuantity,
        "チケットは売り切れです")
      );
      return Behaviors.same();
    })
    .build();
}

private static Behavior saleEnded(
  ActorContext context,
  SaleEnded saleEndedState
) {
  return Behaviors.receive(Message.class)
    .onMessage(PurchaseTicket.class, message -> {
      message.sender.tell(new PurchaseResponseFailure(
        message.ticketId,
        message.purchaseQuantity,
        "チケット販売期間は終了しています")
      );
      return Behaviors.same();
    })
    .build();

 ここまででチケット在庫アクターTicketStockの実装は完了です。GitHubのレポジトリにある完全な形でのソースコードには簡単なシナリオを含むMainクラスがあるので、チケット在庫アクターやチケット購入者アクターがどのようにメッセージを送り合い、状態を遷移させながら、全体としてのアプリケーションが動くか確認して見てください。

次のページ
Web APIとアクターの接続

この記事は参考になりましたか?

Akkaで学ぶアクターモデル入門連載記事一覧

もっと読む

この記事の著者

リチャード 伊真岡(リチャード イマオカ)

 大学卒業後、9年間証券会社にてプログラマ兼技術サポートとして勤務。その後いくつかの企業でバックエンド・エンジニアや技術広報などを務める。  プライベートでは過去にScala/JavaのOSSであるakkaへ貢献。 Twitter:@RichardImaokaJP

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/12642 2020/08/05 11:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー