はじめに

お久しぶりです!
キャスレーコンサルティングの松岡です。

最近、甘いものを食べ過ぎてお腹が少し気になってきました。
Javaと同じように、お腹の脂肪もガベージコレクションで開放してくれませんかねー・・・。

冗談はさておき
前回は、じゃんけんAPIの外部設計の一部をお話しました。
(前回の記事:じゃんけんAPI開発から学ぶアプリケーションアーキテクチャ)

なので、今回は内部設計を進めていこうと思いますが
ボリュームの関係上、じゃんけんAPIの1機能に絞り実装部分を省略して説明致します。

DB物理設計

DB物理設計に入る前に
前回用意できなかったER図を「A5:SQL Mk-2」という、フリーのDB参照ツールを使用して
簡単にですが、作成してみました!

ER図

今回重要となるテーブルを、1つだけ紹介させてください。

じゃんけん成績テーブル

ここでのポイントは3つあります。

①スコア番号(score_no)のデフォルト値にシーケンスオブジェクトを設定している事
 ・これによりレコード登録時は、自動で採番されるようになります。
  (アプリケーション側で採番するのはめんどくさいので、シーケンスを使いました)

②行バージョン(row_version)のデフォルト値に加工処理を設定している事
 ・一意になるような値を生成しております。
  (流れ:現在のタイムスタンプ取得 → 文字列化 → ハッシュ値に変換 → 暗号化 → その値を設定)
 ・楽観ロックの判断に使用します!

③テーブルに対してトリガーファンクションを登録している事
 ・このテーブルに対して”INSERT”または”UPDATE”を行った場合、
  「作成日」「更新日」「行バージョン」を自動更新するようになっています。
 ・また”INSERT”や”UPDATE”の前に行いたいので、トリガータイミングはBEFOREに設定しています。

以下がDDL全体となります。

じゃんけん成績テーブル(DDL)

CREATE TABLE jankendb.tt_janken_score
(
    score_no integer NOT NULL DEFAULT nextval('jankendb.sq_score_no'::regclass),
    client_hand character(1) COLLATE pg_catalog."default" NOT NULL,
    server_hand character(1) COLLATE pg_catalog."default" NOT NULL,
    result character(1) COLLATE pg_catalog."default" NOT NULL,
    user_id character varying(32) COLLATE pg_catalog."default" NOT NULL,
    create_date timestamp without time zone NOT NULL,
    create_user character(32) COLLATE pg_catalog."default" NOT NULL,
    update_date timestamp without time zone,
    update_user character(32) COLLATE pg_catalog."default",
    row_version character varying(256) COLLATE pg_catalog."default" NOT NULL DEFAULT encode(digest(to_char(now(), 'YYYYMMDDHH24MISS.SSS'::text), 'sha256'::text), 'hex'::text),
    CONSTRAINT pk_tt_janken_score PRIMARY KEY (score_no)
)
WITH (
    OIDS = FALSE
)
TABLESPACE pg_default;

ALTER TABLE jankendb.tt_janken_score
    OWNER to postgres;
COMMENT ON TABLE jankendb.tt_janken_score
    IS 'じゃんけん成績テーブル';
COMMENT ON COLUMN jankendb.tt_janken_score.score_no
    IS 'スコア番号';
COMMENT ON COLUMN jankendb.tt_janken_score.client_hand
    IS 'クライアント側出し手';
COMMENT ON COLUMN jankendb.tt_janken_score.server_hand
    IS 'サーバー側出し手';
COMMENT ON COLUMN jankendb.tt_janken_score.result
    IS '結果';
COMMENT ON COLUMN jankendb.tt_janken_score.user_id
    IS 'ユーザーID';
COMMENT ON COLUMN jankendb.tt_janken_score.create_date
    IS '作成日';
COMMENT ON COLUMN jankendb.tt_janken_score.create_user
    IS '作成者';
COMMENT ON COLUMN jankendb.tt_janken_score.update_date
    IS '更新日';
COMMENT ON COLUMN jankendb.tt_janken_score.update_user
    IS '更新者';
COMMENT ON COLUMN jankendb.tt_janken_score.row_version
    IS '行バージョン';

CREATE TRIGGER tg_tt_janken_score
    BEFORE INSERT OR UPDATE 
    ON jankendb.tt_janken_score
    FOR EACH ROW
    EXECUTE PROCEDURE jankendb.tf_date_updater();

日付更新トリガー(ファンクション)

CREATE FUNCTION jankendb.tf_date_updater()
    RETURNS trigger
    LANGUAGE 'plpgsql'
    COST 100
    VOLATILE NOT LEAKPROOF 
AS $BODY$begin
  -- DML実行がINSERTだった場合:「作成日」「更新日」に現在のタイムスタンプを設定
  if (tg_op = 'INSERT') then
    new.create_date := current_timestamp;
    new.update_date := current_timestamp;
  else 
  -- DML実行がUPDATEだった場合:「更新日」に現在のタイムスタンプを設定、
  --                         「行バージョン」には現在のタイムスタンプを基に算出したユニークな値を設定
  --                          (現在のタイムスタンプ取得→文字列化→ハッシュ値に変換→暗号化→その値を設定)
    if (tg_op = 'UPDATE') then
      new.update_date := current_timestamp;
      new.row_version := encode(public.digest(old.row_version ||
	                               to_char(current_timestamp, 'YYYYMMDDHH24MISS.SSS'),'sha256'),'hex');
    end if;
  end if;
  return new;
