Help us understand the problem. What is going on with this article?

オブジェクト合成の力

More than 1 year has passed since last update.

前の記事では、関数合成の力について語りました。
いいねの数からすれば、そこそこ興味を持ってもらっているようなので、
今回は、オブジェクトの合成の力について語ろうと思います。


オブジェクトデザインの二つのパターン

Javaだろうが、C++だろうが、Javascriptだろうが、オブジェクトを作らないといけないときは絶対あります。
それぞれのオブジェクトは何を表すか、そしてどう自分の役割を果たすか検討するときに、主に二つのパターンがあります:継承合成です。

継承

Qiitaを開いている時点で、「継承」という言葉を耳にしたことが一回はあるでしょうが、具体的に考えてみましょう。
ゲームの開発を頼まれたあなたに、クライアントはこう言います:「ゲームには犬とロボットがあります!犬は吠えます!ロボットはホイールついてて動き回ります!」

そこで、「なるほど」と思って、以下のようなクラスの図を作ります:

Robot
 .drive()

Dog
 .bark()

書いて満足している、その瞬間にクライアントから連絡が来ます。「犬以外にも猫がほしいですね!ロボットも、クリーニングするロボットと、殺し屋ロボットがあったらいいな!」. . . 「あ、あと犬と猫はうんちします!」

すると、まずロボットに共通の行動(動き回る)を抽象化して、親のクラスを作ります。そうすることで、CleaningRobotMurderRobotはそれぞれ違う機能を持っていても、どちらも動き回ることができます。つまり、親クラスの.drive()メソッドを継承します。犬も猫もうんちするということで、親クラスとしてうんちできるAnimalを作ります。

Robot
 .drive()
   CleaningRobot
    .clean()
   MurderRobot
    .kill()

Animal
 .poop()
   Dog
    .bark()
   Cat
    .meow()

「完璧!」と思ったら、また連絡が来ます。「すみません、やっぱり殺し屋犬ロボットもほしいです!ただ、吠えることはできるけど、うんちはしないです!」

😡

さて、ここでできることは二つ。

MurderRobotDogを作り、bark()メソッドを複製します。

Robot
 .drive()
   CleaningRobot
    .clean()
   MurderRobot
    .kill()
      MurderRobotDog
       .bark()

今回はbark()だけですが、機能追加でたくさんのメソッドを複製する必要は出てきます。やはりNGです。

RobotAnimalに共通の親クラスを作り、bark()メソッドを継承します。

GameObject
 .bark()
   Robot
    .drive()
      CleaningRobot
       .clean()
      MurderRobot
       .kill()
         MurderRobotDog
   Animal
    .poop()
      Dog
      Cat
       .meow()

しかしこれはもう抽象的すぎて、親クラスの名前も何にすればいいかわからなくなってしまいます。GameObject???
さらに、本来持っていない機能を、いろんなオブジェクトが持つようになり、Joe Armstrong氏が言っていた有名なGorilla Banana Problemになってしまいます。「バナナだけほしかったが、バナナをもつゴリラと、ゴリラが住んでいるジャングルも全部もらった」という。

合成

継承自体は悪くないので、誤解しないでほしいです。しかし、それをうまく利用するには、実装前に全部の条件を知る必要があります
それはなかなかない贅沢なので、合成パターンを使います!

この場合の合成は何かというと、オブジェクトを、そのオブジェクトができる行動の組み合わせとして捉える考え方です。

const createBarker = (state) => ({
  bark: () => console.log('WOOF! I am ' + state.name);
})

const createDriver = (state) => ({
  drive: () => state.position = state.position + state.speed;
})

createBarker({ name: "Shiba" }).bark()
// WOOF! I am Shiba

createBarkercreateDriverは、ひとつだけの行動を持った簡単なオブジェクトを作ります。JavascriptのObject.assignを使えば、いくらでもオブジェクトを組み合わせることができます。上で問題になったMurderRobotDogも、以下のように作れます:

const createMurderRobotDog = (name) => {
  let state = {
    name,
    speed: 100,
    position: 0
  }

  return Object.assign(
    {},
    createBarker(state),
    createDriver(state),
    createKiller(state)
  );
}

createMurderRobotDog("Tama").bark()
// Woof! I am Tama

それだけです。
実装しないといけない機能が増えても、オブジェクトを作る関数を1個増やせばいいだけです。

Pipe編

上の関数を少し変えてみると、pipeを使った合成もなんと可能になります!(詳しいpipeの説明はこの投稿に書きました)

たとえば、うんちできる謎の猫犬を作ってみましょう。

const withBark = obj => Object.assign(obj, { bark: () => console.log("WOOF!") });
const withMeow = obj => Object.assign(obj, { meow: () => console.log("MEOW!") });
const withPoop = obj => Object.assign(obj, { poop: () => console.log("...💩") });

const createPet = (name) => ({ name });

const myPet = createPet("Tama");

const myPoopingDogCat = pipe(
  withBark,
  withMeow,
  withPoop
)(myPet);

myPoopingDogCat.bark()
// WOOF!
myPoopingDogCat.meow()
// MEOW!
myPoopingDogCat.poop()
// ...💩

簡単!

参考にしたもの
  1. https://medium.com/javascript-scene/the-hidden-treasures-of-object-composition-60cd89480381
  2. https://www.youtube.com/watch?v=wfMtDGfHWpA (YouTube)
jlkiri
Webでのユーザーインターフェースを作っています @ Yumemi Co., Ltd.
https://www.kirillvasiltsov.com/
yumemi
みんなが知ってるあのサービス、実はゆめみが作ってます。スマホアプリ/Webサービスの企画・UX/UI設計、開発運用。Swift, Kotlin, PHP, Vue.js, React.js, Node.js, AWS等エンジニア・クリエイターの会社です。Twitterで情報配信中https://twitter.com/yumemiinc
http://www.yumemi.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away