前置き
私が所属しているチームの輪読会で『良いコード/悪いコードで学ぶ設計入門』が選ばれました。
メンバーがそれぞれ章を分担して毎週発表をするスタイルになります。
書籍に書いてあることではなく、自身の言葉で説明要約するために、個人のアウトプットとして本記事を作成しました。
また本記事で利用しているコードは、原則的には書籍の内容とは別物 + TypeScript
を利用しています。
6章 条件分岐
if
やswitch
による条件分岐が複雑になると、以下のようなデメリットが出現します。
- コード全体の見通しが悪くなり、理解に時間がかかる
- 仕様変更時にバグを発生させやすくなる
if, elseは早期returnする
典型的なif文のネストです。
他の実装者や将来の己自身がこれを見た時、コードの読み解きから始まり仕様変更に時間がかかったり、新たなバグを埋め込む可能性すらあり得ます。
if (/* 条件式1 */) {
if (/* 条件式2 */) {
// 数行の処理
return { ... };
} else if (/* 条件式3 */) {
// 数行の処理
return { ... };
} else {
return null;
}
} else {
throw new Error();
}
【リファクタリング】
ifのカッコを外して処理を別の関数に分けると以下のようになります。
分岐条件と実行する内容が明確に理解できるようになりました。
※ otherFunction() は意味のある名付けにしてください
if (/* !条件式1 */) throw new Error(); // 条件式1の否定形でelseとしエラーを発生
// 以降は条件式1を満たす前提で実行される
if (/* 条件式2 */) return otherFunction2(); // 条件式2の処理部分を別関数化
if (/* 条件式3 */) return otherFunction3(); // 条件式3の処理部分を別関数化
return null;
同じ条件式の条件分岐を1箇所にまとめる
同じ条件式を複数の場所で使いたいことがあるかと思います。
動作はしますが、今後もユーザータイプを判別して処理を分岐したい仕様がふえれば、同様の条件式が無造作に増えていくことになります。
const userAccount = {
name: "user1",
type: "standard",
};
function showMovie() {
if (userAccount.type === "standard") {
/* 通常ユーザーは動画広告を表示する処理 */
}
/* 本来の動画再生処理 */
}
function downloadMovie() {
if (userAccount.type === "standard") {
console.error("通常ユーザーは動画ダウンロードできません");
return;
}
/* 動画ダウンロードは有料ユーザーのみ可能 */
}
これは単一責任選択の原則
に違反しています。
同じ条件式は使いまわさず、条件分岐による責任を単一にしましょうという原則です。
【リファクタリング】
initMoviePlayer() 単一でユーザータイプを判別して、別々のインスタンスを返すことで見通しの良い構造になりました。
仕様変更や別のユーザータイプが追加された際にも、クラス実装とinitMoviePlayer()で初期化したインスタンスを渡すだけで済みます。
このようなinterfaceを利用して処理をまとめて切り替える処理をストラテジパターン
と言います。
※ strategy => 戦略
type UserAccount = {
name: string;
type: UserType;
};
type UserType = "standard" | "premium";
interface IMoviePlayer {
userType(): UserType;
showMovie(): void;
downloadMovie(): void;
}
class StandardMoviePlayer implements IMoviePlayer {
userType(): UserType {
return "standard";
}
showMovie() {
/* 動画広告を表示してから本来の動画を再生する処理 */
}
downloadMovie() {
console.error("通常ユーザーは動画ダウンロードできません");
}
}
class PremiumMoviePlayer implements IMoviePlayer {
userType(): UserType {
return "premium";
}
showMovie() {
/* 動画再生処理 */
}
downloadMovie() {
/* 動画ダウンロード処理 */
}
}
const userAccount: UserAccount = {
name: "user1",
type: "standard",
};
function initMoviePlayer(account: UserAccount): IMoviePlayer {
const moviePlayers: IMoviePlayer[] = [
new StandardMoviePlayer(),
new PremiumMoviePlayer(),
];
const targetMoviePlayer = moviePlayers.find(
(moviePlayer) => moviePlayer.userType() === account.type
);
if (targetMoviePlayer === undefined)
throw new Error("MoviePlayerを初期化できませんでした");
return targetMoviePlayer;
}
条件を部品化してカスタマイズ可能にする
例えばWeb入力フォームに入力されたEmailアドレスが正しいかチェックをするとします。
正規表現や文字数のチェックを行って、結果を返す関数を実装してみました。
条件とエラーメッセージが全てベタ書きされてるので、再利用性や見通しも悪いです。
function validEmail(value: string) {
const errorMessages: string[] = [];
// example@example.com などの正規表現を設定
const isCurrentEmail = /.+/.test(value);
if (!isCurrentEmail)
errorMessages.push(`正しいEmailの形式ではありません`);
const isOverMinChar = 4 <= value.length;
if (!isOverMinChar)
errorMessages.push(`最小文字数は4です`);
const isLessMaxChar = value.length <= 20;
if (!isLessMaxChar)
errorMessages.push(`最大文字数は20です`);
const isValid =
isCurrentEmail &&
isOverMinChar &&
isLessMaxChar
;
return { isValid, errorMessages };
}
【リファクタリング】
各種条件とエラーメッセージの組み合わせで部品化を行います。
その部品化された条件をEmailValidator
でインスタンス化することによって、検証やエラーメッセージの取得を行えるようにしました。
また条件を追加削除する際もポリシー配列を変更するだけでよくなります。if文を多用したリファクタリング前のコードであれば、不要な箇所を消してifの条件を変える必要がありました。
interface IFormValidationPolicy {
isFulfilled(value: any): boolean;
errorMessage(): string;
}
class EmailRegPolicy implements IFormValidationPolicy {
isFulfilled(value: string): boolean {
// example@example.com などの正規表現を設定
return /.+/.test(value);
}
errorMessage(): string {
return `正しいEmailの形式ではありません`;
}
}
class MinCharPolicy implements IFormValidationPolicy {
private readonly minChar: number;
constructor(minChar: number) {
// ガード節
this.minChar = minChar;
}
isFulfilled(value: string | number): boolean {
return this.minChar <= String(value).length;
}
errorMessage(): string {
return `最小文字数は${this.minChar}です`;
}
}
class MaxCharPolicy implements IFormValidationPolicy {
private readonly maxChar: number;
constructor(maxChar: number) {
// ガード節
this.maxChar = maxChar;
}
isFulfilled(value: string | number): boolean {
return String(value).length <= this.maxChar;
}
errorMessage(): string {
return `最大文字数は${this.maxChar}です`;
}
}
interface IFormItemValidator {
readonly value: any;
readonly policies: IFormValidationPolicy[];
isFulfilled(): boolean;
errorMessages(): string[];
}
class EmailValidator implements IFormItemValidator {
readonly value: string;
readonly policies: IFormValidationPolicy[];
constructor(value: string) {
// ガード節
this.value = value;
this.policies = [
new EmailRegPolicy(),
new MinCharPolicy(4),
new MaxCharPolicy(20),
];
}
isFulfilled(): boolean {
return this.policies.every((policy) => policy.isFulfilled(this.value));
}
errorMessages(): string[] {
const errorPolicies = this.policies
.filter((policy) => !policy.isFulfilled(this.value));
const errorMessages = errorPolicies
.map((policy) => policy.errorMessage());
return errorMessages;
}
}
その他Badパターン
instanceof
せっかくinterface で実装しているのに、if文を多用することになったBadパターン。
条件分岐の削減どころか増やしてしまっていて意味がありません。
instanceof
の使い所としては外部ライブラリや非同期通信結果など、型の不明な場合に型ガード
として利用する以外には基本使わないというのが個人的見解です。
if (account instanceof StandardAccount) {
/* 処理 */
return;
}
if (account instanceof PremiumAccount) {
/* 処理 */
return;
}
可読性の落ちる条件切り替え引数
機能の切り替えにbooleanや識別用のIDなどの引数を利用している場合、メソッドの外部からは内部機能を想像することが難しくなるBadパターン。
内容が不鮮明なためメソッド内部を読み手が確認することになるでしょう。
第1引数にbooleanを渡して機能を切り替え
function showMovie(flag: boolean) {
if (!flag) {
/* 通常ユーザーは動画広告を表示する処理 */
}
/* 本来の動画再生処理 */
}
セレクトボックスの内容を0, 1, 2 などで管理していて、切り替えの条件式が各所で使われている + 対照表を見ないと内容を把握できないプロダクト
function showMovie(userType: number) {
if (userType === 0) {
/* 通常ユーザーは動画広告を表示する処理 */
}
/* 本来の動画再生処理 */
}
まとめ
オブジェクト指向の学習でinterface
が出てきた時に、使い方として絶対に欲しい内容でした。
初学でinterface
が出てきてもここまで説明はされないので、使い所がないもの
として記憶の隅に追いやられる人も多いんじゃないでしょうか。how
とwhy
, 何故使うかがセットになることで初めて理解できたと言えるでしょう。
タイトル通りになりますが、条件分岐を利用する時はまずinterface設計を試みることが重要になります。
まだ読了していませんが、日々の業務に活かせることも多く、すぐに業務で活用することができるようになりました。引き続き他の章も読んで身につけようかと思います。