end;$BODY$;

続いて、クラス設計へ移りましょう!

クラス設計

クラス図を見ながら、仕組みを簡単に解説していきます!
クラス図作成には、Eclipseプラグインの「AmaterasUML」を使用しております。

コントローラー部分

クラス図

この「Endpoint」クラスが、アプリケーションの最初に呼び出されるエントランスとなります。

getとpostの2つメソッドがありますが、今回呼ばれるのはpostです。
(getは、今後ヘルスチェックを実装する際に呼び出されることを考慮して作っていますが、なくても大丈夫です)

postメソッドの引数には、
 ・リクエストのJSON文字列が設定される「request」
 ・要求されたURIのhttp://localhost:8080/Janken/{URIのリソース名}/の{URIのリソース名}の値が設定される「apiId」
が定義されています。

postメソッドが呼ばれた後に、
「ServiceDispatcher」インターフェースのdispatchメソッドを呼び出します。

実装クラスは「BasicServiceDispatcher」クラスで、
DIされたServiceクラスのインスタンスがMapの型で登録されているので
apiIdをキーにそこからインスタンスを取得し、Serviceクラスを実行します。

ServiceとRepository部分

クラス図

今回、Serviceの実装には「Template Method」というデザインパターンの1つの考え方を踏襲しております。

■Template Methodとは
“スーパークラスで処理の枠組みを定め、サブクラスでその具体的内容を定める”というものです。
今回の例で言うと、Serviceの基底処理は 以下のフローで処理を行っています。
 1) JSON文字列にパース
 2) バリデーション
 3) 固有のService処理
 4) Javaオブジェクトへマッピング

この「固有のService処理」だけをサブクラスでオーバーライドさせ、スーパークラスから呼び出します。

すると、実行時に固有のService処理だけが任意のサブServiceクラスに切り替わり、
シンプル且つ保守性の高い作りにすることができます!
※前回のブログのアクティビティ図でバリデーションは表現されていましたが
 業務ロジックと直接関係のない低レベルな処理(Javaオブジェクトへマッピングなど)は
 今回補足として追加しております。

以下、箇条書きで説明

●Serviceインターフェース
 ・外部クラスへの提供機能の制限

●BasicService抽象クラス
 ・基底となる処理の動きを制御
 ・呼び出されるのはexecuteメソッド
 ・各Serviceで共通の処理を局所化(保守性の向上)
 ・各Service固有のリクエスト/レスポンス情報をジェネリクスで定義
  →基底となる処理の動きを制御を実現
   (パース処理などをサブクラスが意識しなくてもよくなる)

●サブServiceクラス(例:BattleServiceクラス)
 ・最初にvalidateメソッドが実行され入力値チェックで引っかかった場合、
  親クラスのBasicServiceにthrowされエラーレスポンスを生成し返却
 ・validateメソッドで問題がなかった場合、executeメソッドが実行
 ・処理の内容はアクティビティ図と同様
  (テーブル情報の照会は、MultiJankenDaoクラスから取得)

●MultiJankenDaoクラス
 ・各DAOクラスをコンポジションで保持し、提供機能を当クラスで制限
  (DBのテーブル単位でDAOクラスが存在しているため、
   今回は1つ窓口クラスから各DAOを呼び出すように実装しています)
 ・一応メソッドの命名規則でレコード1つのみを照会するメソッドには「findXXX」、
  レコードが複数照会できるメソッドには「queryXXX」と命名

ロジック(じゃんけんの戦略計算)部分

クラス図

API側のじゃんけんの出し手を決めているのが、Strategyクラスになります。

DBの「じゃんけん戦略ユーザーマスタ」から登録されている戦略を取得して、それに対応するStrategyサブクラスを実行します。

DTO部分

クラス図

APIのクライアントと受け渡しするJSONの定義クラス(Bean)です。

CommonRequestDtoが共通のリクエスト情報で、CommonResponseDtoが共通のレスポンス情報となり
APIの機能固有の情報は、これらのジェネリクスで表現しています。

呼び出しイメージ

以下は、Chromeの拡張機能「Postman」というAPIテストツールの画面になります。

リクエスト

こちらを送信すると・・・

レスポンス

正しく返却されましたね!!

最後に

今回全部紹介することができませんでしたが、
基本設計からAPI応答まで、開発することが出来ました!

今後も、何か学んだことがあれば
こうやってアウトプットしていきたいですね!

最後までお読みいただき、ありがとうございました。

また今回ご紹介したAPIのソースはGitHubで公開しておりますので
以下リンクからご参照ください。

GitHub

じゃんけんAPIプロジェクト

松岡 一也
CSVIT事業部 IT(インテグレーションテック)部 松岡 一也
甘いの大好き6年目エンジニア