🌉
デザインパターン:Bridgeパターン
これは何?
独立した複数の次元を切り出したいけど多重継承されると困るから委譲しよう
何が嬉しいのか
問題
弊社のコンテンツには「広告 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.
あ〜〜〜多重継承〜〜〜
多重継承問題について
超要約
サークルの後輩に高校の先輩が入ってきたら何て呼んだらいいかわからんくなる問題のことです。
せや!こんな時こそ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くん"
委譲が書きやすい言語だと楽
下の実装例を参照
動く実装例
参考文献
Discussion