オブジェクト指向プログラミングを極める

より良いプログラマを目指すブログ

「Getter/Setterを作るな」の本当の意味

 オブジェクト指向において「Getter/Setterを作るな」というのは様々な場所で言われていることだが、これが一番難しくて苦労する。Getter/Setterを排除するための対処法として「デメテルの法則」や「Tell, don't ask.」などが世の中のあらゆる場所で紹介されているが、どれもこれも対処療法に過ぎない。Getterをできるだけ無くすにはオブジェクトに対する認識そのものを変える必要がある。

オブジェクトはデータの入れ物ではない

 Qiitaなどでオブジェクト指向に関する記事を検索するとそのほとんどがオブジェクトを「データとそれを操作するメソッドの集まり」として扱っているが、これがそもそもの間違いだ。オブジェクトは決してデータの集まりなんかではない。どういうことか。例えば下記は「間違ったオブジェクト指向」の解説でよく見かけるようなPersonオブジェクトだ。

class Person
{
    int id;
    string name;
    int age;

    public Person(int id, string name, int age)
    {
	this.id = id;
	this.name = name;
        this.age = age;
    }

    public int GetId()
    {
        return this.id;
    }

    public string GetName()
    {
        return this.name;
    }

    public int GetAge()
    {
        return this.age;
    }
}

この時点でGetter以外に既に間違っているところがある。まずPersonからNameを取得するメソッドの名前はGetNameではなく単にNameだ(理由は後述)。他も同様に修正しよう。

class Person
{
    int id;
    string name;
    int age;

    public Person(int id, string name, int age)
    {
	this.id = id;
	this.name = name;
        this.age = age;
    }

    public int Id()
    {
        return this.id;
    }

    public string Name()
    {
        return this.name;
    }

    public int Age()
    {
        return this.age;
    }
}
    // 使い方
    // 10回Personの情報を出力する。(意味不明なメソッド…例えということで勘弁して)
    void Print10Times(Person person)
    {
        for (int i = 0; i < 10; i++) {
            Console.WriteLine(
                "ID {1} : Name = {2}, Age = {3}",
                person.Id(),
                person.Name(),
                person.Age()
            );
        }
    }

この時点ではperson.nameで直接フィールドを参照するのとperson.Name();でメソッドを通して参照することの結果に全く差はない。しかし意味的に言えば、両者には決定的な差がある。person.Name();の方はpersonの名前を取得する手続きが隠蔽されているのだ。この説明でしっくりこないのは Person がこの時点ではまだ単なるデータの入れ物であり、私たちがPersonの中身を知っている前提で話をしているからだ。このPersonを本当の意味でオブジェクトにするためにはインターフェースを書く必要がある。

interface Person
{
    string Id();
    string Name();
    int Age();
}
class ConstPerson : Person
{
    int id;
    string name;
    int age;

    public ConstPerson(int id, string name, int age)
    {
	this.id = id;
	this.name = name;
        this.age = age;
    }

    public int Id()
    {
        return this.id;
    }

    public string Name()
    {
        return this.name;
    }

    public int Age()
    {
        return this.age;
    }
}
    // Personがインターフェースであることに注目
    void Print10Times(Person person)
    {  
        for (int i = 0; i < 10; i++) {
            Console.WriteLine(
                "ID {1} : Name = {2}, Age = {3}",
                person.Id(),
                person.Name(),
                person.Age()
            );
        }
    }

これで手続きが本当に「隠蔽」された。もはやName()メソッド内で何が起こるかは呼び出し側からは全く分からない。端的に言えばpersonオブジェクトが、自身がフィールドとして持っているデータを返そうがデータベースにアクセスしてデータを取ってきてから返そうが呼び出し側にとっては全く関係がなくなった。Personの実装は下記のようなものでも良いわけだ。

// データベースのPerson
class DbPerson : Person
{
    readonly int id;	
    readonly SqlConnection con;

    public DbPerson(int id, SqlConnection con)
    {
        this.id = id;
        this.con = con;
    }

    public int Id()
    {
        return this.id;
    }

