君は心理学者なのか?

大学時代に心理学を専攻しなぜかプログラマになった、サイコ(心理学)プログラマかろてんの雑記。

OSSから設計について学ぶ〜クラスの依存方向について〜

要約

僕が間違ってました。すみませんでした。

いきさつ

「Mapを使ったアプリケーションで、Mapの上にMarkerを立てたい」

という要件を満たすために、leafletというOSSを使うことにした。

github.com

  • 公式サイト

leafletjs.com

インターフェース(ライブラリの使い方)を予想してみた

OSSを使うときに、

こんな感じのインターフェースじゃないかなって予想してみた。

僕「MapがMarkerを持ってるから、こうだろ」

f:id:karoten512:20180804220204p:plain

僕「だから、インターフェースはこんな感じだろ」

const map: Map = new Map();
const marker: Marker = new Marker();
map.addMarker(marker);

僕「で、実装はこんな感じだろ」

class Map
{
    private markers: Array<Marker>;
    constructor() {
      this.markers = [];
    }
    addMarker(marker: Marker) {
        this.markers.push(marker)
    }
}

class Marker
{
    constructor() {     
    }
}

僕「うーむ。実に自然な発想だぞ。」

実際のインターフェース

const map: Map = new Map();
const marker: Marker = new Marker();
marker.addTo(map);

僕「え…?」

class Map
{
    constructor() {
    }
}

class Marker
{
    private map: Map
    constructor() {
    }
    addTo(map: Map) {
        this.map = map;
    }
}

僕「え…?え...?」

f:id:karoten512:20180804221839p:plain

Mapクラス「もしかして」

Markerクラス「わたしたち」

僕「(依存方向が)いれかわってる〜〜〜〜!?

他のOSSもみてみた

google map JavaScript API、mapboxGL, mapboxJSなどもみてみたが、

皆同じようなインターフェースだった。

つまり、

MapがMarkerに依存しているのではなく、

MarkerがMapに依存していた。

どうしてこんな設計なのか考えてみた

こっちのほうが変更に強いからだった。

変更に強いとは

例えば、MapにMarkerじゃなくてPopupも追加しようと思ったら、

僕が考えた実装方法だったらこうなる。

※ 理解を簡単にするため、ソースコードは極限まで単純化しています

f:id:karoten512:20180804223242p:plain

class Map
{
    private markers: Array<Marker>;
    private popups: Array<Popup>;
    constructor() {
        this.markers = [];
        this.popups = []; // 追加
    }
    addMarker(marker: Marker) {
        this.markers.push(marker)
    }
    // 追加
    addPopup(popup: Popup) {
       this.popups.push(popup)
    }
}

// 追加
class Popup {
    constructor() {        
    }
}

つまり、Popupの機能を追加しようとしたときに、

Mapの方も修正が必要になってくる。

さらにOverlay(地図上に何かを覆い隠す)を機能として追加してみると、、、

f:id:karoten512:20180804223708p:plain

いい感じに「やな感じ」がしてきましたね笑

やな感じの正体

新たな機能を追加するたびに、Mapの方にも修正が発生してしまっているというのが、

やな感じの正体です。

これは「開放・閉鎖の法則」に違反しています。

開放/閉鎖原則に沿ったソフトウェアは、既存のソースコードを変更せずに機能修正や機能追加を行うことができる。 そのため、品質検査を再実行する必要がない。(wiki)

また、MapとMarkerだけ使いたいときもあるはずです。

そのときも、MapはPopupやOverlayのことまで知っている必要がある、というのもおかしな話ですね。

依存関係を逆転してみる

f:id:karoten512:20180804224309p:plain

class Map
{
    constructor() {
    }
}

// 追加
class Popup
{
    private map: Map
    constructor() {
    }
    addTo(map: Map) {
        this.map = map;
    }
}

既存のコードに手を加えずに、機能を追加することができます。

すごい。すごいね。

どんなときに応用できるか

たとえば、ペイントのようなアプリをつくるとしましょう。

f:id:karoten512:20180804224540p:plain

機能をボンボコ追加しても、修正をしても他のクラスには影響がでません。

素敵ですね。

まとめ

依存関係を逆転して考えると、

その後の機能追加時のコストに大きな差が出てきそうです。

そんなことを、今回知りました。

ただ、今回本当に単純化しているので、実際はもうちょっと複雑なはず。

注意したいです。