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

アンチパターンで理解を深めるクラス設計

概要

この記事の続き。
クラス間に関する話が中心。
前回のタイトル引きずってますが思ったよりアンチパターンが浮かびませんでした。

クラス設計の基本

オブジェクト指向で設計をしていくとクラス間の関係性が重要になってきます。
ざっくり分けて以下の種類があります。

  • コンポジション: 全体/部分
  • 集約: 容器/要素
  • 汎化: 抽象/具体

コンポジション関係

全体と部分の関係。
複雑な問題を全体と部分に分けて考えるのは問題解決の王道1であり、特に重要な関係。

スクリーンショット 2020-02-09 13.52.03.png

増改築でクラスがデブって来たらこの関係で分割できないか検討する。

後述の集約と似ているので混同しやすい。(自分もしばしば混乱する)
集約と違ってオブジェクト成立時に子オブジェクトの存在は必須。
なのでコンストラクタで初期化しよう。

生成時に確定する関係なので全体オブジェクトそのままで部分オブジェクトをすげ替えるなんてことはないです。
部分オブジェクトの状態が変化することはあるけど。
その時は全体オブジェクトを再生成する必要があるか、集約の関係と混同してる。

車とタイヤの関係でよく例えらる。
HTMLの情報を管理するクラスが有ればHeaderとFooterを管理するクラスは分けとくかぁって感覚。

class Page {
    construct(header, footer) {
        this.header = header
        this.footer = footer
    }
}
new Page(new Header(), new Footer())

アンチパターン

  • Dependency-Injection使って無い
  • メソッドで部分クラス初期化してる
  • 部分クラス途中で置き換えちゃってる

集約関係

入れる容器と、入れられる要素の関係。
こちらも問題を分割するという意味ではコンポジションと同じぐらい重要。

コンポジションと違って容器側の生成が先で、後から要素が入ってくる動的な関係。
動的な関係ということはステートがそこにあるということなので、業務ロジックをと絡みやすい。

メソッドを使って要素をセットしたり取り除いたりする。
コンストラクタではとりあえず空値セットしておくけど初期値セットするのもあり。

要素は必ずしも複数とは限らず0,1の場合もある。
ワインセラーとワインの関係に似てる。

いい例が思いつかん。。。

class Ring {
    constructor() {
        this.champion = null
        this.challenger = null
    }
    setChallenger(user) {
        this.manager = user
    }
    resetChampion() {
        this.champion = this.challenger
        this.challenger = null
    }
    ...
}
ring = new Ring()
ring.setChallenger(new Challenger())

アンチパターン

  • 容器側のセットする箇所が多すぎてクラスが巨大化
  • この関係を使いすぎて状態がよくわからんことになってる

汎化関係

クラス設計を抽象レイヤーと具体レイヤーに分離する地獄の始まり。
継承、拡張、特化、汎化いろいろ呼び方がある。
抽象設計は高度な概念的思考力が求められるので、使わないに越したことはない。2
こいつのせいでオブジェクト指向設計わからんってなる諸悪の根源。

上記2つをhas-a関係というのに対してis-a関係と言ったいるする。
抽象側を親クラスとか基底クラス、具体側を子クラスとか派生クラスと呼ぶ。

特に抽象クラスの作成は抽象化思考が求められるので困難を極める。
常にソクラテスの三段論法3を頭で唱えながら、そのクラスを扱う覚悟があるなら作成しよう。

メンバの拡張と、メソッドの実装の両方を指すので混乱する。
分けて説明する。

メンバの拡張

抽象クラスにメンバを追加して具体クラスを作る。
メンバを増やしただけなので、抽象クラスにできることは具体クラスで全てできる。

抽象クラスで不十分な機能を追加する目的で行う。
原則抽象クラスのメソッドだけ呼び出す拡張メソッドを作る。

拡張フィールドを作ってしまうと、一つのオブジェクトに対して抽象側と具体側の両方の状態を把握しないといけなくなる。
チームの平均IQが200以下なら辞めておくのをオススメする。

メソッドの実装

抽象クラスでメソッドの宣言だけ行って、具体クラスで具体的な実装を行う。
宣言と一緒にデフォルトの挙動が実装されている場合もある。
フレームワークなどで特定の名前のメソッドの実装を求められるのがこれ。

抽象側で使うためのメソッドなので、指示された内容を守って正しく実装していれば問題ない。
抽象側で何というクラスがどういう風に呼び出してるかは気にしないでいい。
リファレンスをよく読まないと痛い目にあう。

素直に上書きだけして、具体側の他のクラスからは触らずそっとしておく。
下手に依存関係をもたせるとフレームワークの仕様変更で大爆死する。

アンチパターン

  • 必要ないのに使っちゃった
  • コードのコピーのために使っちゃった
  • フレームワークの指示無視しちゃった
  • 無駄に概念階層深くなった

汎化関係の代替え概念

インターフェイス

メソッドの抽象化に特化した仕組み。

メソッドの宣言箇所だけ抜き出したものをインターフェイスと言って特別扱いする。
専用の構文が用意されていることが多い。

ミックス・イン

メンバ拡張の代わり。
2つのクラスを合成して機能を合体させたクラスを作る。

拡張部分を別クラスとして宣言して合成すれば同じことができる。

ジェネリックス

Javaで使われる。
引数の型だけを抽象化する仕組み。
静的型付け言語は型の差分だけで無限にクラス増えたりしちゃうので仕方ないね。

テンプレートクラス

C++の仕組み。
ジェネリックスと混同しそうになるが根本的に違う。
コンパイル時にテンプレートを元に動的にクラスを生成する。
どっちかと言うと大量の似た実装のクラスを省略表記してるという方が正しい。
バイナリサイズがソースコードの何倍にも膨れ上がったりする。怖い。

参考文献

忘れたときによく参照します
http://www.itsenka.com/contents/development/uml/class.html


  1. 分割統治法 

  2. ライブラリやフレームワークのクラスから具体化させるときしか使わないんじゃないだろうか? 

  3. 「人間は死ぬ」かつ「ソクラテスは人間である」、よって「ソクラテスは死ぬ」 

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
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした