52
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ワイの書いたクソコードを、よめ太郎さんがお焚き上げ(リファクタリング)してくれるお話です。

とある休日

娘(9歳)「パパ、単一責任原則って何?」

ワイ「おぉ、娘ちゃん」
ワイ「なんや、難しい言葉を知ってるんやな」

娘「うん!学校でプログラミングの授業があったの」

ワイ「そうなんか」
ワイ「ほな、パパが単一責任原則について説明してあげるでぇ〜」

娘「やった〜」

ワイ「単一責任原則っていうんは──」
ワイ「単一のクラスに全ての責任を持たせる!」
ワイ「──っていう原則や!」

娘「へぇ〜」

ワイ「ほら、パパが作った『単一責任原則』を完璧に実現したクラスを見てみ?」

ワイのかんがえた最強クラス

TypeScript
class DoEverything {
  constructor(
    /* 省略 */
  ) {}

  async doAnything(
    shouldCreateUser: boolean,
    shouldSendEmail: boolean,
    shouldValidateAddress: boolean,
    shouldCheckPayment: boolean,
    shouldGeneratePDF: boolean,
    shouldNotifySlack: boolean,
    shouldUpdateStatus: boolean,
    shouldLogToDatabase: boolean,
    shouldRunBatchJob: boolean,
    isTestMode: boolean,
  ) {
    /* 実際の処理 */
  }

  /* 省略 */
}

娘「へぇ〜、DoEverythingクラスかぁ」
娘「なんだか凄そう!」

ワイ「せやでぇ」
ワイ「ほんで、その中のdoAnythingメソッドはこんな感じや!」

TypeScript
async doAnything(
  shouldCreateUser: boolean,
  shouldSendEmail: boolean,
  shouldValidateAddress: boolean,
  shouldCheckPayment: boolean,
  shouldGeneratePDF: boolean,
  shouldNotifySlack: boolean,
  shouldUpdateStatus: boolean,
  shouldLogToDatabase: boolean,
  shouldRunBatchJob: boolean,
  isTestMode: boolean
): Promise<void> {
  if (shouldCreateUser && (shouldSendEmail || shouldValidateAddress)) {
    await this.db.createUser();
    if ((shouldNotifySlack && shouldCheckPayment) || (shouldRunBatchJob && !isTestMode)) {
      await this.slack.notify();
      if (shouldGeneratePDF && shouldUpdateStatus) {
        await this.pdfGenerator.generate();
      }
    }
  } else if (shouldSendEmail || shouldCheckPayment) {
    if (shouldValidateAddress && (!shouldLogToDatabase || shouldRunBatchJob)) {
      await this.validateAddress();
    }
    if (shouldLogToDatabase && (shouldUpdateStatus || isTestMode)) {
      await this.db.logAction();
    }
  } else if (shouldRunBatchJob && (!shouldCheckPayment || shouldLogToDatabase)) {
    if (shouldGeneratePDF && (!shouldUpdateStatus || isTestMode)) {
      await this.pdfGenerator.generate();
    }
  }

  /* さらに条件分岐が続く */
}

ワイ「このメソッド一個で、なんでもできるんや!すごいやろ?」

娘「へぇ〜、じゃあこんな感じで使うの?」

TypeScript
const doEverything = new DoEverything(db, mailer, slack, pdf);
await doEverything.doAnything(true, true, true, true, false, true, true, true, false, false);

ワイ「せや」

娘「なるほど〜、truefalseを組み合わせて、やりたいことを指示してるんだね!」

ワイ「そうそう、賢いやないか」

娘「てへへ」

よめ太郎「おいおい・・・」

ワイ「あ、よめ太郎!」
ワイ「ちょうどええとこに来たな」
ワイ「娘ちゃんに単一責任原則について教えてたんや」

よめ太郎「いやいやいや、それ完全に間違ってるで」
よめ太郎「っていうか、クソコードすぎるやろ」

ワイ「ファッ!?」

娘「どうして?」

DoEverythingクラスのデメリット

よめ太郎「まずこのコード、誰が読めるねん
よめ太郎「truefalseをいっぱい組み合わせてプログラミングするって」
よめ太郎「もうそれ、ほぼ機械語やないかい」

ワイ「ぐぬぬ・・・」

よめ太郎「それにな?」
よめ太郎「そのクラス、修正したい時どうなると思う?」

ワイ「修正?」

よめ太郎「例えば、メールの送り方を変えたくなったとき」

娘「あ!わたし分かる!」
娘「パパのクラスだと、メール送信の部分を変えたら・・・」
娘「他の部分も全部確認しないといけないよね?だって、全部繋がってるから」

よめ太郎「その通りやで」
よめ太郎「それに、if文も複雑やし、3重になっとるし」
よめ太郎「修正しようとしたら、バグらせる自信しかないわ

ワイ「Oh・・・」

よめ太郎「他にも問題あるで」
よめ太郎「例えば、新しい機能を追加したくなったらどうする?」

