関数設計:責任を委譲してテストしやすいコードにする
#設計 #テスト
クラス設計とかでも考え方は一緒。
この「責任委譲」というイメージを抽象的に持っておくと、どんな時でも一定数綺麗なコードを書けると思われる。
以下のコードに、何か「臭い」を感じ取れるか
code: ts
function calculateMonthlySalary(employee: Employee, attendance: Attendance): number {
// ...
}
interface Employee {
id: string;
baseSalary: number;
position: "staff" | "leader" | "manager" | "director";
yearsOfService: number;
hasDependent: boolean;
commuteCost: number;
}
interface Attendance {
workDays: number;
expectedWorkDays: number;
overtimeHours: number;
lateCount: number;
holidayWorkDays: number;
}
このまま進むと、Employee、Attendance関連の計算を、calculateMonthlySalary内で行う恐れあり
しかし、そうなるとコード内は複雑怪奇でテストなんてできなものではなくなっていく。
baseSalaryの検証処理(0以上チェック)をcalculateMonthlySalary内で直接やるかも(初学者は特に)
明らかにその状況は、多く処理をcalculateMonthlySalaryに任せすぎている。委譲すべき。
こういう時は、データを持つクラスを作ると良い。以下のように。
code: ts
class Employee {
constructor(
public id: string,
public baseSalary: number,
public position: "staff" | "leader" | "manager" | "director",
public yearsOfService: number,
public hasDependent: boolean,
public commuteCost: number
) {
this.validate();
}
private validate(): void {
if (this.baseSalary < 0) throw new Error("基本給は0以上");
if (!this.id) throw new Error("IDは必須");
}
getPositionAllowance(): number {
const allowances = {
director: 100000,
manager: 50000,
leader: 30000,
staff: 0
};
return allowancesthis.position;
}
getDependentAllowance(): number {
return this.hasDependent ? 10000 : 0;
}
getTenureAllowance(): number {
return Math.floor(this.yearsOfService / 5) * 5000;
}
}
さらにもっというと、Empolyee内の各属性も値オブジェクトにして検証処理などを委譲できる。
ここはバランスなので、どっちでもいい。値オブジェクトを作らずにEmpolyeeで完結しても良い
上記のようにすれば、calculateMonthlySalary内の処理が複雑になるのを防げます。
なお、classにこだわらなくてもいい。
interfaceと関数群で、凝集を作るのもあり。まぁ具体はなんでもいい。
ポイント
なんか匂ったら、データを持つクラスに処理を委譲することを検討しよう。