シェルスクリプトマガジン

Webアプリケーションの正しい作り方(Vol.68記載)

著者:しょっさん

ソフトウエアを正しく作るために、エンジニアたちはどんなことを知らなければならないでしょうか。実際のコードを使って、より良くしていくためのステップを考えてみましょう。第8回は、第1回で述べた「クリーンアーキテクチャ」に習って、ロジックと、フレームワークやライブラリ、その他の処理を分離します。

シェルスクリプトマガジン Vol.68は以下のリンク先でご購入できます。

図1 経費精算申請を処理しているコード

import { Request, Response, NextFunction } from "express";
import Express from "express";
import { Expense } from "../models/expense";
const router = Express.Router();

// POST 経費の入力
router.post("/", (req: Request, res: Response, next: NextFunction) => {
  Expense.create(req.body)
    .then((result) => {
      res.status(200).json(result);
    })
    .catch((err) => {
      console.log(err);
      res.status(400).json({ id: 20002, message: err });
    });
});

図3 「User Entity」オブジェクト

class User {
  id: number;
  name: string;
  salaray: number;
}

図5 「common/index.ts」ファイル

export enum approval_status {
  minimum,
  unapproved,
  approved,
  reject,
  reimburse,
  maximum,
}

// エンティティ用のオブジェクトの基本構成
export abstract class EntityObject<T> {
  protected props: T;

  protected constructor(props: T) {
    this.props = props;
  }
}

// プリミティブ型のビジネスルール実装のための基本構成
export abstract class PrimitiveObject<T> extends EntityObject<T> {
  get value(): T {
    return this.props;
  }
}

図6 「domains/expenseEntity.ts」ファイル

import { EntityObject, approval_status, PrimitiveObject } from "../common";

export const MAX_LENGTH = 64;
export const MAX_AMOUNT = 1000000;

// 費目名のルール
class Type extends PrimitiveObject<string> {
  static create(value: string): Type {
    if (value.length > MAX_LENGTH || value.length <= 0)
      throw new Error("費目名が長すぎるか、ありません");
    return new Type(value);
  }
}

// 承認コードのルール
class Approval extends PrimitiveObject<approval_status> {
  static create(value: approval_status = approval_status.unapproved): Approval {
    if (value <= approval_status.minimum || value >= approval_status.maximum)
      throw new Error("承認コードがおかしい");
    return new Approval(value);
  }
}

// 請求金額のルール
class Amount extends PrimitiveObject<number> {
  static create(value: number): Amount {
    if (value <= 0 || value >= MAX_AMOUNT)
      throw new Error("請求金額が範囲を超えている");
    return new Amount(value);
  }
}

// 経費精算で利用されるクラスの実態
interface IExpenseProps {
  id?: number | undefined;
  user_id: string;
  user_name?: string;
  date: Date;
  type: Type;
  description?: string | null;
  approval: Approval;
  amount: Amount;
}

// オブジェクトを構成する要素
export interface IExpenseValue {
  id?: number | undefined;
  user_id: string;
  user_name?: string;
  date: Date;
  type: string;
  description?: string | null;
  approval: approval_status;
  amount: number;
}

export class ExpenseEntity extends EntityObject<IExpenseProps> {
  constructor(props: IExpenseProps) {
    super(props);
  }

  set approval(status: approval_status) {
    this.props.approval = Approval.create(status);
  }

  static create(values: IExpenseValue): ExpenseEntity {
    return new ExpenseEntity({
      id: values.id,
      user_id: values.user_id,
      user_name: values.user_name,
      date: values.date,
      type: Type.create(values.type),
      description: values.description,
      approval: Approval.create(values.approval),
      amount: Amount.create(values.amount),
    });
  }

  public read(): IExpenseValue {
    return {
      id: this.props.id,
      user_id: this.props.user_id,
      user_name: this.props.user_name,
      date: this.props.date,
      type: this.props.type.value,
      description: this.props.description,
      approval: this.props.approval.value,
      amount: this.props.amount.value,
    };
  }
}

図7 「usecases/SubmitExpense.ts」ファイル

import { IExpenseRepository } from "./IExpenseRepository";
import { ExpenseEntity, IExpenseValue } from "../domains/expenseEntity";

export class SubmitExpense {
  private _expenseRepository: IExpenseRepository;

  constructor(expenseRepository: IExpenseRepository) {
    this._expenseRepository = expenseRepository;
  }

  execute(expense: IExpenseValue) {
    const e = ExpenseEntity.create(expense);
    return this._expenseRepository.store(e);
  }
}

図8 「usecases/IExpenseRepository.ts」ファイル

import { ExpenseEntity } from "../domains/expenseEntity";

export interface IExpenseRepository {
  findAllApproved(): Promise<ExpenseEntity[]>;
  findAllRejected(): Promise<ExpenseEntity[]>;
  findUnapproval(id: string): Promise<ExpenseEntity[]>;
  updateApproval(id: number, expense: ExpenseEntity): Promise<ExpenseEntity>;
  findById(id: number): Promise<ExpenseEntity>;
  update(expense: ExpenseEntity): Promise<ExpenseEntity>;
  store(expense: ExpenseEntity): Promise<ExpenseEntity>;
}

図9 「interfaces/ExpenseController.ts」ファイル

import { SubmitExpense } from "../usecases/SubmitExpense";
import { IExpenseValue } from "../domains/expenseEntity";
import { ExpenseRepository } from "./expenseRepository";

export class ExpenseController {
  async submitExpenseController(expense: IExpenseValue): Promise<IExpenseValue> {
    const expenseRepository = new ExpenseRepository();

    try {
      const usecase = new SubmitExpense(expenseRepository);
      const result = await usecase.execute(expense);
      return result.read();
    } catch (error) {
      throw new Error(error);
    }
  }
}

図10 「interfaces/ExpenseRepository.ts」ファイル

import { Expense } from "../../models/expense";
import { approval_status } from "../common";
import { ExpenseEntity } from "../domains/expenseEntity";
import { IExpenseRepository } from "../usecases/IExpenseRepository";

export class ExpenseRepository implements IExpenseRepository {
  findAllApproved(): Promise<ExpenseEntity[]> {
    return Expense.findAll({
      where: {
        approval: approval_status.approved,
      },
    }).then((results) => {
      return results.map((value, index, array) => {
        return ExpenseEntity.create(value);
      });
    });
  }
(略)
  store(e: ExpenseEntity): Promise<ExpenseEntity> {
    return Expense.create(e.read())
      .then((result) => {
        return ExpenseEntity.create(result);
      })
      .catch((err) => {
        throw new Error("請求処理が失敗しました");
      });
  }
}

図11 「expense.ts」ファイル

// POST 経費の入力
router.post("/", (req: Request, res: Response, next: NextFunction) => {
  const e = new ExpenseController();

  e.submitExpenseController(req.body!)
    .then((result) => {
      res.status(200).json(result);
    })
    .catch((err) => {
      res.status(400).json({ id: "20201", message: err });
    });
});

図13 「index.ts」ファイル

// API
app.use("/api/auth", auth);
app.use("/api/expense", Authorization.isAuthorized, expense);
app.use(
  "/api/payment",
  Authorization.isAuthorized,
  Authorization.isAccounting,
  payment
);
app.use(
  "/api/approval",
  Authorization.isAuthorized,
  Authorization.isBoss,
  approval
);