娘「えっと・・・booleanの引数を増やすの?」

よめ太郎「そう。で、今度は何個の組み合わせになる?」

娘「今は10個だから・・・210乗で、1024通り!」
娘「新しいの増えたら・・・211乗で、2048通り!?」

よめ太郎「そう。で、全部テストせなあかんのやで?」

ワイ「まぁ、そうやな・・・」

よめ太郎「ほな、明日から追加で1024個のテストケース書くんか?」

ワイ「」

よめ太郎「しかも元からあるテストケースも修正必要やで?」

ワイ「」

よめ太郎「このクラスのせいで開発が止まってしまうわ」

ワイ「ぐぬぬ・・・」

よめ太郎「単一責任原則どころか、単一障害点になってしまってるやん」

ワイ「なるほど・・・それはアカンな・・・」

娘「じゃあ、どうすればいいの?」

よめ太郎さんによる、お焚き上げ(リファクタリング)開始

よめ太郎「まず、それぞれの責任を分けた小さなクラスを作るんや」

TypeScript
// メール送信だけを担当するクラス
class EmailService {
  async sendEmail(to: string, subject: string, body: string) {
    // メール送信のロジックだけを持つ
  }
}

// 住所検証だけを担当するクラス
class AddressValidator {
  async validate(address: string): Promise<boolean> {
    // 住所検証のロジックだけを持つ
  }
}

// ユーザー作成だけを担当するクラス
class UserCreator {
  async createUser(userData: UserData) {
    // ユーザー作成のロジックだけを持つ
  }
}

// PDFの生成だけを担当するクラス
class PDFGenerator {
  async generatePDF(data: PDFData) {
    // PDF生成のロジックだけを持つ
  }
}

娘「それぞれのクラスが一つのことだけをやってるんだね」

よめ太郎「そうや!こうすると、それぞれのクラスは──」

  • テストが書きやすい
  • 修正した際の影響範囲が狭い
  • 再利用もできる

よめ太郎「──っていうメリットがあるんや」

ワイ「でも、これらのクラスをどうやって使うんや?」
ワイ「小さなバラバラのクラスを組み合わせて使うの、面倒やん?」

UseCase層も作る

よめ太郎「そこで次は、UseCase層を作るんや」

TypeScript
class UserRegistrationUseCase {
  constructor(
    private emailService: EmailService,
    private addressValidator: AddressValidator,
  ) {}

  // ユーザーの関心事をそのままメソッド名に!
  async register(email: string, address: string) {
    // 裏側の複雑な処理は隠蔽
    if (!await this.addressValidator.validate(address)) {
      throw new Error('住所が違うよ!');
    }
    await this.emailService.sendEmail(email, 'ようこそ!');
  }
}

娘「そっか」
娘「細かなクラスを組み合わせて」
娘「ユーザーさんの関心事に沿った名前をつけてあげるんだね」

よめ太郎「こうすると、使う側はこんな感じになるんや」

TypeScript
// ユーザーの視点に合わせたシンプルな使い方
const useCase = new UserRegistrationUseCase(emailService, addressValidator);
await useCase.register("test@example.com", "東京都...");

娘「わぁ、すっきり!」
娘「コードと睨めっこしながら沢山のtruefalseを渡す必要もないんだね!」

ワイ「ほんまや」
ワイ「これなら使う側は、中の仕組み知らんでも使えそうやな」

よめ太郎「せや」
よめ太郎「単一責任の部品を組み合わせて、ユーザー目線の機能を作るんや」

ワイ「なるほど・・・」

娘「パパの最強クラスは、いろんなことを詰め込みすぎて、かえって使いにくなってたんだね・・・」

まとめ:クソデカクラス vs 細かいクラス

よめ太郎「最後にまとめて行くで」

クソデカクラス

👎 デメリット

  • テストケースが組み合わせ爆発
  • 一部分の修正が全体に影響する
  • 新機能追加のたびに引数が増える
  • コード全体を理解してないと修正できない
  • 再利用が難しい(全部セットでしか使えない)

細かいクラス

👍 メリット

  • テストコードが書きやすい(機能ごとに独立)
  • 修正の影響範囲が明確
  • 細かく命名されていて、コードの理解がしやすい
  • 新機能を追加しやすい
  • 再利用性が高い

UseCase層の役割

  • 細かいクラスを適切に組み合わせる
  • ユーザーの関心事に合わせたインターフェースを提供
  • 内部の複雑さを隠蔽
  • 処理の順序を制御

娘「パパ、分かった?」

ワイ「うん・・・最強クラスを作ろうとして、かえって最弱になってたわ」

よめ太郎「そうそう」
よめ太郎「単一責任の『単一』は、『一つのクラスで全部やる』んやなくて」

一つのクラスは一つの責任だけ

よめ太郎「ってことやったんや」

ワイ「ありがとう、勉強になったやで!」

〜おしまい〜

新しい記事もよろしくやで😃

52
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?