著者:しょっさん
ソフトウエアを正しく作るために、エンジニアたちはどんなことを知らなければならないでしょうか。実際のコードを使って、より良くしていくためのステップを考えてみましょう。第8回は、第1回で述べた「クリーンアーキテクチャ」に習って、ロジックと、フレームワークやライブラリ、その他の処理を分離します。
シェルスクリプトマガジン Vol.68は以下のリンク先でご購入できます。
図1 経費精算申請を処理しているコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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」オブジェクト
1 2 3 4 5 6 |
class User { id: number; name: string; salaray: number; } |
図5 「common/index.ts」ファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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」ファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
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」ファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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」ファイル
1 2 3 4 5 6 7 8 9 10 11 12 |
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」ファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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」ファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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」ファイル
1 2 3 4 5 6 7 8 9 10 11 12 |
// 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」ファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 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 ); |