    public string Name()
    {
        try {
	        con.open();
	        string command = "select name from people where id ='" + id +"'";
	        SqlDataReader data = new SqlCommand(command).ExecuteReader();
	        return (string)data["name"];
        }
        finally {
	        con.close();
        }
    }

    public int Age()
    {
        try {
	        con.open();
            string command = "select age from people where id ='" + id + "'";
	        SqlDataReader data = new SqlCommand(command).ExecuteReader();
	        return (int)data["age"];
        }
        finally {
	        con.close();
        }
    }
}
    // Personがインターフェースであることに注目
    void Print10Times(Person person)
    {  
        for (int i = 0; i < 10; i++) {
            Console.WriteLine(
                "ID {1} : Name = {2}, Age = {3}",
                person.Id(),
                person.Name(),
                person.Age()
            );
        }
    }

 これはYegor氏のブログの「SQLを話すオブジェクト」を参考にしたものだ。ここで気付かれるだろうが、もしDbPersonのインスタンスPrint10Timesメソッドに渡されていたとすると、Name() を呼ぶたびにデータベースアクセスが発生することになる。そこで「Name() メソッドを呼び出したときに、データベースアクセスが起こるかもしれないなんて誰も想像できないじゃないか。これは悪い設計だ!」という人(かつての私)がいるかもしれない。

喝!!

