🌉

デザインパターン:Bridgeパターン

2024/09/06に公開

これは何?

独立した複数の次元を切り出したいけど多重継承されると困るから委譲しよう

何が嬉しいのか

問題

弊社のコンテンツには「広告 or 自社コンテンツ」「Mp4 or PNG」という2つのtypeがあるとします

abstract class Content {
  createdByType: "AD" | "ORIGINAL"
  fileType: "MP4" | "PNG"
}

class AdMp4Content extends Content {
  createdByType: "AD"
  fileType: "MP4"
}

class TokyoMp4Content extends Content {
  createdByType: "ORIGINAL"
  fileType: "MP4"
}

class AdPngContent extends Content {
  createdByType: "AD"
  fileType: "PNG"
}

class TokyoPngContent extends Content {
  createdByType: "ORIGINAL"
  fileType: "PNG"
}

入稿は全て社内ツールから行うし、プレイヤーも共通のものを使います

/** 社内ツール */
class InternalTool {
  async register(content: Content) {
      if (content.type === "AD") {
          await contentReportingService.register(content) // 広告主用レポーティングサービスに登録
      }
      await content.type === "AD" ? registerAdContent(content) : registerTokyoContent(content)
      return
  }
}

/** プレイヤー */
class Player {
  async play(content: Content) {
      // PNGはそのままでは再生できないのでmp4に変換
      return device.play(content.type === "PNG" ? convertToMp4(content) : content)
  }
}

ファイルの種類が増えたらどうなる?

abstract class Content {
    createdByType: "AD" | "ORIGINAL"
    fileType: "Mp4" | "PNG" | "JPEG"
}

class AdMp4Content extends Content {
    createdByType: "AD"
    fileType: "MP4"
}

class TokyoMp4Content extends Content {
    createdByType: "ORIGINAL"
    fileType: "MP4"
}

class AdPngContent extends Content {
    createdByType: "AD"
    fileType: "PNG"
}

class TokyoPngContent extends Content {
    createdByType: "ORIGINAL"
    fileType: "PNG"
}

// 追加

class AdJpgContent extends Content {
	createdByType: "AD"
	fileType: "PG"
}

class TokyoJpgContent extends Content {
	createdByType: "ORIGINAL"
	fileType: "JPG"
}

→ 組み合わせ爆発

こっちもこっちで地獄↓

/** プレイヤー */
class Player {
    async play(content: Content) {
        // jpgも動画に変換しないとね
        return device.play(["PNG", "JPG"].includes(content.type) ? convertToMp4(content) : content)
        //return device.play(content.type === "PNG" ? convertToMp4(content) : content)
    }
}

/** 社内ツール */
class InternalTool {
    // ん、こいつはなんか対応しなきゃいけないことあるんだっけ?
    async register(content: Content) {
        ...
        return
    }
}

解決?

抽象クラスに切り出してみる

/** 誰かに入稿されたコンテンツ */
abstract class CreatedContent {
  type: "AD" | "ORIGINAL"
}

/** ファイルとしてのコンテンツ */
abstract class FileContent {
  type: "MP4" | "PNG" | "JPEG"
}

気にしなくていいことをお互い気にしなくて良くなってHappy

/** 社内ツール */
class InternalTool {
  async register(content: CreatedContent) {
      switch (content.type) { // いい感じ
          .....
      }
      return
  }
}

/** プレイヤー */
class Player {
  async play(content: FileContent) {
      switch (content.type) { // いい感じ
          .....
      }
  }
}

型の方も書くか

/** 誰かに入稿されたコンテンツ */
abstract class CreatedContent {
  type: "AD" | "ORIGINAL"
}

/** ファイルとしてのコンテンツ */
abstract class FileContent {
  type: "MP4" | "PNG" | "JPEG"
}

class AdMp4Content extends CreatedContent, FileContent {}
//                                         ˜˜˜˜˜˜˜˜˜˜˜
// error: Classes can only extend a single class.

あ〜〜〜多重継承〜〜〜

多重継承問題について

https://ja.wikipedia.org/wiki/菱形継承問題

超要約

サークルの後輩に高校の先輩が入ってきたら何て呼んだらいいかわからんくなる問題のことです。

せや!こんな時こそinterfaceや!

interface CreatedContent {
    type: "AD" | "ORIGINAL"
}

interface FileContent {
    type: "MP4" | "PNG" | "JPEG"
}

interface AdMp4Content extends CreatedContent, FileContent {}
//        ˜˜˜˜˜˜˜˜˜˜˜˜
// Interface 'AdMp4Content' cannot simultaneously extend types 'CreatedContent' and 'FileContent'.
// Named property 'type' of types 'CreatedContent' and 'FileContent' are not identical.

おーん😩

解決

継承ではなく 委譲 delegate をします

abstract class Content {
  type: "AD" | "ORIGINAL"
  file: File
}

abstract class File {
  type: "MP4" | "PNG" | "JPEG"
}

まず型

class Mp4File extends File {
  type: "MP4"
}

class AdMp4Content extends Content {
  type: "AD"
  private file: Mp4File
  get fileType() { // fileTypeも直接出せる
  	return this.file.type
  }
}

そしてclient

/** 社内ツール */
class InternalTool {
  async register(content: Content) {
      switch (content.type) { // いい感じ
          .....
      }
      return
  }
}

/** プレイヤー */
class Player {
  async play(file: File) {
      switch (file.type) { // いい感じ
          .....
      }
  }
  
  // もしくはこう(file以外の部分がもし必要ならば)
  async play2(content: Content) {
      switch (content.file.type) {
          .....
      }
  }
}

その他の推しポイント

そもそも多重継承問題の解決として普通に便利 (Deletgation Pattern)

// BAD

// サークル後輩
class CircleJunior {
    name: string
    get nickname(): string {
        return `${this.name}くん`
    }
}

// 高校先輩
class HighSchoolSenior {
    name: string
    get nickname(): string {
        return `${this.name}さん`
    }
}

class CircleJuniorHighSchoolSenior extends CircleJunior, HighSchoolSenior {
    // nicknameはどっちになる???
}
// GOOD

class Human {
  constructor(readonly name: string) {}
}

// 高校先輩
class HighSchoolSenior extends Human {
  name: string
  get nickname(): string {
      return `${this.name}さん`
  }
}

// サークル後輩Adapter
class CircleJunior extends Human {
  name: string
  private human: Human
  
  constructor(human: Human) {
      this.name = human.name
      this.human = human
  }
  
  get nicknameAsSenior(): string {
      return this.human.nickname
  }
  
  get nicknameAsJunior(): string {
      return `${this.name}くん`
  }
}

class CircleJuniorHighSchoolSenior extends CircleJunior {
  static from(name: string) {
      return new CircleJunior(new HighSchoolSenior(name))
  }
}

const confusing = CircleJuniorHighSchoolSenior.from("hoge")

console.log(confusing.nicknameAsSenior) // "hogeさん"
console.log(confusing.nicknameAsJunior) // "hogeくん"

委譲が書きやすい言語だと楽

下の実装例を参照

動く実装例

https://pl.kotl.in/AFHuN6VWO

参考文献

https://tomosoft.jp/design/?p=3444

Tokyo, inc. Engineers

Discussion