目次
記事の概要
目的
「依存性の注入」という言葉。プログラミングに携わっている方なら、一度は耳にしたことがあるのでは?どうやら、良いプログラムを書くために役に立つ作法?のようですが・・・
オブジェクトについて関心の分離を行いましょう。とか、疎結合なプログラムが実現できます。とか、ちょっと難しい言葉で説明されることが多いですね。何だかよくわからない・・・という感覚をもつかたもいらっしゃるのではないでしょうか?
今回の記事は「依存性の注入」について、ちょっとした例を交えて、すこ〜し紐解いてみよう!という試みです。ご興味があれば少しばかりお付き合いください。
この記事はこういう方にオススメ
依存性の注入という言葉は耳にしたことがあるけど、いまひとつピンとこないという方。変更に強いプログラムってどういうこと?と悩んでる方に読んでいただければ幸いです。
歴戦の勇者からのマサカリは、決して望んでいません。絶対です!悪しからず・・・
そもそも依存性って何でしょう?
まずは、ググるのが正義ですね。「依存性」を検索してトップに表示された結果を読んでみます。
特定の何かに心を奪われ、「やめたくても、やめられない」状態になること・・・
厚生労働省
トップ表示は厚生労働省、さすがです。ただ・・・これは「依存症」の結果ですね。ちょっと探している内容ではなさそうです。
気を取り直して、それらしい結果を探しますが・・・。依存性で検索してもほどよい結果が得られません・・・。「依存性 コンピューター」で探してみても、ネット依存・・・。
どうやら、依存性という言葉自身が、すこし掴みにくいもののようです。
スマホのスピーカー
みなさんスマホはお持ちですか?スマホで動画を再生すると、スピーカーから動画の音声が流れてきます。
でも・・・もしスピーカーが壊れていたら、どうなるでしょう?きっと動画の音声は再生されないですよね。
音を出すというスマホの機能はスピーカーが担っています。そのスピーカーが壊れているとなれば音声の再生ができないのは当然の結果です。
このように、スマホが音声を再生するためにスピーカーを利用している。という状況が、まさしく「依存」です。スマホはスピーカーに依存しています。
さて、そのスマホで音声を再生できるようにするには、スピーカーを修理するしかありません。スピーカーはスマホに内蔵されていて簡単に取り出したり、交換することができません。音を出すようにするためには、スマホショップに修理を依頼することになり、スマホごと預けなければなりません。その間、もちろんスマホは利用できません。
この状態が、スマホとスピーカーの「結合度」や「依存度」が強いという状況です。切り離して扱えないのです。これが「密結合」です。
音を出すというスマホの機能を簡単に復元できるよう分離しやすく(疎結合)するにはどうすれば良いのでしょうか?
DIP(依存性逆転)原則
「SOLID原則」というソフトウェア設計をより簡単で柔軟に、保守しやすくするためのルールがあります。その中の一つに、モジュール間の結合度を低くすることを目的とした「DIP(依存性逆転)原則」があります。この原則は、以下の2つのルールで構成されています。
- 上位モジュールはいかなるものも下位モジュールから持ち込んではならない。
双方とも抽象に依存するべきである。 - 抽象は詳細に依存してはならない。詳細が抽象に依存するべきである。
こんな内容ですが、文章を読んでも、なにかピンときません。どういうことでしょうか?
下位に直接依存してはならない
まず1番目の原則には、「上位モジュール」、「下位モジュール」、「抽象」というキーワードがあります。
先ほどのスマホの例でいえば、「上位モジュール」がスマホ、「下位モジュール」がスピーカーということになりそうです。密結合になるので、スマホがスピーカーの機能を直接使っちゃだめですよ、ということのようですね。
次に「抽象」とはどういうことでしょうか?電子機器から音を出すという機能を考えた場合、共通した普遍的なルールのようなもはあるのでしょうか?例えば、音は電気信号で表されている。そして右音声用の信号と、左音声用の信号に分けて送られる。ということは考えられます。
詳細が抽象に依存
2番目の原則では、「詳細」が「抽象」に依存するべきと言っています。電子機器から音を出すという状況で、音を実際に出す装置になるスピーカーが詳細と言えるでしょう。
スピーカー(詳細)に合わせて電気信号の送り方(抽象)を変えるのではありません。どうやって電気信号が送られるか(抽象)を理解する。そして、それに合わせてスピーカー(詳細)を設計すべき、ということを表しているようですね。
スマホにはイヤホンジャックがある
さて、ここまで話してきたうえで、お気づきの方もいらっしゃると思いますが、スマホにはイヤホンジャックがついてますよね。スピーカーが壊れたスマホのイヤホンジャックに外付けスピーカーを繋げば、修理しなくても音が出せるようになります!(え、今どきついてないよ?!BTでしょ!と言いたい気持ちがあるかもしれませんが、ちょっと抑えてください(笑))
外付けスピーカー(詳細)は、左右で分けられた電気信号(抽象)を「規約」として理解して設計されています。それをイヤホンジャックという「インターフェース」に繋ぐことで音が出るようになるのです。
依存性の注入
スピーカーが壊れたスマホに、外付けスピーカーを繋ぐことで無事音が出るようになります。
同様に、ヘッドホンを繋いだときには、細かい部分まで聞き分けられる音が楽しめるようになります。
もしオシロスコープを挿せば、今度は音が出るわけではなく、電気信号が目に見える形で表示されます。スマホは同じ信号を出しているのに、繋ぐものによって動作が変わるのです。
外付けスピーカー、ヘッドホン、オシロスコープに共通していることは、
- 左右で分けられた電気信号という「規約」を理解して作られている。
- イヤホンジャックという「インターフェース」に繋げるようになっている。
ことです。
「規約」に準じて、設計された「詳細」をインターフェースを通じて、接続すること。
これが、依存性の注入(Dependency Injection)です。「上位モジュール」の中身を変更せずに「下位モジュール」を接続することで、その機能を利用(依存)することができます。
コード例
配列の中身を順に並べたい場合、sort関数を利用すると思います。そもそも、配列の要素の大小は、どうやって比べているのでしょうか?
例えば、数値であれば、二つの値を簡単に比較することができますが、文字列ならどうでしょう?キーを持つオブジェクト同士だったら、どう比較するのでしょうか?
比較の方法には、いろんなバリエーションが考えられます。文字列ならあいうえお順とか、文字列の長さとか、文字コード順とか。
もし比較の処理がsort関数の中に内包されているとしたら。とてもたくさんsort関数が必要になってしまいそうです。密結合の弊害ですね。
そこで、sort関数を「上位モジュール」として考えてみます。そして、比較する処理としてcompare関数を「下位モジュール」とします。DIP原則に従って結合度を下げた状況を考えてみましょう。
- 規約:一つ目の値と、二つ目の値を比べて
- 一つ目の値が大きい時は、正の数値を返す
- 一つ目の値が小さい時は、負の数値を返す
- 二つの値が等しい時は、0を返す
- インターフェース:二つの値を引数にとり、結果を数値として返す関数オブジェクト
上記の規約に準拠してcompare関数を実装することで、sort関数に比較方法を注入することが出来ます。
ちなみに・・・現在のsort()は、元の配列自身が変更されてしまう破壊的な処理ですが、ES2023では非破壊バージョンとなるtoSorted()がサポートされるようです。元のデータは極力変更しないようにしたいものですね。
type Human = { name: string, height:number, age:number } const humans: Array<Human> = [ {name: "Taro Yamada", height: 160, age: 20}, {name: "Jiro Suzuki", height: 175, age: 32}, {name: "Saburo Tanaka", height: 182, age: 40}, {name: "Shiro Nakamura", height: 150, age: 18}, {name: "Goro Sato", height: 167, age: 50}, ] // 「下位モジュール」compare関数 // 年齢で比較する const orderByAge = (h1: Human, h2: Human) => { return h1.age - h2.age } // 「下位モジュール」compare関数 // 背の高さで比較する const orderByHeight = (h1: Human, h2: Human) => { return h1.height - h2.height } const sortedByAge = humans.sort(orderByAge) console.log(sortedByAge) // [ // { name: 'Shiro Nakamura', height: 150, age: 18 }, // { name: 'Taro Yamada', height: 160, age: 20 }, // { name: 'Jiro Suzuki', height: 175, age: 32 }, // { name: 'Saburo Tanaka', height: 182, age: 40 }, // { name: 'Goro Sato', height: 167, age: 50 } // ] const sortedByHeight = humans.sort(orderByHeight) console.log(sortedByHeight) // [ // { name: 'Shiro Nakamura', height: 150, age: 18 }, // { name: 'Taro Yamada', height: 160, age: 20 }, // { name: 'Goro Sato', height: 167, age: 50 }, // { name: 'Jiro Suzuki', height: 175, age: 32 }, // { name: 'Saburo Tanaka', height: 182, age: 40 } // ]
規約、インターフェース
sort関数の例では、規約に基づいてcompare関数を実装して、sort関数に注入していました。
ところで、ルールとして規約するだけではなく、仕組みとして規約を強制することはできないのでしょうか?
多くの言語でサポートされているinterfaceがあります。interfaceは、まさに規約を明確にコーディングするためのものです。
ちなみに、関数の定義だけ書いて中身を書かないのに、何の役に立つのだろう?と思ったことはありませんか?私にはそんな頃がありました(笑)
interfaceを参照する場合、interfaceに定義された関数をもれなく実装しなければなりません。言い換えれば、規約として定義された関数の実装を強制されることになります。
- interfaceを実装する=要求する機能を全て準備する
- ルールに則った注入可能なオブジェクトが作成される。
// 動物は前進する interface Creature { goForward(): void } // 鷹は前進するために飛ぶ class Hawk implements Creature { fly(): void { console.log('The hawk flies!') } goForward(): void { this.fly() } } // 馬は前進するために駆ける class Horse implements Creature { gallop(): void { console.log('The horse gallops!') } goForward(): void { this.gallop() } } // カンガルーは前進するためにジャンプする class Kangaroo implements Creature { jump(): void { console.log('The kangaroo jumps!') } goForward(): void { this.jump() } } class Parade { private creatures: Creature[] = [] join(creature: Creature): void { this.creatures.push(creature) } start(): void { this.creatures.forEach((creature) => {creature.goForward()}) } } const parade = new Parade // 動物みんなパレードに参加 parade.join(new Hawk()) parade.join(new Horse()) parade.join(new Kangaroo()) parade.start() // The hawk flies! // The horse gallops! // The kangaroo jumps!
まとめ
依存性の注入について、自身の拙い知識の範囲で紐解いてみました。
ネットでは自動テストでスタブやモックへ置き換える事例が多く見つかる依存性の注入ですが、本質としては、疎結合なプログラム構造を実現するための仕組みです。自動テスト以外でも取り入れられると思いますので、いろんな場面で考慮してみてはいかがでしょうか。私自身も、抽象に依存することを意識して、プログラムを書いていきたいと思います。