中身を気にしなくてよい(カプセル化というオブジェクト指向の大原則はどこへ行ったのか。真のオブジェクト思考プログラマーはフィールドを返すName()とデータベースアクセスを行うName()を区別しない。冗談ではなく本当に区別しない。私が求めるのは person.Name()がperson自身の名前を返すこととその名前が正しいことであり、途中で何が起こってようがそれはどうでもいいことだ。もしperson.Name()の動作が遅くてシステム要件を満たせない…となったらその時に初めて私はpersonの中身を確認し、データベースアクセスが原因であることを突き止める。そしてデータをキャッシュする新しいPersonの実装クラスを作り、下記のようにするだけだ。

class CachedPerson : Person
{
    int id;
    string name;
    int age;

    public CachedPerson(int id, string name, int age)
    {
	this.id = id;
	this.name = name;
        this.age = age;
    }

    public int Id()
    {
        return this.id;
    }

    public string Name()
    {
        return this.name;
    }

    public int Age()
    {
        return this.age;
    }
}
    void Print10times(Person person)
    {
        // 最初にキャッシュする。もし最初からpersonがCachedPersonでも無意味なだけで大丈夫。
        Person cached = new CachedPerson(person.Id(), person.Name(), person.Age());
        
        for (int i = 0; i < 10; i++) {
            Console.WriteLine(
                "ID {1} : Name = {2}, Age = {3}",
                cached.Id(),
                cached.Name(),
                cached.Age()
            );
        }
    }

 さて、改めて上記のコードを見返してみよう。DbPersonはデータの居場所(SqlConnection)とIDを知っているだけだが、メソッドの呼び出し側からすれば立派にPersonとして振舞っている。DbPersonはデータベースのデータをPersonとして振舞わせてるオブジェクト、まさに「データベース内に生きるPerson」と表現できる。もはやName()はGetterではない。

 ここで私が言いたいのは、オブジェクトは別に中にデータを持っている必要はないし、メソッドは中のデータを必ず使わないといけないわけではないということだ。オブジェクト指向においてオブジェクトに求めることはメソッドの戻り値が正しいことで、その手段ではない。だからGetNameとかFindなんとかとかいうメソッド名は間違っている。GetしてないかもしれないしFindしてないかもしれないから。オブジェクトの属性を得るメソッド名は単に名詞で十分だ。動詞を使って実装を縛るような発想を与えるのはよろしくない。同じ理由でインターフェースを書かずにいきなりクラスを書き始めるのも間違っている。オブジェクトを設計するときはそのオブジェクトが何かではなく何をしてほしいか(インターフェース)を先に考える。そして後から中身(クラス)を考えるのが正しい手順だ。*1

 さて、上記のコードに戻る。一時的にキャッシュのために用意したCachedPersonは「メモリの中に生きるPerson」だ。このクラスのメソッドは…残念ながらまだGetterだ。しかし「オブジェクトはデータの入れ物ではない」という基本ができていれば問題ない。ここからはやっと「デメテルの法則」や「Tell, don't ask.」が役に立つだろう。*2

まとめると、Getterを無くすための合言葉は「オブジェクトはデータの入れ物ではない」だ。

Setterは…?

今回はGetterを無くすための考え方を紹介したが、Setterについては別の機会に記事を書く。ざっくり紹介するとSetterを無くすための合言葉は「オブジェクトは生まれたときに完成していなければならない」だ。

*1:現実問題としていちいちインターフェースを作ってられないというのはごもっともである。大事なのは上記の考え方だと思う

*2:具体的な方法は別の記事に書きたい

privateメソッドは不純物

あるクラスの中で同じ処理が何度も出てくることがある。
このときコードの重複を排除するためにprivateメソッドを使うことを思いつく。

    class SomeDevice
    {
        // デバイスの温度を取得
        public float GetTempareture()
        {
            byte[] rawData = input.read();
            return ParseToShort(rawData) - 273.15f;
        }

        // デバイスの電圧を取得
        public float GetVoltage()
        {
            byte[] rawData = input.read();
            return ParseToShort(rawData) * 1000 / (float)0xFFFF;
        }

        // 2byteの生データをushortに変換
        private ushort ParseToShort(byte[] rawData)
        {
            return (ushort)((rawData[0] << 8) | (rawData[1]));
        }
    }

さらに他のクラスでも同じprivateメソッドが現れたとする。

    class AnotherDevice
    {
        // デバイスの角度を取得
        public float GetLotation()
        {
            byte[] rawData = usb.read();
            return ParseToShort(rawData) / (float)0xFFFF;
        }

        // デバイスの速度を取得
        public float GetSpeed()
        {
            byte[] rawData = usb.read();
            return ParseToShort(rawData) * 100 / (float)0xFFFF;
        }

        // 2byteの生データをushortに変換
        private ushort ParseToShort(byte[] rawData)
        {
            return (ushort)((rawData[0] << 8) | (rawData[1]));
        }
    }

ここでprivateメソッドが重複してしまうので抽象クラスにまとめる。

    abstract class Device
    {
        protected ushort ParseToShort(byte[] rawData)
        {
            return (ushort)((rawData[0] << 8) | (rawData[1] & 0xFF));
        }
    }

    class SomeDevice : Device {...}
    class AnotherDevice : Device {...}

コードの重複は避けられた…が、これはまったくエレガントではない。他のデバイスが2バイト以外のデータを返す場合は結局別のメソッドを作らないといけなくなるし、だいいち私の嫌いな継承が使われている。最良の方法はこのprivateメソッドをオブジェクトにしてしまうことだ。

    class UShortFromBytes
    {
        readonly byte[] rawData;

        public UShortFromBytes(byte[] rawData)
        {
            this.rawData = rawData;
        }

        public ushort Value()
        {
            return (ushort)((rawData[0] << 8) | (rawData[1]));
        }
    }

    class SomeDevice
    {
        // デバイスの温度を取得
        public float GetTempareture()
        {
            byte[] rawData = input.read();
            return new UShortFromBytes(rawData).Value() - 273.15f;
        }

        // デバイスの電圧を取得
        public float GetVoltage()
        {
            byte[] rawData = input.read();
            return new UShortFromBytes(rawData).Value() * 1000 / (float)0xFFFF;
        }
    }

この方法は抽象クラスにまとめる方法よりも多くの利点がある。まず分離したオブジェクトは単体テスト可能である。さらにポリモーフィズムにより拡張可能である。そして何より分離後のクラスは小さくてシンプルなものだ。privateメソッドが現れた場合、早いうちにそれをオブジェクトとして独立できないか検討するべきだと思う。特に同じ内容のprivateメソッドが複数のクラスに渡って存在する場合、それは間違いなくオブジェクトとして分離すべきなのでどんどんオブジェクト化していきたい。

privateメソッドはオブジェクト指向とは関係ない?

オブジェクト指向におけるメソッドとは、特定のメッセージに反応して処理されるコードブロックのことらしい。それならばprivateメソッドとはいったい何なのだろうか。thisを通してしか呼び出せないprivateメソッドは自分自身へのメッセージのみに反応して処理されるコードブロックということになる。オブジェクト指向プログラミングは「オブジェクト同士」によるメッセージのやり取りでプログラムを表現するもの、ということから考えるとprivateメソッドにはコードを読みやすくする以上の役割はない。以上のことからprivateメソッドはオブジェクト指向とは関係のない不純物と言えるだろう。真のオブジェクト指向プログラムでは、メソッドはpublicのみが存在するべきである。

継承のことは忘れよう

 ご存知の通り、オブジェクト指向の三大要素であるはずの継承においてはたくさんの問題点が報告されている(こことか、こことか)。確かに継承よりもコンポジション(委譲、合成)のほうが優れている点が多い。下記はその一例だ。

いま、Windows.FormsのButtonクラスに機能を足して押された回数をカウントして表示するボタンを作りたいのでButtonクラスを継承して新しいクラスを作るとする。

    class CountingButton : Button
    {
        private int count = 0;

        protected override void InitLayout()
        {
            base.InitLayout();
            this.Text = this.count.ToString();
        }

        protected override void OnClick(EventArgs e)
        {
            base.OnClick(e);
            this.count++;
            this.Text = this.count.ToString();
        }
    }

元のButtonクラスのコードはすべて引き継がれているので変更が必要な初期化とクリックの処理だけをオーバーライドすればOKだ。とても便利!…と思いきや、IDEで入力補完を見るとすぐに問題があることが分かる。

f:id:takesho0214:20190325220055p:plain

これが継承の弱点だ。Textプロパティの操作まで公開されてしまっている。Publicなメソッドやプロパティを継承先のクラスで隠すことができないのでどうしようもない。これでは意図しない操作によりオブジェクトが破壊される可能性があるので非常に不安になる。使わないように気を付けたら良いという問題ではない。例えば親クラス型のコレクションに入れられたときにどうなるか。

    List<Button> buttons = new List<Button>();
    buttons.Add(new Button());
    buttons.Add(new Button());
    buttons.Add(new CountingButton());
    foreach (Button button in buttons) {
        button.Text = "アウト!";
    }

このコレクションに私の作ったオリジナルボタンが入っていないことを気にしなければならなくなる。これは中身を気にしなくてよいというカプセル化の原則に違反している。
コンポジションならこの問題は発生しない。

    class CountingButton
    {
        readonly Button origin;
        int count = 0;

        public event EventHandler Click;

        public CountingButton(Button origin)
        {
            this.origin = origin;
            this.origin.Click += origin_Click;
            this.origin.Text = this.count.ToString();
        }

        private void origin_Click(object sender, EventArgs e)
        {
            count++;
            this.origin.Text = this.count.ToString();
            this.Click(sender, e);
        }
    }

f:id:takesho0214:20190326214505p:plain

問題だらけの継承よりコンポジションが優れている。だから継承は悪。
…本当だろうか。

継承の使い方がまちがっている

 継承が悪かどうかという話においては上記のような実用上の問題点にフォーカスが当てられがちだが、その多くは継承を不正に使っていることが原因だ。上記の例はまさに典型的な継承の誤用である。Buttonクラスを継承してコードを再利用しようとするのは良いが、Textプロパティを操作できないような仕様にするのはまずい。Windows.Formsの世界において、ButtonはTextプロパティを操作できるものと決められているからだ。もはやこの世界において、CountingButtonはButtonとは認められないのでButtonクラス型の変数に入れられるようになっていてはいけない。したがって最初からコンポジションを使うのが正解だ*1

そもそも継承の仕組みがオブジェクト指向の世界にそぐわない

 上記の例のように、実装の再利用という観点で継承を理解すると無意識のうちに悪用してしまう。どうしてJavaC#、その他オブジェクト指向言語にはこのような危険な仕組みが用意されているのだろうか。便利だから?こんな仕組みはオブジェクト指向の世界に全くそぐわない。元祖オブジェクト指向言語Smalltalkのことを思い出してほしい。継承のキーワードはinherit(継承)ではなくderive(派生)だ。これは分類学的な意味でサブクラスを作るという意味(例えば人間は哺乳類のサブクラス)であり実装をコピーして引き継ぐこととは何の関係もない。そしてそれはインターフェースで間に合っている。

interface Animal 
{
    void Breathe();
}

interface Human : Animal 
{
    void Talk();
}

interface Dog : Animal 
{
    void Run();
}

interface Fish : Animal
{
   void Swim();
}

 分類学的な意味でのサブクラスはスーパークラスの性質を完全に引き継ぐ必要がある。スーパークラスの性質(メソッド)をオーバーライドしたり差し引いたりしたサブクラスはもはやスーパークラスと同列に扱ってはいけない存在だ。にもかかわらず継承の仕組みはそれを許してしまっている。それこそが本当の問題ではないだろうか。

 不正なサブクラスが存在しているとき世界は歪んだ状態になっている。歪んだ世界において我々はスーパークラスのオブジェクトを信用できないので中身を調べたり(is演算子[C#] / instanceof演算子[Java])、無理やり認知(ダウンキャスト)したりしなければならなくなる。そのようなプログラムはオブジェクト指向とは言い難い。
 つまるところ、継承はオブジェクト指向とは何の関係もない仕組みなので忘れるべきだろう。それが世界を守るための唯一の方法だ*2。もちろんすべてのクラスをsealed(Javaではfinal)にして第三者による破壊からも守らなければならない。

参考文献

*1:コンポジションだと元のButtonの必要なメソッドを呼ぶためのメソッドを全部書く必要があるというデメリットはある。そもそもちゃんとインターフェースが適切に設計・公開されていればこんなことにはならないのだが…Microsoft

*2:「継承を正しく使いましょう」というアドバイスはもはや意味を成していない。Androidプラットフォームに触れたおかげでよく分かった。

オブジェクト指向が劇的に上達するたった一つのルール「-erで終わるクラスを作るな」

私が出会った、オブジェクト指向エクササイズ*1を凌ぐほどの素晴らしいルール。

「-erで終わるクラスを作るな」

私はこれを見たとき何言ってんだこいつと思った。私が当たり前のように作ってきた Renderer、Manager、Helper、Controller…などのクラスは間違いだと言うのだ。むしろこれらのクラスを「作り出す」ことができるということがオブジェクト指向をマスターしている証だとさえ思っていた。

しかし、このルールを実践した後、私の考えは180度変わった。
Renderer、Manager、Helper、Controller…
これらのオブジェクトは現実に存在しないクソオブジェクトだ。
現実に存在しない架空のオブジェクトを作ってはいけないのだ。

今すぐ「なんとかer」をやめるべき理由

責務の配置を間違う

「なんとかer」というオブジェクトは現実に存在するオブジェクトではなくプログラマーの都合により生み出された架空のものだ。なので本来負うべきではない責務を負わせたり、逆に負うべき責務に気付かなかったりする。
例として、私が作っていた将棋ゲームの汚いコードの一部を示す。

class Koma
{
    int kind;               // 駒の種類
    int boardX, boardY;     // 盤上の位置

    int timer;              // アニメーションタイマー
    float posX, posY;       // スクリーン上の位置
    int status;             // アニメーション状態

    // 盤上の位置(destX, destY)に移動できるかどうか返すメソッド
    public bool CanMove(int destX, int destY)
    {
        // 駒の種類によって動けるかどうかを判定
    }

    // 駒を移動する
    public void Move(int destX, int destY)
    {
        // 盤上の位置を移動
        boardX = destX;
        boardY = destY;

        timer = 30; //30フレームかけて移動
        status = 1; //アニメーション状態を「移動中」にセット
    }

    // 駒の更新(画面更新毎に呼ばれる)
    public void Update()
    {
        if (status == 1 && timer > 0) {
            posX += (CELL_SIZE * boardX - posX) / 10.0f;  // 目標へ滑らかに移動する
            posY += (CELL_SIZE * boardY - posY) / 10.0f;
            timer--;
        }
        // ...以後アニメーション関係の処理(500行くらい)
    }

    // 駒をスクリーンに描画
    public void Render(IKomaRenderer renderer)
    {
        renderer.Render(this.kind, this.posX, this.posY); // Rendererに処理を委譲
    }
}

// 駒の描画クラス
class KomaRenderer : IKomaRenderer
{
    public void Render(int kind, float posX, float posY)
    {
        Graphic g = GraphicManager.Get(kind);
        g.Draw(posX, posY);
    }
}

駒クラスは駒の種類、盤上の位置、スクリーン上の位置を管理していて、盤上の位置が変更されると内部でアニメーションのステータスが変更される。
そして60分の1秒ごとにUpdate()が呼び出されることで滑らかに移動する。
駒の描画はKomaRendererに委譲されておりインターフェースを介して駒をスクリーン上へ描画する。
問題点はお分かりだろう。描画こそ別クラスに委譲されているが、Komaクラスには依然として将棋に関する知識とアニメーションに関する知識が同居している。そのためKomaクラスの役割はその名前から想像できる範疇を超えており、コードは1000行近くまで醜く太っていた。

なぜこうなったのか。このコードを書くときに私はこう考えていた。

駒のスクリーン上の位置は誰が管理するのが適切か?駒に関係することだからやはりKomaか?Rendererは描画することに集中するべきだし「描画する人」が座標をプロパティとして持つのはどう考えてもおかしい…。だからRendererではないだろう。ならばやはりKomaしかない。

こうして無事に1000行近くのKomaクラスが誕生したのである。
もしこの時、KomaRendererではなくRenderedKoma命名していたら、駒のスクリーン上の位置を誰が管理すべきかは明白だっただろう。
私はこれまであらゆるボードゲームのプログラミングをしているときに、このような「ゲーム自体の知識とアニメーションの知識が同居する」問題に悩まされていたが、名前の付け方を変えるだけであっという間に解決してしまったのだ。

そもそもオブジェクト指向的な発想ではない

よく考えてみれば、そもそも Getter/Setter を用意したデータオブジェクトを「なんとかer」に突っ込んで処理してもらい、結果を得るというのは構造化プログラミングの発想である。
一方、上記に挙げたRenderedKomaはどうだろうか。それはディスプレイに表現された駒そのものを表しており、これこそモノ中心…すなわちオブジェクト指向ではないか!

今あるクソオブジェクトをどうやって置き換えたらいいのか

ではどうしたらいいのか。ルールを実践した結果いくつかヒントを得られたので下記に紹介する。コツとしてはそのオブジェクトが本当に表しているものを考えると良い。

Manager

みんなこの名前が大好きだ。このクラスは色々な責務を担当させてManagerという名前を付けてごまかしているのてだいたい神クラスと化す傾向がある。
もしこのクラスが単にオブジェクトのコレクションならそのままCollection、List、またはその複数形(例:CardManager → Cards)を名付ける。もしこのクラスが何かリソースの管理をしているなら、それが管理しているモノの名前を付ける(例:×MemoryManager → ○Memory)。
もしこのクラスが複数のオブジェクトを連携させるための仲介役(GameManager等)であるならこれを廃してオブジェクト同士が直接連携するようにする。そうすると結合度が高くなりすぎるというときは設計がそもそも間違っているのでオブジェクトを小さく分割して責務を再配置したりインターフェースを介して連携したりすることを検討する。

Controller

Managerの仲介役の場合に同じ。

Renderer, Printer

これらはオブジェクトの中身を外界に表現するときに使われる。Rendererという名前は表現する手続きに着目しているのでよろしくない(→手続き型への退行)。代わりに表現されたオブジェクトの方を命名する。PlayerRendererではなくRenderedPlayerという風に。

Helper

これらは大抵の場合、データベースとかネットワーク通信とかごちゃごちゃした手続きを押し込めるために作られる。
カプセル化の真意を思い出し、手続きの結果得られるモノの方をオブジェクトとして名付ける。
HTTPHelperではなくWebPageという風に。


いかがだろうか。技術書はもちろんのこと.NETやAndroidなどの大御所にまで登場する「なんとかer」が間違いだというのはにわかに信じがたいかもしれない。しかし、何か複雑な処理をするものに無理やり"なんとかer"と名付けてクラスを「作り出す」考えから脱却することでオブジェクト指向の極みを垣間見ることができるので是非実践してみてほしい。

*1:ThoughtWorksアンソロジーの中でJeff Bay氏が提唱したルール。このスライドに詳しい。

カプセル化=データの変更制限ではない

カプセル化オブジェクト指向プログラミングの三大要素*1のひとつでオブジェクト指向プログラミングの入門書の最初に出てくる言葉だ。そして、カプセル化の説明としてよく使われるのが「データを封じ込めメソッドを介してのみ変更可能とすることでデータの予期せぬ変更を防ぎプログラムを堅牢とする」という様な説明だ。実際、私も技術書や講義でこの説明をされたので長年そういうものだと信じていた。最初のうちはこの理解でも良いのかもしれないが、より良いプログラムを書きたいのであればこの認識を捨てなければならない。なぜならカプセル化=データの変更制限という認識はカプセル化の重要な意味を見落としてしまうからだ。

カプセル化されていないデータの何が悪いのか

そもそもプリミティブをそのまま使うことの何が問題なのだろうか。まずはそこからおさらいしてみたい。次のコード(C#)はロボットを指定された秒数だけ移動させる。

int time = 3;
Console.WriteLine("ロボットを" + time + "秒間移動します");
robot.Move(time);

シンプルなコードだ…。バグの入り込む余地もない。だがこのコードには重大な問題がある。それはtimeが「秒」であるというコードに現れない前提条件があることだ。もしある日突然、時間設定をミリ秒で行う仕様に変更されたらこの前提条件は崩壊し、コードのtimeに依存している場所すべてを変更しなければならなくなる。しかもコンパイラはこの前提条件をチェックしてくれないのでMove() メソッドの引数に渡された数値が時間以外…温度とか速度とかでも問題なく受け入れられてしまう。プログラマーは常にtimeの中身を気にしなければならなくなる。このように、コードに現れない前提条件を隠れた結合*2と呼ぶ。隠れた結合こそプログラムが脆弱となる要因だったのだ。

プリミティブtimeカプセル化するとどうなるか。

class Second
{
    readonly int time;

    public Second(int time)
    {
        this.time = time;
    }

    public override string ToString()
    {
        return time + "秒";
    }
}
Second time = new Second(3);
Console.WriteLine("ロボットを" + time.ToString() + "間移動します");
robot.Move(time);

timeカプセル化されSecondという意味付けがされたことで、コードが何をやっているのか一目瞭然となった。また、Move()メソッドがSecondオブジェクトを受け取るようになりコンパイラのチェックが働くようになったので、プログラマーはtimeの中身を気にする必要がなくなった。カプセル化は隠れた結合を排除することでプログラムを堅牢なものとする。

カプセル化の本質はデータを現実の物体として振舞わせること

先ほどの例ではint型の数値をカプセル化することで「秒数」として意味付けた。言い換えてみれば 、intが「秒数」という現実の物体として振舞っている。突き詰めていくと、カプセル化の本当の意味はデータを現実の物体すなわちオブジェクトとして振舞わせることだということに気付く。例えば、下記はint型のコードで管理されるトランプのカードを表すオブジェクトだ。

// トランプ
interface PlayingCard
{
    int Suit();    // スート
    int Number();    //数字
}
// int型のコードで管理されるトランプ
class IntCard : PlayingCard
{
    readonly int code;
    
    public IntCard(int code)
    {
        this.code = code;
    }

    public int Suit()
    {
        return code / 13;
    }  

    public int Number()
    {
        return code % 13 + 1;
    }

    public override string ToString()
    {
        string[] suit = {"スペード", "ハート", "クラブ", "ダイヤ"};
        return suit[this.Suit()] + "の" + this.Number();
    }
}
// 使い方
PlayingCard card = new IntCard(0);  // スペードの1

IntCardはint型のコードをカプセル化し、外界に対してはあたかもトランプのカードのように振舞わせている。フィールドはコードだけでSuitやNumberなどのデータは一切持っていない点に着目してほしい。振る舞いはあくまでも実行時に決まるものだ。コンストラクタでsuitやnumberを計算してフィールドに持たせてはいけない。データを現実の物体として振舞わせるイメージを持つと、Getter/Setterがなぜ悪いのか分かる気がする。カプセル化=データの変更制限という認識を捨てることで Getter/Setter の乱立を防ぎより洗練されたプログラムを書けるようになる。