著者:しょっさん
ソフトウエアを正しく作るために、エンジニアたちはどんなことを知らなければならないでしょうか。実際のコードを使って、より良くしていくためのステップを考えてみましょう。第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
);