Javaのクラスとオブジェクトについて再度解説を試みる

オブジェクト指向プログラミングの考え方については、今までこのブログでも何度か取り上げてきました。
[オブジェクト指向] - 達人プログラマーを目指して
オブジェクト指向プログラミングはプログラミング技法のすべてではないとはいえ、Javaのようなオブジェクト指向言語で本格的なプログラムを作るには理解を避けて通ることができませんし、また、関数型言語など他のパラダイムの言語を利用するにしても、オブジェクト指向の考え方をまったく理解しないまま使いこなすということは困難でしょう。オブジェクト指向の考え方はデータ構造やアルゴリズムといったことと同様に、プロフェッショナルなプログラマーが理解しておくべき基本的な素養といってもよいと思います。実際、海外では募集要項でオブジェクト指向の理解を前提とすると書かれていることが普通ですし、プログラマーの面接試験で、アルゴリズムと並んでオブジェクト指向プログラミングの基本についての正しい理解を問うケースが多いようです。
しかしながら、特殊なケースを除いて、我が国ではいまだになかなか普及していないようですね。数週間の新人研修やOJTのみで短期間に理解できるほど簡単ではないというのは事実かもしれません。それゆえ、SIの開発案件ではJavaVisualBasicといったオブジェクト指向言語を用いながらも、実際にはオブジェクト指向を利用しないで開発できるような規約を作るといったこともかなり一般的に行われているようです。
オブジェクト指向プログラミングの普及を妨げる原因の一つとして、わかりやすい入門向けの解説を作ることが難しく、また、実際にそのような解説が書かれている入門書も非常に少ないということがあると思います。実際に、Javaの入門書の場合、ずっとmainメソッドのみでifやforなどの構文を説明した後、いきなりVehicle(乗り物)クラスやCarクラスなどの説明に飛躍するといったケースも多く、初めてオブジェクト指向プログラミングを学ぶ人にとっては非常に理解しにくいところがあると思います。*1
さらに、一言でオブジェクト指向、(あるいはもっと範囲を狭めてオブジェクト指向プログラミング)と言っても、プログラミング言語によっていろいろな考え方やアプローチがあり、さまざまな人々がいろいろな解説を試みているので、学習する側の立場としてはどの考え方が正しいのか混乱に拍車がかかるというところがあるかもしれません。
私自身は「オブジェクト指向とはどう考えるべきか」という哲学的な研究をしているわけではなく、単に仕事をするうえで便利な考え方の枠組みの一つとして考えています。要するに、

  • 共通のロジックを流用しやすくする(再利用)
  • 大規模なプログラムを分割して扱い易くする(モジュール化)
  • 変化や拡張に強いプログラムにする(拡張性)
  • 設計のアイデアを共有する(パターン)
  • ビジネスルールをプログラムとしてわかりやすい形で表現する(モデル化)

といった目的を達成するための手段一つとして、欠かせないものとして考えています。
ここでは、Java言語のオブジェクト指向プログラミングの基礎となるクラスとオブジェクトについて、再度、自分なりの解説を試みてみることにしたいと思います。

staticおじさんの世界におけるクラスの役割

一昔前のJavaの解説では、「Javaは純粋なオブジェクト指向言語である」という説明のされ方をされることが多くありました。ご存じのように、Javaではどんなに簡単なプログラムでもクラスを作成することが必須です。実際、HelloWorldは以下のようにクラス内に定義する必要があります。

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

最初から、クラスというオブジェクト指向の概念を使う必要があるから、純粋なオブジェクト指向言語なのであるということです。これは、「純粋なオブジェクト指向」という言葉の定義にもよりますが、一般にはちょっと不適切で勘違いしやすい用法だと思います。確かに、このプログラムに登場する文字列や配列はオブジェクトですが、ユーザー定義のオブジェクトは利用していません。ただし、HelloWorldというユーザー定義のクラスは使っているので、クラス指向といった方がよいかもしれません。
さて、このHelloWorldプログラムと同様の形式を拡張して、すべてのクラスのメンバー(フィールド、メソッド)にstaticキーワードをつけることで、完全にstaticな世界で*2プログラムを作成することができます。staticフィールドやstaticメソッドは、Smalltalkなどの純粋なオブジェクト指向言語の用語をまねてクラス変数、クラスメソッドなどと呼ばれることもありますが、本来staticというのは静的、すなわち、動的(dynamic)と反対の意味を持つ用語です。Java言語において、staticキーワードの持つ意味はあまり理解されていないようですが、staticフィールドについては、一旦クラスローダーによってクラス定義がJVMに読み込まれたら、クラスごとに固定的に変数領域が割り当てられてずっと存在しているということを示しています。また、staticメソッドはそのようなフィールドを操作するためのメソッドとして、やはりクラスごとに定義が読み込まれます。*3
この点を理解するためには、以下のごく簡単な例を考えてみてください。

public class ClassA {
    public static int field;

    public static void method() {
        System.out.pirntln("ClassAのメソッド");
    };
}

public class ClassB {
    public static int field;

    public static void method() {
        System.out.pirntln("ClassBのメソッド");
    };
}

public void MainClass {
    public static void main(String[] args) {
        ClassA.field = 100;
        ClassB.field = 200;

        System.out.println(ClassA.field); // 100
        System.out.println(ClassB.field); // 200

        ClassA.method(); // ClassAのメソッド
        ClassB.method(); // ClassBのメソッド
    }
}

ClassA、ClassBのそれぞれに独立した変数の定義、メソッドの定義が存在し、クラスが読み込まれるとともに静的にメモリーに読み込まれます。このことは、フィールド名やメソッド名が以上の例のように同じであっても影響を受けません。特に、この例で、int型のstaticフィールドに対応するメモリー領域は、クラスがロードされると共にクラスごとに用意されることに注意してください。
このように、staticな世界におけるクラスの役割とは、単にたくさんあるフィールドやメソッドを種類ごとに分類して、適切な部品の単位に分割するというモジュール化の役割があるに過ぎません。つまり、クラスは単にstaticフィールドやstaticメソッドの入れ物として機能しています。また、Javaの場合、基本的には一つのクラスを一つのソースファイルに記述しますから、クラスというのは物理的なソースコードの分割単位としての役割もあります。英語のclassifyに分類分けするという意味があるように、クラスには分類されたものという意味もありますから、大規模なプログラムで必要な変数やメソッドの定義を適切な単位に分割して扱うというのは、オブジェクト指向以前にクラスの持つ重要な役割であることがわかります。

抽象データ型の扱えることの便利さを理解することがstaticな世界を卒業する最初の一歩*4

このように、staticおじさんの世界でも適切にクラスを定義して、フィールドやメソッドをしかるべきクラスに定義することで、大規模なプログラムを適切な単位に分割して管理することができるようになります。しかしながら、この場合の問題は、プログラムが扱える変数やパラメーターの型が基本型、文字列型、またその配列といった非常に限定された型しか扱えないということです。FORTRANCOBOLなどの昔の言語しかしらないstaticおじさん*5の問題点はプログラム言語が扱えるのはこのような限定された型のデータのみであると決めつけてしまっているところにあると思います。
ところが、実際の複雑なプログラムでは、

  • 注文をデータベースに登録する
  • 入力フォームを開く
  • カタログ一覧を検索する

といったように、プログラムで処理したい単位はintやfloatなどといった基本的な型のデータなのではなく、「注文」「入力フォーム」「カタログ項目」といった人間にとってもっと自然な単位でデータを扱いたいということがあります。JVMの仕組みを考えてみればわかるように、本当はコンピューターが扱いやすいデータというのは有限のビットの集まりとして表現できる数値や文字といったデータなのですが、そういう詳細のことは無視して、データをより抽象化して扱えるようになれば、プログラムを実際のユースケース(仕様)に近い形で記述でき、よりわかりやすく記述できるようになります。実際にJavaの場合は、intやfloatといった基本型の値だけでなく、文字列、日付、注文書といったさまざまなオブジェクトを利用することで目的に応じてあらゆるデータを変数から参照したり、メソッドのパラメータや戻り値としてやり取りできるようになります。
もちろん、オブジェクトを真に使いこなせるようになるには、カプセル化ポリモーフィズムといったさまざまな事柄を理解する必要があるのですが、いきなりそのような概念を理解しようとせず、まずはこのようなさまざまなデータを扱えることの便利さを理解することが、staticな世界を卒業する最初の一歩として重要なのではないでしょうか。

一般のJavaのオブジェクトを理解する前に、まず配列オブジェクトの性質を理解するとよい

ところで、一般のオブジェクトはクラスを使って自分で定義し、newを使ってメモリーに割り当てて使うわけですが、最初に勉強する際に、オブジェクトの性質と同時にクラスの定義方法を学習するのは、一気にいろいろな概念を覚えなくてはならずなかなか難しいかもしれません。そこでお勧めなのは、最初に配列の性質をきちんと理解するという学習手順です。配列はクラスで定義する必要がないため特殊ですが、Javaの配列は

  • new演算子を使ってオブジェクトがJVMのヒープと呼ばれるメモリー領域に動的に生成される。
  • 配列型変数は参照型の変数である。
  • 多数の値をまとめて扱う抽象データ型の一種である。

という性質を考えると、一般のオブジェクトと共通の性質を備えていることがわかります。
一般的な入門書の説明では、配列は複数の値を読み書きする箱が並んだ絵が説明に使われることが多くあります。一個の変数が値を格納する箱なので、これが連続して横に並んだものが配列というわけです。これは、昔のBASICやC、COBOLなど多くの言語の配列の説明には最適ですし、この説明自身は間違っていないのですが、これだけではJavaの配列が持つ以上の重要な性質を見逃してしまうことになります。

int[] x = new int[3];
int[] y = x;

x[0] = 1;
x[1] = 2;
x[2] = 3;

System.out.println(y[0]); // 1
System.out.println(y[1]); // 2
System.out.println(y[2]); // 3

以上のコードは理解している人にとっては当然のことですが、配列オブジェクトは一個しか生成されておらず、xとyという二つの別々の変数によって同一オブジェクトが参照される状態となっているため、変数xの要素を変更すると、連動してyの参照する値も変わって見えるということです。これは、独立した値の箱として扱われる普通のint型変数の振る舞いと大きく違っています。

int x = 1;
int y = x;

x = 2;

System.out.println(y); // 1のまま

もちろん、参照型の変数はオブジェクトを参照するためのものなので、nullという特別な値が代入されることで何もオブジェクトを指していない状態にもなれます。この状態の変数にアクセスすると、有名なNullPointerExceptionが発生することになります。
なお、C言語そっくりにするため、Javaの配列を宣言する際には中括弧の初期化子を使って宣言することも可能です。

int[] x = {1, 2, 3};

あるいは、Javaでは推奨されませんが、以下のように書くこともできて、これだと本当にC言語のように見えます。

int x[] = {1, 2, 3};

ただし、似ているようにみえるのは見かけだけで、C言語の配列とはメモリーの割り当て方が全く異なることに気を付けてください。このように、初期化子を使った場合でも、実際にはnewを使って動的に生成した場合と同様に配列オブジェクトは常に実行時に動的に生成されてメモリーに確保されます。*6
独自のメソッドを定義できないなど制約もありますが、配列は実際にオブジェクトの一種です。まずは、この配列の意味や配列変数の使い方に習熟することで、動的にオブジェクトを生成して使うというstaticでないプログラミングの世界への第一歩を踏み出すことができます。

ユーザー定義のオブジェクトを定義して生成する手段としてのクラス

以上で説明した、動的な配列オブジェクトの生成と配列型変数によるオブジェクトの参照ということが理解できたら、一般のオブジェクトを理解することも難しくありません。配列との違いは、以下の点です。

  • クラス内部にメンバー(フィールド、メソッド)を自由に定義できる。
  • オブジェクトを生成するもとになったクラスがそのオブジェクトを参照するための変数の型となる。

配列の場合は、同一の型の値の集合しか表現できませんが、クラスを使えば、任意の値の組み合わせからなる構造のオブジェクトを生成できるようになります。

public class Person {
    public int age;
    public String name;
}

Person personX = new Person();
Person personY = personX; // 二つの変数が同じオブジェクトを参照するようになる

personX.age = 20;
personX.name = "test";

System.out.println(personY.age); // 20
System.out.println(personY.name); // test

フィールドを自由に定義できることと、要素にアクセスする際の演算子がピリオドである点を除けば、配列オブジェクトとまったく同様の性質を持っていることがわかります。要するに、Java言語においてオブジェクトとは、ヒープに割り当てられたメモリ領域で、オブジェクト生成元のクラスを型として持つ変数によって参照されるものということになります。まずは、データの塊がヒープに生成されて、変数を使ってそれを参照して使うということをイメージできるようになることが何よりも大切です。つまり、Javaオブジェクト指向プログラミングを理解する前提としては、

  • 変数の宣言とは独立して実行時にオブジェクトが生成される
  • newされるたびに新しいオブジェクトが生成されるので、同じ型でも複数のオブジェクトが存在する
  • 変数は生成したオブジェクトを参照する
  • 変数が何も参照しない場合はnullという値を持つ
  • 変数の数とヒープメモリー上のオブジェクトの数は一般に一致しない
  • 代入によってオブジェクトのコピーは作成されず、複数の変数が同一のオブジェクトを参照するようになる。

という性質をしっかりと理解することが重要だと思います。クラスがロードされたタイミングで最初から用意されるstaticなメモリ領域とは異なり、newされるたびに新しいオブジェクトが動的にヒープに割り当てられていきます。
ところで、どうしてJavaのオブジェクトには、このような性質があるのでしょうか。これも、配列の性質を考えてみれば納得ができると思います。配列は一般にはたくさんの要素を含むので、あらかじめ静的に変数の領域を確保したり、メソッドが呼び出されるつどメモリーを毎回スタックに割り当てるのはあまり効率的とは言えません。実行時に必要なサイズで動的にメモリーを割り当てて、必要がなくなるまで(ガーベッジコレクションされる)ヒープに確保して使うというのは、一般的なプログラムでは効率的と考えられます。さらに、変数への代入時やパラメーターの受け渡し時にすべての値のコピーを行うよりも、単に既にメモリー上にあるオブジェクトを変数が参照するようにした方が、プログラムの実行性能からも有利です。他の言語では、さまざまな方法でオブジェクトを割り当てることができるものがありますが、Javaの場合は常に動的にヒープに割り当てて、変数から参照するというという決まりになっています。常に、最適というわけではないかもしれませんが、JVMガーベッジコレクションの仕組みと合わせて、この割り切った仕様により、Javaでは効率的にオブジェクトを扱えるようになっていると考えることができます。

オブジェクト指向プログラミングは、オブジェクトを定義するクラスとモジュールとしてのクラスを同一視することから始まる

前節で説明したクラスは、他の言語では構造体、レコード、あるいはユーザ定義型と呼ばれるものに過ぎず、複数の値から構成されるデータ構造を定義していました。
しかし、ここで本当に理解すべき重要なことは、Javaではオブジェクトの構造を定義して、そこから生成されたオブジェクトを参照する変数の型となるためのクラスと、「staticおじさんの世界におけるクラスの役割」の節で説明した、大きなプログラムを分割するモジュールとしての役割を持つクラスを同一視しているということにあります。
まず、Javaにおいて、この事実が当たり前と思えるようになれば、相当オブジェクト指向で考えられるようになっていると言えます。これは、Javaで適切な単位でクラスを設計し、オブジェクト指向でプログラミングできるようになるための大前提であり、オブ脳の基本回路が脳に形成されたといっていいでしょう。
この同一視により、カプセル化という考え方も自然に理解できると思います。つまり、モジュールとして何を一緒に含めるのが自然であるかということを考えれば、オブジェクトに含まれるデータに関連する処理をメソッドとして一緒にクラスに定義し、フィールド自体はprivateにして外部から勝手に操作されないようにする考え方も納得がいくのではないでしょうか。String型のオブジェクトに含まれる文字列を加工するためのメソッドはStringクラスに定義されているので、文字列オブジェクトに対してそのまま呼び出すことができます。一般的にはこのように関連するデータと処理を同じクラス内に定義することが基本となります。
どうすれば、オブ脳が鍛えられるのでしょうか。こればかりは、JDKオープンソースのライブラリーをお手本として、また自分なりに似たような設計を試しながらプログラミングの経験を積むことに尽きると思います。

まとめ

ここでは、一般の入門書にあるような説明とは違ったアプローチで、Javaのクラスとオブジェクトについて理解するための説明を試みてみました。

  • オブジェクトを考えない世界(staticな世界)ではクラスは単にモジュール化の手段
  • オブジェクトとは動的にメモリーに割り当てられて変数から参照される値の集合
  • Javaの配列はオブジェクトの一種。一般のオブジェクトを理解する前に、メモリーへの割り当てや変数からの参照など配列の性質を完全に理解するのが近道
  • オブジェクトは変数から参照したり、メソッドのパラメーターや戻り値として簡単に受け渡すことができる
  • 一般のオブジェクトを定義して、生成するためにはクラスを利用する
  • オブジェクトを定義して生成する手段してのクラスとモジュール作成手段としてのクラスを同一視することがJavaにおけるOOPの始まり

実際には、最後の項目はかなり内容を端折っているので、クラスを有効なモジュールとして設計するためには、カプセル化の他に継承やポリモーフィズムなどさまざまなことを理解する必要があります。
いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について - 達人プログラマーを目指して
さらに、実際の問題を上手に分割してクラスに割り当てるということができるためには、やはり、それなりの経験を積んで勘を養う必要があります。*7そして、クラスをオブジェクトと結びつけて自然な単位として扱うのがなぜよいのかといったことを納得するには時間がかかるでしょう。しかし、プログラミング言語としてのオブジェクトやクラスの意味や存在意義はここに書いたように非常に単純なものに過ぎないということもできます。
もちろん、オブジェクト指向プログラミング言語で実現する方法はさまざまですし、Rubyなど、より純粋な言語を使って学ぶべきであるという意見もあるかもしれません。しかし、大部分のJavaプログラマーにとっては、モジュール、配列、構造体といった言語の仕組みの自然な拡張として、クラスやオブジェクトを理解するというパスがわかりやすいのではないかと思いますが、いかがでしょうか。乗り物や動物を使った説明や、逆に専門的すぎて難しい解説を読んでよく理解できなかったという人は、まずは、ここに書いたようにプログラミング上の当たり前の考え方との比較から徐々に理解するのがよいのではないかと思います。そして、この基本が理解できたら、お手本となるソースはいくらでも転がっているのですから、オブジェクト指向の良いクラスライブラリーを読んだり、拡張したりすることに是非挑戦してみてください。

*1:ただし、オブジェクト指向プログラミングを動物や乗り物で解説するのは和書だけでなく、世界的にも一般的な傾向のようです。

*2:ただし、厳密には文字列や配列オブジェクトは除く

*3:勘違いしやすいところですが、メソッド定義はstaticでもそうでなくても静的にクラスごとに読み込まれます。ただし、staticでないメソッドは暗黙のthisオブジェクトを受け取ることで、動的なインスタンス変数にアクセスできる点が違います。

*4:抽象データ型や参照はオブジェクト指向言語に限らず重要だと思います。スクリプト言語Perlでも、バージョン5から参照が使えるようになって随分と高度なプログラミングが可能になっていますし、このあたりは本格的なプログラミングには欠かせないツールなのだと思います。

*5:本家のstaticおじさんはC#VisualBasic使いなので、適切でなかったかもしれません。ここではオブジェクト嫌いなおじさんという意味の一般用語として使っています。

*6:重要な違いとして、C言語の配列サイズはコンパイル時に決定されますが、Javaの配列は実行時に決められます。

*7:経験を補うものとして、デザインパターンなどがあるわけですが。

普通の構造化プログラマーがオブジェクト指向の存在意義を理解するコツ

オブジェクト指向言語の存在意義を理解するのは難しい?

id:amapetasさんによる、ちょっと興味深い記事がありました。
オブジェクト指向言語が流行した必然性について考える(1) - Programmer’s Log
そして、その記事の中で説明されているのですが、やはり、C言語など構造化言語のプログラマーにとってはオブジェクト指向の存在意義を理解するのがなかなか難しいところがあるようですね。

初心者向けの書籍を最近読んでいないので、最近の書籍ではうまく説明されているのかもしれませんが、そんな話を聞いたことがないので、たぶん今でもオブジェクト指向に関する説明の始まり方は

「世界は全部オブジェクトで出来ているんじゃぁぁーーー」

という「オブジェクト至高教への洗脳」から始まっているものと推察しますw テンション高めの説明から入るのでドン引きする人多数な感じがかなりアレですね。オブジェクト指向の説明は独特な説明から入るので宗教っぽいんですよね、若干。宗教という表現をさけるなら思想、哲学といえば柔らかいでしょうか。

僕がその手の初心者向け書籍を読んだ時に思った事は「なぜオブジェクト指向言語(Java)で書く必要があるのか? 構造化言語(C言語等)ではダメなのか? 何が違い、何がメリットなのか?」にうまく答えられていない、という疑問でした。オブジェクト指向の世界観の説明で手一杯でそこらへんをうまく説明できていない気がしました。

構造化言語(C言語等)ではできなくて、オブジェクト指向言語(JavaC++等)で出来るようになったものとは何か?

引用元の記事ではオブジェクト指向自然言語の構造と関連させて論じているのですが、ここでは論点を変えて普通の構造化言語のプログラマーオブジェクト指向のメリットを理解してもらうためにはどのように説明したらよいかという点について考えてみたいと思います。

伝統的なプログラマーの頭の中にある数のイメージ

我々は小さい頃から、まず、ものを数えるということから自然数として数の概念を習得します。そして、成長するにしたがって、金額やテストの点数など、他のところで使われる数も存在することを体験を通して徐々に学んでいきますが、日常生活では基本的には整数は数えたり、大小比較のために使うものという考え方が根強く存在するでしょう。
もちろん、このような普通の整数の概念は(範囲に注意すれば)プログラムを使って表現することができます。しかしながら、コンピューターの内部で表現される整数が特定の個数のビット(あるいはバイト列)からなるデータに過ぎないという発想は本来一般人にはない考え方であると思います。
実際、コンピューターの整数データは32ビットや64ビットなど表現可能な値の範囲という厄介な性質がある一方で、画像を構成する一個一個の画素(ピクセル)の明るさや色、音声データ、通信データなど様々なところで利用されています。ベテランのプログラマーの頭の中にある整数のイメージは、一般人のそれとは違っているのかもしれません。ビットやバイトなどを意識する必要のある(意識できる)比較的低水準な言語を利用することで、プログラマーはコンピューターの構造と近いレベルで考える必要がある反面、効率的なプログラムを記述できます。
ここで重要なのは、コンピューターの構造上の都合で決まっている32ビットや64ビットの情報を使って表現できる値を使う必要があるということであって、これは伝統的なプログラマーの頭では自然な単位なのかもしれませんが、一般人の頭ではまったく自然な単位ではないということです。しかし、コンピューターの処理能力の急激な進歩に従って、最近のアプリケーションはどんどん高度な機能を求められるようになってきています。たとえば、銀行のシステムの金額計算であれば、32ビットの範囲を超えたなどという理由で計算結果が不正になることなどはあってはいけません。

オブジェクトはデータを抽象化したもの

Javaなどのオブジェクト指向言語では、クラスを使ってデータ型を抽象化することで、文字列、誤差なく計算できる金額、日付、リストなどアプリケーションの記述に便利な値(=オブジェクト)を生成して利用できるようになります。実際、Javaでは文字列は基本型に準ずる形で簡単に扱うことができるようになっていますが、単純な文字列結合のような処理であっても、本来のVM上の動作から考えるとかなり複雑な処理が必要なのですが、そういった処理は抽象化されているので普通は考える必要がありません。
コンピューター内部の構造によらない抽象化されたデータ型を利用したり、独自にクラスとして定義したりできることは、このようなオブジェクト指向言語のメリットの一つと考えられます。コンピューター(VM)の作りの制約に縛られた基本型の値を使う代わりに、適切に抽象化されたオブジェクトを使うことで、ビットやバイトといった発想を忘れてよりアプリケーションの要求に近い塊の単位で簡単にロジックを組むことができるようになるのです。
そして、オブジェクトに対して呼び出し可能なメソッドは、intなどの基本データ型に対する+などの演算に近いものであると考えることができます。Javaでは残念ながら独自の演算子を定義することはできませんが、実際にC++などの多くの言語ではクラスに対して演算子を定義することができます。
さらに、基本型の演算に対しても一種のポリモーフィズムは存在しています。*1実際、Javaのint型とfloat型では同じ32ビットでもまったく異なる内部表現を使っているのですが、同じ+演算子を使って足し算を実行することができます。しかし、両者はオーバーフローの有無など、かなり異なる振る舞いをします。

オブジェクト指向の入門書が理解を妨げる?

本来はSmalltalkRubyのように「すべてがオブジェクト」という純粋なオブジェクト指向の世界で考えた方が分かりやすいのですが、普通の構造化言語のプログラマーが最初にオブジェクト指向を学習するには、「社長と社員との関連」「哺乳類も犬もすべてオブジェクト」などということを考えるよりも、intなどの基本型や構造体の値に対する自然な拡張として、段階的にオブジェクトを考える方がしっくりくるのではないでしょうか?私自身も最初にオブジェクト指向を学習した際にはそのようなプロセスで徐々に理解しました。
多くのオブジェクト指向の入門書は、伝統的なプログラミングの考え方とはかけ離れた世界でオブジェクト指向を語ろうとする傾向があるので、かえって構造化プログラミングの知識があると理解を妨げるところがあるかもしれません。つまり、これらの入門書では、ちょっと便利なデータ型を定義できるという段階をスキップして、いきなり実世界の「もの」の関係をモデル化するところから説明しようとするために、かえってプログラミングとの関連性が見えにくくなっているところはあると思います。
つまり、Objectを普通の英語の意味で実世界の「もの」と考えるのではなく、コンピューターのメモリ上に確保された値のことを指す専門用語としてとらえた方が、構造化言語のプログラマーにとってはしっくりくるのではないでしょうか。たまたま、顧客など実世界のエンティティに対応したオブジェクトも必要かもしれませんが、プログラムの都合上で必要な値の塊も立派なオブジェクトになり得ます。

プログラミングでオブジェクト指向を習得するステップ

ところで、C++、Objective C、Object PascalJavaといった言語では、比較的低水準な伝統的なプログラマー脳が必要な基本型と演算子を使ってコーディングを行うことも、クラスライブラリーを利用して高水準のオブジェクト指向プログラミングを行うことも可能です。複数のパラダイムが混在しているため、なかなかい理解しにくいというところもあるかもしれません。
まずは、配列、文字列、日付といった抽象データ型がオブジェクトの一種なのであるというところから慣れ、次に、

あたりを順番に理解していくようにすれば、理解しやすいのではないかと思います。そして、プログラミングで正しく独自のクラスの作成ができるようになってくれば、自然に業務ドメインなど実世界の問題も、クラスを使ってモデリングする力がついてくるはずです。

*1:Javaを含めて、ここで説明する数値演算に対するポリモーフィズムコンパイル時に決定されるので実際にはオーバーロードされたメソッドの呼び分けに近いものですが。

*2:簡単なスタックやリストなどを自作してみるのも学習のためにはよいですね。

staticおじさんとオブジェクトおじさんはお互いに分かり合えるようになるかもしれません。

先日書いたstaticおじさん達に伝えたい、手続き指向とオブジェクト指向の再利用の考え方の違いについて - 達人プログラマーを目指してのエントリに、なんと、みながわけんじ氏ご本人よりコメントを頂きました。もともとは一般のstaticおじさん達(英語ではstatic ojisansという感じ)に向けて書いたのですが、思いがけず、元祖staticおじさん(The static ojisanあるいはMister staticといった感じ)ご本人からのご意見をいただき、本当に嬉しく思います。

オブジェクト指向の再利用性と非オブジェクト指向の関数やサブルーチンとの違いを明確に示していないから
いろいろ理屈を込めても無駄ではないでしょうか?
誰かが作ったクラスを継承して再利用したところで、バグが少なくて、メンテナンス性がいいものができるでしょうか?
そんなものをあてにするより、天才が作ったクラスライブラリやフレームワークを利用して、自分はstaticで作ってしまったほうが、
よっぽど開発効率がよい!というのが今の考え方です。これが今時点で勝つための方法です。
今時、再利用云々いっているのは、十年古い考え方で、私はユーザー企業なので、そういう古臭い会社とは、おつきあいしません。
私が仕事をぜひとも依頼したい人はマイクロソフトのクラスライブラリをよく知っている技術者です。
私の考え方は古くない、むしろコンテンポラリーだと認めていただけないと、たぶん、あなたはビジネスチャンスを潰すでしょう。

いつも通り、なかなかに手厳しいご意見なのですが、ご本人のブログに以下のように書かれていました。

レイヤーつまりソフトウェアアーキテクチャについて正しい認識、センスを持つということで彼に同意です。

いいクラスライブラリ、いいライブラリ関数というレイヤーの上で、いい業務アプリケーションが開発できるというのが素直な考え方です。多くのかたがレイヤーという概念というかセンスを持っていないことに日本のプログラム開発の悲劇がありそうです。レイヤーという概念を把握せずに、クラス分け、つまりプログラムをクラスというサブプログラムに分割してしまうことにより、趣味の悪いメンテナンス性の悪いプログラムができあがってしまう。

私の自論としては、最上位のレイヤーは関数やstatic関数でかなりいけてしまう、その下のレイヤーは現代の開発ツールですとクラスライブラリ化、コンポーネント化されているのでオブジェクト指向となります。だからと言って、業務アプリケーション開発者はオブジェクト指向の勉強をしなくていいということにはなりません。クラスライブラリを使いこなすには、かなりの努力が必要です。

なるほど、以上を読むとオブジェクトおじさんの私としても、かなり共感できるところがありますね。つまり、アプリケーション開発(特に業務アプリケーション開発)の世界において、オブジェクトは部品として利用するけれども、最上位のレイヤーでは手続き的な記述で十分であるということです。staticにするかどうかはともかく、多くの業務システムは上位のアプリケーション層やプレゼンテーション層はステートレスで手続き的な処理を記述することが一般的ですし、並行処理の観点やスケーラビリティの観点からも、それが好ましい場合が実際に少なくないのです。実際、Springなどを使ったJavaのサーバーサイドの開発ではコントローラーやサービスといったクラスはステートレスで、かつシングルトン(インスタンスが一つ)として作成することが一般的であり、実質的にはstaticメソッドで手続きを記述するといったことと大差はありません。また、AccessVBAなどでもデータアクセスやボタン、入力項目などの画面部品はオブジェクトとして再利用しますが、処理は標準モジュールと言語の構文を使って手続き的に記述すればよいことが多いです。
実際、以前にJava EEや.NETはCOBOLやVB6よりも本当に生産性が高いか? - 達人プログラマーを目指してでも書いたのですが、たいしたロジックが不要なデータベースのCRUD処理を中心とした業務システムで、無意味なオブジェクトを多用すればかえって生産性が大きく下がるといったことも事実なのです。
残念なことに、単純にUPDATE文を一つ発行すれば済む処理なのに、SIerの不適切なフレームワークの規約に従う必要から、大量のクラスを作成して何度も値の詰め替えのみ繰り返しながら、ビジネスロジックであるSQL文の実行を行い、結果も逆順に詰め替えてようやく画面に表示するといったようなケースをいろいろな現場で目撃してきました。(侵略的なフレームワーク - 達人プログラマーを目指して)ひどいケースでは本質的なロジックが占める割合が分量からいって10%以下というケースも珍しいことではありません。このような設計は開発工数や保守費用を水増しして売上を増大させるというSIerのメリットにはなっても、決してユーザ企業のメリットになることはありません。もしかしたら、みながわさんもそのようなSIerのダメダメな自称オブジェクト指向フレームワークの被害者の一人なのかもしれないと思いました。
私自身本当にそのような無意味なオブジェクト(お邪魔妖怪アンチパターン)が世の中に氾濫することを心から憎みますし、そういうフレームワークが広く使われているということは本当に問題であると考えています。そうであれば、みながわさんの主張されるようにマイクロソフトなどが提供する使い勝手のよいフレームワーク言語ツールを活用して、生産性を上げるということは(ベンダロックインなどの問題を考えないのであれば)有効な手段であると考えます。
CRUD処理を行うような簡単なアプリケーションに対して、画面を作成するのであれば、

  • フォームを生成する
  • フォームに入力フィールドを追加する
  • フォームに検索グリッドを追加する
  • 検索グリッドにDBのXXXテーブルをバインドする
  • フォームにボタンを配置する

といった非常に簡単な記述(場合によってはGUIツール上のプロパティ設定)により、アプリケーションが完成します。この場合は、フォームやDBグリッドといったようなオブジェクトがプログラム記述の言葉としても使われているのですが、VBAのような上位レベルのプログラムをこのような特化した「言葉」を用いたある種のDSLドメイン特化言語)であると考えることができます。*1GUI部品のクラスライブラリがDSLのモデルを提供し、それを利用する上位層は手続き的なスクリプトやグラフィカルなエディタ上の設定をDSLとして利用すればよいのです。この場合のDSLは一般にデータベースアプリ構築や画面構築に特化した言語であり、汎用のオブジェクト指向言語のように継承したりデザインパターンを使ったりすることは必要ありません。このようなDSLという考え方については、最近以下のような書籍が出版されています。

Domain-Specific Languages (Addison-Wesley Signature Series (Fowler))

Domain-Specific Languages (Addison-Wesley Signature Series (Fowler))

DSLs in Action

DSLs in Action

このように、データベースアプリケーションの開発はマイクロソフトやオラクル、Salesforceといったベンダが最も得意とする領域(ドメイン)なのであり、それらの提供する言語やツールをDSLとして活用することで効率的にアプリケーションを作成するということが可能です。
ただし、ここで見落としてはならない重大な落とし穴があるという点に注意が必要なのです。マイクロソフトが提供する便利な部品はあくまでもCRUD処理のような汎用的なデータベースアプリケーションを開発することを念頭においたしかけのみを提供しているということです。したがって、処理が純粋なデータアクセス処理の範囲であれば非常に有効に機能するのですが、業務処理そのものが複雑なドメインではまったく力不足ということがあるのです。多種類の注文の銘柄を扱ったり、顧客の種類に応じてチェックロジックが微妙に異なったりするといったことを汎用部品はカプセル化してくれません。そのような複雑な業務ロジックを画面部品とDBアクセス部品と手続的なif文やループ文のみで表現した場合、きわめて複雑でメンテナンス不可能なスパゲッティコードになったり、ほんの一部しか異なる部分が無いような大量の関数がいたるところにコピーされてしまうといった状況に陥ってしまうのです。
画面部品はオブジェクトとして実際には複雑な描画ロジックをカプセル化してくれています。だから、わざわざ昔のBASICのように線を引く、色を塗るといった低レベルのルーチンの組み合わせを呼び出さなくても簡単に画面に配置することで再利用できます。同様に、本来は複雑な注文や顧客といったオブジェクトも同様に部品として再利用可能なオブジェクト指向ドメインモデルを作って上位のアプリケーション層やプレゼンテーション層から再利用するという発想だって可能なのです。それが、本ブログで既に何度も紹介している
エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

で書かれているレイヤ化アーキテクチャドメインモデルの構築といった発想につながってきます。なお、注文などのドメインモデルを構築する際にはオブジェクト指向の分析・設計・コーディングが必要なわけですが、そのようなモデルが構築できればその上に必要に応じてDSLを構築することすら可能なのです。そうすることで、画面部品を配置したりするのと同じような気軽さで、業務のオブジェクトを簡易言語やツールで簡単に利用したりすることもできます。そして、この場合も画面とDBとの間にドメイン層が加わってはいるものの、staticおじさんの主張するように上位のプレゼン層やアプリケーション層では継承などのバリバリのオブジェクト指向プログラミングは不要であることが言えます。
結局、staticおじさんとオブジェクトおじさん(DDDおじさん)の違いは、常に与えられたオブジェクトのみを利用してアプリケーションを作ると考えるか、必要に応じて独自の部品を作ろうと考えるかの違いに過ぎないのではないでしょうか。いずれにしても、SIerの変なフレームワークが嫌いということでは共通していますし、レイヤーという考え方を重視するという点、部品の利用を重視する点においても共通しています。(少なくも元祖の)staticおじさんとオブジェクトおじさんは、本来はお互いに分かり合えるようになるかもしれないと思いました。

*1:DSLの実現方法はオブジェクトモデル上に被せた手続き的なファサードに限らず関数型やルール記述言語などの形式が適する場合ももちろん考えられます。実現手段や使い勝手として最適なものを選択すべきという立場です。

staticおじさん達に伝えたい、手続き指向とオブジェクト指向の再利用の考え方の違いについて

何が良いプログラムかという点はもちろん人やコンテキストによって異なりますが、少なくともプログラマーとしての私の信念としては、

  • 機能拡張や変更が容易なプログラム
  • 単体試験によって正しく動作することの検証が容易なプログラム
  • どういった内容が記述されているか理解しやすいプログラム

といったものこそ、「品質の高い」プログラムが持つべき性質として、まず真っ先に挙げるべき事項であると考えています。もちろん、前提として顧客の要件に従うということは大切なことです。しかし、一般に要件は長期にわたって変更されるものですし、使い捨てのプログラムを除けば、プログラムを長期にわたって保守するコストという点も見過ごすべきではありません。したがって、ユーザーの目には触れない上記の性質をもっと重視すべきだと思うのです。

DRYの原理

上記のような性質を満たすプログラムを作る上で大切になってくる原理として、DRYの原理という原理が知られています。これは、Don't Repeat Yourselfということで、同じ作業を2度と繰り返すなという考え方です。同じようなロジックのコードが現れたら、メソッドとして一か所にくくり出すなどの共通化を図れということですね。この原理は

達人プログラマー―システム開発の職人から名匠への道

達人プログラマー―システム開発の職人から名匠への道

  • 作者: アンドリューハント,デビッドトーマス,Andrew Hunt,David Thomas,村上雅章
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2000/11
  • メディア: 単行本
  • 購入: 42人 クリック: 1,099回
  • この商品を含むブログ (347件) を見る
でも、達人プログラマーが守るべき重要な原理の一つとして紹介されています。DRYの原理が守られていれば、

  • そもそも記述するコードを少なくできる。
  • ロジックの単体試験が一か所ですむ。
  • バグの修正や機能変更があった場合でも一か所の修正ですむ。
  • プログラムのサイズが小さくなるからコードが理解しやすくなる。
  • なによりも、プログラマーにとってきれいなコードを書いているという満足感がある。

など、さまざまなメリットが得られます。
もちろん、さまざまな制約から、原理主義的に完璧にこの原理を守るということは不可能ですし、また、特に大規模な構造の設計になれば、意図的にDRYの原理を破った方がメリットが高いということも考えられます。達人プログラマーの本でも理想的なプログラムは決して作れないということが書かれています。したがって、実践的な職業プログラマーに対する教えとして、あくまでもチャンスがあれば少しでも目指すべき方向という意味でこの原理を紹介しているのだと理解しています。

staticおじさんの頭の中における再利用のイメージ

だから、私は「できれば、DRYなコードを目指したい。それができないときは、罪の意識を持って実行し、チャンスがあればリファクタリングしたい。」というような考え方でプログラムを書きたいと考えてきました。
しかし、SI業界の多くの現場においては、共有すべき目標という意味においてすら、このDRYの原理が守られていないということがあるのだということを知りました。業界の情シス部門やSIerで何十年前にCOBOLアセンブラなどで開発を担当し、現在は現役でコードを読むことも書くこともないが、開発基準やアーキテクチャを決める上で発言権のある、いわゆる上級エンジニアという立場の方々が多数いらっしゃいます。ここではそのような方々をちょっと親しみを込めて総称的にstaticおじさんと呼ぶことにしましょう。つまり、まったくIT技術と無関係の方々ではなく、長年専門の技術者として業務システムの開発や運用に関する仕事を経験され、組織内で技術面での意思決定者として、かなり高い地位を得ているような方々です。
もともとのモデルはこの方ですが、ここではこの業界ではどこにでもいそうな一般的な技術者を指すものとします。
実はオブジェクト指向ってしっくりこないんです!:気分はstatic!:エンジニアライフ
あるいは、
高慢と偏見(1)隣は何をする人ぞ:Press Enter■:エンジニアライフ
に登場する三浦マネージャのような人をイメージしています。
むしろ、プログラミングをまったく経験したことのない人であれば、できる限り無駄を省くDRYの法則というのは合理的であり、直感的にメリットを理解しやすいと思います。しかし、staticおじさんの場合は、

  • コードを共有化すると、共有しているプログラムを修正した場合の修正の影響範囲が広がってしまう。
  • 機能ごとに似たようなコードをコピーし、独立したプログラムとして開発すれば、それぞれ独立して変更できるからメンテナンスが楽。
  • コピペを中心とした開発であれば開発担当のPGのスキルも低くてすみ、外注コストも削減できる。
  • ホストからのダウンサイジングもある程度進んでおり、今時フルスクラッチで新規開発する案件は少なく、2次開発案件では部分的なコピペで機能を追加できれば十分。
  • だから、小難しい理屈を使いこなすような達人プログラマーなどは不要であり、若いうちにSEやPMになることを考えた方がよい。

というような考え方をされる場合が多いということが、最近私もそのような方々と何回か接するにつれて、ようやくわかってきました。もちろん、そのようなstaticおじさん達を「老害」などと呼んで最初から相手にしないでおくということもできるかもしれません。しかし、正しくコミュニケーションするためには、冷静に相手の立場に立って考える必要があります。そう思って、staticおじさんの気持ちを考えると、彼らの考え方も一理あるのではと思うところが出てきます。
まず、アセンブリCOBOLのような言語では、オブジェクト指向言語で一般的なカプセル化という考え方がきわめて弱いということがあります。変数は静的なグローバル変数が中心であり、処理をくくりだして共通化しても、それは見かけ上コードサイズが削減されたということでしかなく、結局各処理は密に結合したものになってしまいます。また、言語自体のサポートとしてはポリモーフィズムという考え方がなく*1、共通処理の呼び出し元と共通処理とは結局コンパイル時に結合されて、一体の目的ファイルにコンパイルされます。これも、共通処理とその呼び出し側が密に結合する原因となります。
つまり、staticおじさんの世界観におけるコードの共通化は、単にコードのサイズを少し削減するという手段でしかないのです。逆に、下手にコードを共通化したことによって、スパゲッティコードになったり、影響範囲が理解しにくくなったりするというデメリットの大きさを考えればあまりにも費用対効果の小さなものに見えるのも当然です。だから、結局文字列の編集といったごく基本的な処理は除いて、業務ロジックにかかわるような処理は画面ごと、機能ごとに独立してコピーを作成するという考え方も冷静になって考えればまったく理解できないものではありません。

オブジェクト指向アーキテクチャではパッケージの安定性を考えることが大切になる

では、次に、Javaのようなオブジェクト指向言語アーキテクチャではどうして再利用が可能なのか、そして、それがどうして望ましいものにできるのか、その理由を考えていくことにしましょう。

カプセル化と依存関係

まず、重要なこととして、カプセル化という考え方の存在するオブジェクト指向の世界では、手続き型言語におけるグローバル変数というものが、少なくとも見かけ上は存在しないということがあります。*2だから、プログラムの状態というものは各オブジェクトの中身(インスタンス変数)、あるいは各メソッド内(ローカル変数)に限定されます。この事実だけでも、共通処理をくくりだした場合の結合度というのは低くなります。
つまり、手続き指向のプログラムではくくりだした各関数がグローバル変数を通して暗黙に結合していたのに、オブジェクト指向ではお互いの依存関係がより明確に可視化しやすいということが言えます。多くのケースでは、あるクラスが別のクラスをimportして呼び出していたら依存関係があり、そうでなければ独立していると考えることができるのです。

安定依存の原則

クラスやパッケージを再利用するということは、必然的に再利用する側とされる側の間に依存関係が生じるということになります。したがって、staticおじさんが心配するように、変更の影響による再利用のデメリットを少なくするには

  • 頻繁に変更される不安定なモジュールはなるべく依存されることを避ける(再利用される側でなく再利用する側に回る)
  • 逆に、変更が少ない安定したモジュールを再利用する

という方向になるように、全体的なアーキテクチャを工夫すればかなり前進できることになります。つまり、安定する方向に依存せよということですね。依存関係とパッケージ(モジュール)の安定性との関連に関するこの規則は安定依存の原則(SDP、Stable Dependencies Principle)と呼ばれています。
実際に、モジュールの安定度は以下のように定量的なメトリックとして定義することも可能です。

  • C_a(求心結合度):あるパッケージの中のクラスに対して、外部の別のパッケージ中から依存しているクラスの個数。
  • C_e(遠心結合度):あるパッケージの中のクラスが依存している外部のパッケージのクラスの個数。
  • I=\frac{C_e}{C_a + C_e}(不安定度、instability):パッケージの不安定性。0から1の範囲の数値で1に近いほど不安定。

この場合、不安定度Iがパッケージの安定性の目安となる指標です。結局、外部のパッケージから依存されているだけで、逆に自分は外部に依存していないというパッケージは(一般的にフレームワークやutilなど)I=0という安定なパッケージとなり、逆に他からまったく依存されていないパッケージはI=1という不安定なパッケージということができます。*3
以下の図はUMLの書き方にしたがって、破線の矢印の元が矢印の先のパッケージに依存していることを示しています。(ここでは、依存される安定側を下に描いています。)まず、安定したパッケージでは(I=0)、以下のように他から依存されることはあっても、逆に他に依存することがありません。

逆に、不安定なパッケージ(I=1)では、以下のように他から依存される(共有される)ことがないということになります。この場合、不安定なパッケージ内のクラスに手が加わっても、外部に影響を与えることがないことが依存関係から理解できます。

したがって、まず再利用性を高めるためには、全体のアーキテクチャ設計の観点からパッケージ分けを適切に行って、安定した再利用が可能なパッケージとそうでないパッケージの色分けを明確にできるようにするということが大切です。Javaのような言語においてパッケージ分けとは単に巨大なプログラムを小さく分類する入れ物の分割ということだけでなくて、このような安定性の分類という重要な観点があるということですね。
ちなみに、言葉の印象から誤解しそうですが、必ずしも不安定なパッケージが悪で安定したパッケージが善というわけではありませんので注意が必要です。以上の定義による不安定なパッケージとは他から使われていないということですから、自由に変更ができるということでもあります。画面など変更が頻繁に発生するホットスポットを不安定なパッケージに分離しておくことで、修正の影響を最小限にすることができます。

安定性とアーキテクチャパターン

このようにパッケージ間の依存性と安定性の関連を念頭に入れて考えると、一般的なアプリケーションにおけるMVCやレイヤーなどのアーキテクチャパターンは実にうまく考えられているということがわかります。
まず、MVCパターンでは

  • 画面の表示やユーザーの操作を受け付けるビュー
  • ユーザーの入力をもとにモデルを操作するコントローラー
  • 本質的なロジックやデータをカプセル化するモデル

に分割して考えますが、このパターンに従った設計では、モデルの部分は他の要素には依存しません。

これは、一般的にはユーザーインターフェースの変更頻度の方が本質的な部分よりも多いという傾向を考えれば納得のいく設計です。
また、一般的な業務システムでは

  • プレゼンテーション層
  • アプリケーション層
  • ドメイン
  • インフラ層

などのレイヤーに分割して設計します。(DDDの読書記録(第4章、ドメインを隔離する) - 達人プログラマーを目指して)レイヤーパターンでは上位レイヤーから下位レイヤーの方向で依存性を持たせるということになるため、上位層に行くにしたがって不安定であり再利用性に乏しいと考えているということになります。
このように、オブジェクト指向的なアーキテクチャ*4を適切に設計することによって、再利用が可能な安定した部分と、逆に、修正を頻繁に行える不安定な部分を切り分けることができます。これによって、修正の影響範囲が共通化により大きくなるという問題を軽減しながら、再利用のメリットを享受するということが可能になるのです。

安定性とテスト容易性

安定性については、テスト容易性の観点からも嬉しいことがあります。それは、より安定度が高く、他に対する依存が少ないクラス程、一般的に単体試験の作成が容易であるということです。特に、他にまったく依存していないのであれば、そのパッケージ単体に閉じて試験を作成することができるからです。したがって、多くのクラスから再利用されるフレームワークなどでは単体試験を強化することで品質を高めることが可能です。そして、もし変更の必要性が出てきた場合でも、テストの自動化により変更によるデグレードの危険を少なく抑えることができます。

オブジェクト指向における再利用性をけた違いにアップさせるポリモーフィズム

このように、変更頻度などの安定性を考えて正しいパッケージにクラスを格納するようなアーキテクチャにするだけでも、従来のstaticおじさん的な手続き指向の世界とはまったく違う次元での再利用が達成できます。

ポリモーフィズムについて再び復習

しかし、オブジェクト指向設計の再利用における本当の切り札は、インターフェースを中心としたポリモーフィズムの活用というところにあります。
いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について
で継承とポリモーフィズムについて紹介しましたが、結局、この記事で言いたかったもっとも大切なことは、あるクラスがインターフェースをimplementsしていたり、(抽象)クラスを継承して、メソッドをオーバーライドしている場合、インターフェースや親クラス型の変数にサブクラスのインスタンスを代入して利用できるということでした。つまり、メソッドを呼び出す側は、抽象的なインターフェースや親クラスのみに依存するという形になっているにも関わらず、ポリモーフィズムにより実際にはオーバーライドしているサブクラスのメソッドが呼び出されるということです。

依存関係逆転の法則

このポリモーフィズムが再利用性を促進する上でどうして重要なのかというと、インターフェースを固定することができれば呼び出し側のロジックをインターフェースとともに安定性の高いパッケージに格納して再利用する対象にできるという事実があるからです。再利用が可能な安定性の高いパッケージは文字通り変更頻度が少なくなくてはなりません。そうするとポリモーフィズムが存在しない世界では、結局、数学ライブラリーや文字列計算、カレンダー計算のように本当にロジックが一つに固定できるようなものしか再利用できないということになってしまいます。つまり、設計上まったく柔軟性や拡張性がないものしか安全に再利用の対象にできないということですね。だから、不変の原理として数学ライブラリを共有できても、どんどん仕様の変化する業務処理を共有するということは極めて難しかったのです。
しかし、インターフェースを使ってポリモーフィックにさまざまな処理が呼び出すことが可能なら、別のパッケージ内でそのインターフェースを実装したさまざまなクラスを作成することで、柔軟に機能を拡張することができます。これは、StrutsやSpringなどのフレームワークで必ずと言っていいほど利用されている発想であり、依存関係逆転の原則(Dependency Inversion Principle、DIPと呼ばれています。
この原則を使ってフレームワークをうまく設計することで、フレームワーク自身を変更の影響を受けにくい安定したパッケージにおいて再利用しながらも、インターフェースを実装する個別のクラスを後付けすることで柔軟な拡張を行うことができるのです。なお、このことはなるべく抽象度の高いパッケージに依存せよという考え方にもつながってきます。(安定度・抽象度等価の原則、SAP)

もちろん、staticおじさんが特に意識していなくてもWebブラウザや.NETなどのフレームワークを使って開発する以上、水面下でDIPによるロジックの再利用は活用されています。実際、私がこうして文章を打ち込んでいる環境でも水面下では表示やプロセス管理、ネットワーク通信などOSの基本的な処理の中でDIP的な発想が活用されています。これと同じ発想を少しでも業務ドメインやアプリケーションの領域に取り込むことで、アプリケーション開発の再利用性を向上させることができたら素晴らしいことではないでしょうか

ポリモーフィズムレガシーシステムとの連携にも有効活用できる

以上紹介したポリモーフィズムは、まったく新規にアプリケーションを開発する際のみに活躍するわけではありません。たとえば、ほとんどソースを読みたくなくなるようなスパゲッティーコードでできたプログラムに対して、安定したインターフェースからなるパッケージを定義し、システムのその他の部分はこのインターフェースを経由してレガシーシステムにアクセスするといったような設計が可能です。そのようにレガシーシステム(あるいはモジュール)と新システムとの間にレイヤーを設けることで、新システムがレガシーシステムの悪い影響を受けることを防止することができます。(腐敗防止層)このようにしておき、レガシーシステムをあるべき設計に徐々に置き換えていくなど、全体的なアーキテクチャを段階的に改善するようなことが可能になります。

この考え方は、特定の製品への依存やデータベースなどオブジェクト指向でないレイヤーとのインターフェースにも活用できます。

まとめ

ここでは、従来型の手続き指向のプログラム設計に対する再利用の限界と、オブジェクト指向的なアーキテクチャではその限界をどのように克服できるのかという点について説明しました。そして、再利用性の高い設計を実現するうえでは、パッケージ間の依存関係や安定性といったことが大切であることを説明しました。特に、オブジェクト指向設計における

  • パッケージの依存関係と安定性の関係に着目した安定依存の原則(SDP)
  • ポリモーフィズムに着目した依存関係逆転の原則(DIP
  • なるべく抽象に依存すべきという安定度・抽象度等価の原則(SAP)

という考え方について紹介しました。もちろん、これらの法則以外にも設計上考慮すべきことはたくさんありますし、また、あるべき正しいアーキテクチャを構築することは簡単なことではないということは確かです。しかし、努力して正しいアーキテクチャ設計を採用するメリットは長期的には保守性や拡張性の向上において無視できないレベルのメリットを生み出すことができます。それゆえ、長期にわたって保守拡張していくようなエンタープライズの基幹システムにおいてこそ、正しいアーキテクチャ設計を頭を使って実施するということが大切になってくるものと私は信じます。
なお、ここで紹介した原則については、以下の書籍を参考にしました。

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技

また、以下のサイトでも各種原則について説明されています。
http://d.hatena.ne.jp/asakichy/
なお、以下のtogetterのまとめでも、今回のテーマについて議論しています。
staticおじさんに再利用の有効性をわかってもらうには? - Togetter
ちなみに、staticおじさんに支配された世界での開発がどのようなものなのかについては以下のまとめが参考になります。
派遣PG時代の思い出 - Togetter
これは誇張ではなく、現場によっては今でも普通にみられる光景です。2011年現在、このような開発が行われているのは世界でも類を見ないのではないでしょうか。

*1:CORBAなどの技術を使えばCOBOLのプログラムをサービスとしてインターフェースと実装を分離することは可能です。

*2:もちろん、staticおじさんがやりそうなように、public static変数でグローバル変数に相当する機能をエミュレートすることは可能ですが。

*3:これらの指標を計測するにはJDependというツールがお勧めです。

*4:本来はオブジェクト指向に限らず、ソフトウェアのアーキテクチャを考えることは可能なはずですが。

汎用のフレームワークがあれば業務アプリ実装にオブジェクト指向は不要という考え方は適切でないと思う

前回のエントリいまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味についてのブクマのコメントで、

すごく今さら感がw 最近の開発はフレームワーク使うことが多いようだから知らなくても作れちゃうと思ってたけど違うのかなあ。

という感想をいただきました。実際に、SI業界で多くの方々、特に、アプリケーション開発の下流工程を担当しない層の方でこのように考えている方はほんとうに多いのではないかと思います。確かに最近ではSalesforceなどの製品もありますし、CRUD処理を行うような見栄えの良い業務アプリケーションは非常に簡単に開発できるようになっているということはあります。また、Visual BasicやMS Accessなど気軽にアプリケーションを開発できるツール類は昔からありました。そして、業界構造などの理由からやむを得ない側面があるとはいえ、SIerの提供する多くのフレームワークでは、アプリケーション開発を行うPGができるかぎり頭を使わず単純作業でアプリケーションを開発できるようなツールやフレームワークを必要以上に尊重する傾向があります。Java EEや.NETはCOBOLやVB6よりも本当に生産性が高いか? - 達人プログラマーを目指して
そして、大規模案件ではSOAやデータ統合などの名目で上流の工程に莫大な予算をつぎ込んで長期間「分析」「設計」をし、Excel方眼紙などの成果物を山のように作成する一方で、肝心のプログラムは自動生成だったり、コピペだらけだったり、まったく目も当てられないようなひどいコードを大量に作成するようなことが今でも日常行われているようです。
業務アプリケーション開発といっても、マスタデータの管理など単純な案件もあり、そういった業務では確かにツールで自動生成してしまえば、あえてオブジェクト指向設計する必要のない場合もあるでしょう。しかし、一般的に何百億円をかけて開発するような、大規模な基幹業務システムの業務ロジックは、想像を絶するほどきわめて複雑なものとなっているという事実を忘れてはならないと思います。実際、金融のシステムで一つの注文取引を投入する際には、残高の確認やインサイダー取引のチェックなど様々なチェックロジックが何万ステップにもわたってスパゲッティーのように入り組んで呼び出されているということがありますが、話はそれだけでは全然終わらないのです。金融の注文といっても、株も債券もあり、それも外国株とか国債とか多岐にわたった種類があります。一つの商品の注文を扱うだけでも相当複雑なチェックが必要なのに、それが各商品ごとにまったく同じではないけれども少しずつ違う処理が存在しているということです。
金融にかかわらず製造や流通などさまざまな業務で、このような複雑な処理が必要なのですが、こうした複雑なドメインを上手に扱うときにこそ本来はオブジェクト指向の設計が威力を発揮するところだと思います。残念ながら私が見たほとんどの大規模基幹システムでは、Java言語を使っていても、オブジェクト指向ということは全くと言っていいほど考慮されておらず、商品ごとにほとんど同じようなロジックが何十か所にもわたってコピペされているという状況になっていました。
確かに、

  • 画面部品
  • データベースアクセス
  • ワークフロー
  • 通信

など、汎用的なところではオブジェクト指向フレームワークが使われているのですが、肝心の業務ロジックの部分でオブジェクト指向が活用されているというケースが少なすぎるのではないかと思うのです。

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

の503ページに以下の記述があります。

素人にもわかるフレームワークを作成してはならない。
設計ができるほどには賢くない人も開発者の中にいると想定したチーム分割は、失敗する可能性が高い。これはアプリケーション開発の難しさを過小評価しているからである。設計ができるほどには賢くないなら、ソフトウェア開発を担当させてはならない。逆に十分に賢いのであれば、大事に面倒を見ようとすると、かえって担当者が必要とするツールとの間に障壁を作り出すだけである。

これは、単価の低いPGを大量に雇って品質の低いプログラムを労働集約的に量産させると考えている従来のSIerの考え方とまったく逆の主張です。もちろん、オブジェクト指向設計のできる上級プログラマーの人数には限りがあるというところも考慮すべきですが、従来何万行のロジック+コピペで作成していたような複雑なドメインロジックの開発を大量の人月をかけて労働集約で開発するのではなく、少ない人数のコアのプログラマーが中心となって慎重に設計しながら開発するというモデルにシフトすれば、全体として開発費用を大幅に削減できるでしょう。そして、できたプログラムの機能拡張やバグ修正も容易となり、長期にわたったメンテナンスコストは大きく削減できると思うのです。
もちろん、3か月だけ稼動して後は捨てるといったシステムを短期間に作成するといった場合は、とにかく大人数で作るというモデルが適合しているケースもあるのですが、少なくとも何十年にもわたってメンテナンスするような基幹業務のシステムでは、もっと品質の高いプログラムを作成して維持するという方式にいいかげん切り替えていくべきなのではないでしょうか。
なお、同じような考え方はプログラム内部の設計より大きなシステムの設計や調達といった大きな粒度にも当てはまると思います。実際、DDDの4部では、本来のEA的、SOA的な考え方にもつながるような考え方が書かれています。こだわりのある職人プログラマーほど、無駄なコードを少なくしたいものという事実を理解してほしい - 達人プログラマーを目指してで書いたこととも関連しますが、既存のレガシーシステムを活用しながらも、戦略的・段階的に企業のシステムのアーキテクチャを発展させていくような考え方が説明されています。これなどは粒度は異なるところがあるとはいえ、プログラムの段階的なリファクタリングという考え方に通じるところがあります。EA、SOAクラウドなどキーワード上はこうした考え方を取り入れている(つもりになっている)大企業はたくさんあると思いますが、実際にはベンダの言われるままにハードやミドルにお金をかけているだけで考え方が全然理解されていないのではないかというところがあります。これは、Javaを使っているけれどCOBOLと同じような設計になっているというところと似ています。
今後、あるべき姿に発展させていくためには、業界構造の変革を通して、SIer自身が変わっていかなくてはならないだけでなく、システムを調達するユーザー企業の側もプログラムやシステムの設計の品質に対する考え方*1を変革していくことが大切であると思います。

*1:むしろ、ささいなバグを一つでも出してはいけないなど、アーキテクチャによらない細かいところの品質には必要以上にこだわっているところがあります。

いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について

正しく意味を理解している方にとっては、まったく常識レベルの話であり、何をいまさらと思われる方々も多いかと思いますが、大規模案件のレガシーコードなど、私が仕事で見かけるJavaのコードを読むと、「このコードを書いたSEやPGの方々は、はたして継承の意味を正しく理解していないのではないか」と思われる設計のコードに出会うことが少なからずあります。現在では改良されましたが(Javaプログラミング能力認定試験の問題がかなり改善されていました - 達人プログラマーを目指して)、以前のJavaプログラム認定試験の問題は、そうした不適切な設計がされている典型的な例となっていたのですが、実際、SI業界ではあのような品質のコードのシステムが今でも現役で多数稼動しているというだけでなく、現在でも新たに生み出されているというのは残念ながら紛れもない事実のようなのです。
確かに新人研修で「哺乳類を継承して犬クラスと猫クラスができる」といったようなオブジェクト指向の説明を聞いただけで、簡単に理解できるものではありませんが、以降手続き型のレガシーコードしか相手にしていないPGやコードを目にすることもなくExcel方眼紙ばかり描いて(書いて)いるSEの方々は、継承の意味などをきちんと理解する前に忘れてしまっているという方も多いかもしれません。私の経験上、この業界ではこうした基本的な知識が理解されていないという現場が、むしろ、典型的なケースなのではないかという疑いすらあると思えてくるのです。(あの認定試験問題の品質をSI業界の代表的なプログラム品質と考えることの是非 - 達人プログラマーを目指して
オブジェクト指向プログラミングの方法を理解して使いこなせるようになるには、通常はそれなりの努力と期間を要するものですし、きちんと指導してもらえる先輩に巡り合うということも大切ですが、ここでは、「継承」の意味に絞って、最低限知っておいてほしいポイントについて、今更ですがまとめてみたいと思います。あらかじめお断りしておくと、これを読んですぐに理解できるという保証もできないのですが、あと一歩のところで正しい理解に到達できない人にとってヒントになるところもあるかもしれません。そして、もちろん、継承はオブジェクト指向プログラミングのほんの一部分の要素でしかありませんが、その意味を理解することで、次のステップに進みやすくなるとも考えられるのです。(ここではJava言語を例として説明していますが、C#Visual Basicなど、業務で利用する主流のオブジェクト指向プログラミング言語でも基本的なポイントはほぼ同じです。)

クラスの継承の文字通りの意味

Java言語ではクラスを継承するときにはextendsというキーワードを使ってクラスを宣言します。これは、文字通り「拡張する」という意味ですが、親クラスの定義を拡張して新しいクラスを定義するという働きがあります。以下のコード例をみてください。*1

class Parent {
    public String fieldA = "field A";
	
    public String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    public String fieldB = "field B";

    public String methodB() {
	return "method B";
    }
}

この場合、以下のようにChildクラスのインスタンスを生成して、フィールドやメソッドにアクセスすると以下のようになります。

public class Inheritance {

    public static void main(String[] args) {
        Child child = new Child();
        System.out.println(child.fieldA); // field A
        System.out.println(child.fieldB); // field B
        System.out.println(child.methodA()); // method A
        System.out.println(child.methodB()); // method B
    }
}

もともとの「継承する」「拡張する」という意味の通り、ChildクラスはParentクラスのフィールドやメソッドを継承元の親から引き継いで自分自身で定義しているような動作となっています。つまり、Childを以下のように定義した場合と同じ動作ということですね。

class Child {
    public String fieldA = "field A";
    public String fieldB = "field B";

    public String methodA() {
	return "method A";
    }

    public String methodB() {
	return "method B";
    }
}

継承を活用することで、このように共通のデータ(フィールド)や処理(メソッド)を親クラスにまとめて定義することができるのです。

子(サブ)クラスで同一の形のメソッドをオーバーライドする

「なんだ、かんたんじゃないか」これで、一人前に継承を理解できたと考える人もいるかもしれません。ですが、残念ながら話はここで終わらないのです。むしろ、実は、ここまでの話はJava言語の継承の働きの中でも本当に20%というか、継承の本当に威力のあるポイントを見逃していることになるのです。話のクライマックスはまだこれからなのです。
話が面白くなるのは、子クラスが親クラスと同じ形(シグネチャ)のメソッドを定義している場合です。以下の定義を見てください。

class Parent {
    public String fieldA = "field A";
	
    public String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    public String fieldB = "field B";

    @Override
    public String methodA() { // 親クラスのメソッドをオーバーライドする。
        return "method A in Child";
    }

    public String methodB() {
	return "method B";
    }
}

ここでは、Childクラスにおいて親クラスで定義されているmethodA()と同じ形のメソッドを再度定義しなおしています。この形で、親クラスのメソッドを子クラスで再定義することをオーバーライドと呼んでいます。なお、Java5以降のバージョンでは、正しくオーバーライドしていることをコンパイラにチェックさせるために@Overrideというアノテーションを明示的につけることが推奨されています。(うっかりスペルミスをしたり、シグネチャが異なっていたりするとコンパイルエラーとなります。)
次に、これを実行してみましょう。

public class Inheritance {

    public static void main(String[] args) {
        Child child = new Child();
        System.out.println(child.fieldA); // field A
        System.out.println(child.fieldB); // field B
        System.out.println(child.methodA()); // method A in Child
        System.out.println(child.methodB()); // method B
    }
}

期待通り、child.methodA()の呼び出しはParentクラス中のメソッドでなく、Chlildクラス中のメソッド呼び出しに置き換えられています。これも、オーバーライドの機構を理解してしまえば、難しいところはないと思います。通常は親クラスのメソッドが継承されてくるのに、子クラスでオーバーライドすると、継承元の親クラスの側のメソッドでなく子クラスのメソッドが呼び出されるということです。共通部分を親クラスに定義しておき、必要に応じて、差分があれば子クラスでオーバーライドできるということで、便利ですね。

継承には型の継承というもう一つの重要な側面がある

実は、多くのJavaプログラマーの理解がこの段階で止まってしまっているのではないかと思われるのですが、Java言語の継承には型の継承というもう一つの重要な側面があります。つまり、今までの説明では「継承とは親クラスのフィールドやメソッドを子クラスで再利用するための便利な方法」という意味しかなかったのですが、それに加えて、子クラスのオブジェクトは親クラスの型と代入互換性があるという性質があるのです。Javaの変数は型をつけて宣言する必要があったことを思い出してください。

int a = 3;
String b = "hello";
b = a; // コンパイルエラー

つまり、以上の例のように基本的には同じ型の変数にしか代入できないように、コンパイラがチェックしてくれます。一方、クラスに親子関係があると、親クラスの型の変数に子クラスの型のインスタンスを代入できるという重要な規則があります。

Child child = new Child();
Parent parent = child; // コンパイルOK

この規則は、冷静になって考えると自然なものであると納得ができます。今まで説明した継承のメカニズムによって、子クラスは親クラスのすべてのデータや振る舞いを保持しているのですから、場合によっては親クラスの型であると抽象化して考えても問題ないということですね。そして、以下の結果を見てください。

public class Inheritance {

    public static void main(String[] args) {
        Parent parent = new Child();
	System.out.println(parent.fieldA); // field A
	System.out.println(parent.methodA()); // method A in Child
    }
}

ここで、非常に大切なポイントはparent.methodA()の呼び出し結果がParentクラスのmethodA()でなく、ChildクラスのmethodA()にバインドされているという事実です。このポイントはレイトバインディングや仮想メソッド呼び出し*2などと説明されることがありますが、とにかく、宣言されている変数の型ではなく、実際に変数に代入されているインスタンスの型によって実行時の振る舞いが決まるということです。
つまり、継承によるメソッドのオーバーライドが、Java言語でポリモーフィズムを実現する手段となっているという事実を理解してください。これが、実はJava言語における継承の威力の中でも最も重要な働きをするポイントとなっているのです。
ポリモーフィズムについては、以前、ドラゴンボールで学ぶオブジェクト指向 改 - 達人プログラマーを目指してで、

共通のメソッド呼び出しで、対象とするオブジェクトの種類に応じてまったく異なるさまざまな処理を実行可能な性質をポリモーフィズム多態性)と呼びます。

のように説明しています。なお、この説明だけだと、まだ何が嬉しいのかピンと来ないかもしれませんが、親クラスの型の変数に代入できるという性質は変数だけでなく、メソッドのパラメーターにも同様に成り立ちます。だから、

public void someMethod(Parent parent) {
   // parentを使って何らかの処理
}

のようなメソッドを一度定義しておくと、Parentクラスの任意の子クラスをパラメーターとして渡して処理をさせることができるというわけです。つまり、このポリモーフィズムの性質を使うことで、拡張性や再利用性の高いライブラリーを作成できるのです。実際、たとえば証券のドメインであれば、ある銘柄の注文を表すOrderクラスを入力として受け取るメソッドを定義しておくと、株式や債券など様々な種類のOrderの子クラスを処理するように拡張させるといった設計が可能になるのです。

型の継承によるポリモーフィズム的な側面にのみ着目したのが抽象メソッドとインターフェース

このようにJavaの継承の構文が持つ働きには

  • 親クラスからフィールドやメソッドの定義を文字通り継承する(コードの再利用)
  • 子クラスが親クラスの型を持っているが、実行時の振る舞いは代入されているインスタンスで決まる(ポリモーフィズム

という二つの側面があるのです。特に、後者の概念はなかなか難しく、(私の説明のまずさもありますが)話を聞いただけですぐに理解できないかもしれません。しかし、コードを実際に写経*3するなどして、実際に試しながら、じっくりと理解することにしましょう。Javaプログラミングでは、この壁を越えらえるかどうかが非常に重要なポイントで、ここをいったんクリアできれば、デザインパターンフレームワークなど、オブジェクト指向プログラミングの広大な世界を冒険する準備ができたことになります。
さて、この型の継承とポリモーフィズムということに着目すると、もともとの親クラスのメソッドの実装は不要になる場合があります。その場合は、以下のように親クラスをabstractクラスにして、オーバーライドするメソッドをabstractメソッドとして宣言することができます。

abstract class Parent {
    public String fieldA = "field A";
	
    public abstract String methodA(); // 抽象メソッド
}

この場合も、前回と同様にParent型の変数のChildのインスタンスを代入して実行すると、正しくChildのメソッドが実行されるのです。

public class Inheritance {

    public static void main(String[] args) {
        Parent parent = new Child();
	System.out.println(parent.fieldA); // field A
	System.out.println(parent.methodA()); // method A in Child
    }
}

Parentクラスの抽象メソッドは型としてmethodA()が呼び出せるということをコンパイラに知らせている働きがある一方で、実際にメソッドの実装はポリモーフィズムにより、Childクラスのメソッドにバインドされています。
さらに、Javaの場合、継承による型の継承とポリモーフィズムという点を究極に推し進めたものとしてインターフェースが定義できます。上記のParentをインターフェースに置き換えると以下のようになります。

interface Parent {	
    String methodA();
}

class Child implements Parent {
    public String fieldB = "field B";
    
    @Override
    public String methodA() {
        return "method A in Child";
    }

    public String methodB() {
        return "method B";
    }
}

public class Inheritance {

    public static void main(String[] args) {
        Parent parent = new Child(); // Childのインスタンスをインターフェース型に代入
        System.out.println(parent.methodA()); // method A in Child
    }
}

(補足)Java言語ではフィールドはオーバーライドできない*4

本エントリに対して重要なコメントをいただきましたので、子クラスで親クラスと同一名のフィールドを定義した場合にどうなるかについて補足させていただきます。通常は、親クラスと同一名のフィールドを子クラスで定義すべきではなく、このセクションで書いた内容の理解は後回しでもよいと思いますが、うっかりバグの原因となることがあるので、注意が必要であるということは知っておく方がよいかもしれません。
実際、子クラスを以下のように定義して確認してみます。

class Parent {
    String fieldA = "field A";

    public String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    String fieldA = "field A in Child"; //親クラスと同一名称のフィールドを定義
    String fieldB = "field B";

    @Override
    public String methodA() {
        return "method A in Child";
    }

    public String methodB() {
        return "method B";
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Child child = new Child();
        Parent parent = child;

        // フィールドは宣言されている変数の型で決まる
        System.out.println(parent.fieldA); // field A
        System.out.println(child.fieldA); // field A in Child

        // Child型の変数で隠ぺいされているParentのフィールドを参照するにはキャストすることも可
        System.out.println(((Parent)child).fieldA); // field A

        // メソッドはオーバーライドされてポリモーフィックに呼び出される
        System.out.println(parent.methodA()); // method A in Child
        System.out.println(child.methodA()); // method A in Child
    }
}

結果は以上のようになりました。メソッドの場合はすでに説明したように子クラスの実装で親クラスの実装がオーバーライドされており、変数の型によらずに、実際に生成されているオブジェクトの型に応じて子クラスのメソッドが呼び出されるのでした。しかし、同一名のフィールドを定義した場合は変数の型によって、親クラスか子クラスのどちらかのフィールドが参照されています。これはどういうことかというと、フィールドは同一名称であっても決してオーバーライドできないということを意味しています。つまり、たまたま名前が同じ変数が親と子に別々に含まれているだけであって(異なる名前のフィールドの継承とおなじく)子クラスのインスタンスにはどちらの変数も含まれているということになります。ただし、同一名のフィールドをこのように宣言してしまうと変数の型によってどちらかのフィールドが参照できなくなってしまうのです。これは、一般にグローバル変数とローカル変数で同じ名前のものがあると、ローカル変数しか見えなくなってしまうといったことと似ています。それぞれの変数の領域は存在しているのですが、単に名前が隠ぺいされて参照できなくなっているということです。

(補足)staticメソッドもオーバーライドできず、ポリモーフィズムが存在しない

つぎに、staticメソッドの継承について調べてみます。以下の例は、以前の例に対して、staticキーワードを各メソッドに追加しています。

class Parent {
    String fieldA = "field A";

    public static String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    String fieldA = "field A in Child";
    String fieldB = "field B";

    public static String methodA() { // 親クラスと同一形のstaticメソッド。普通はやらない。
        return "method A in Child";
    }

    public static String methodB() {
        return "method B";
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Child child = new Child();
        Parent parent = child;

        // staticメソッドはクラスごとに存在しており、変数の型で呼び出し対象が静的に決まる。
        // つまり、ポリモーフィズムが存在しない。
        System.out.println(parent.methodA()); // method A
        System.out.println(child.methodA()); // method A in Child

        System.out.println(((Parent)child).methodA()); // method A

        // 紛らわしいので、普通はインスタンスでなくて、クラスに対して呼び出す。
        System.out.println(Parent.methodA()); // method A
        System.out.println(Child.methodA()); // method A in Child
    }
}

staticでない普通のメソッドと違い、staticメソッドはクラスごとに別々の実装が独立して存在しており、子クラスで同一名のメソッドを定義してもオーバーライドできません。見かけ上インスタンス経由で呼び出した場合、インスタンスの型でなく宣言されている変数の型によって呼び出し先が決まります。前の節で説明したフィールドの場合と似たように解決されています。
このようにstaticメソッドにはポリモーフィズムが存在せず、コンパイル時に呼び出し先が一つに固定されます。staticメソッドの呼び出しにより、コンパイル時に実装が一つに固定されてしまうということです。(一方、普通のメソッドは、ポリモーフィズムにより、オーバーライドしている子クラスの数だけ無数に実装が存在している可能性があります。)staticメソッドが拡張性が低く、また、クラスを分離した単体試験が難しくなるということと関連しています。

(補足)コンストラクタ、初期化ブロック、static初期化ブロックについて

コメント欄にて川久保さんにご指摘いただきましたので、コンストラクタや初期化ブロックに関して簡単に補足させていただきます。初期化ブロックはともかく、コンストラクタに関する理解は重要だと思います。

コンストラクタは継承されない(しかし、子クラスのコンストラクタから呼び出される)
class Parent {
    public Parent() {
        System.out.println("Parent no args constructor");
    }
    
    public Parent(String arg) {
        System.out.println("Parent one arg constructor arg = " + arg);
    }
}

class Child extends Parent {
    public Child() {
//        super(); コメントをはずしても同じ
        System.out.println("Child no args constructor");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Parent parent = new Parent(); //Parent no args constructor
        Parent parent2 = new Parent("test"); //Parent one arg constractor arg = test
        
        Child child = new Child(); // Parent no args constructor, Child no args constructor

  // コメントをはずすとコンパイルが通らない。 
  // コンストラクタはメソッドのようには継承されない
  //      Child child2 = new Child("test");
    }
}

ご指摘のとおり、Java言語の場合にはコンストラクタは通常のメソッドのように自動的に子クラスに継承されません。したがって、以上の例では文字列のパラメーターをとるコンストラクタは親クラスには存在しますが、子クラスには存在しません。*5ただし、子クラスのコンストラクタ中から親クラスのコンストラクタが呼び出されます。(呼び出しが省略された場合は親クラスのパラメーターのないコンストラクタの呼び出しが自動的に行われる仕様。)

インスタンス初期化ブロックはnew実行時に自動的に実行され、オーバーライドできない

存在自体知らない人も多いかもしれませんが、コンストラクタの代わりに初期化処理を行うための、インスタンス初期化ブロックという仕掛けがJDK1.1のころからあります。以下の例を実行してみると、各クラスのコンストラクタの実行前に呼び出されることがわかります。親クラスの初期化ブロックは自動的に呼び出されるため、子クラスでオーバーライドできません。

class Parent {

    // インスタンス初期化ブロック
    {
        System.out.println("Parent instance initilizer block");
    }

    public Parent() {
        System.out.println("Parent no args constructor");
    }
}

class Child extends Parent {
    // インスタンス初期化ブロック
    {
        System.out.println("Child instance initilizer block");
    }

    public Child() {
        System.out.println("Child no args constructor");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        // Parent instance initilizer block
        // Parent no args constructor
        Parent parent = new Parent();

        System.out.println();

        // Parent instance initilizer block
        // Parent no args constructor
        // Child instance initilizer block
        // Child no args constructor
        Child child = new Child();
    }
}

この動作は、各フィールドの初期化子に似ています。

static初期化ブロックはクラスロード時に自動的に一度だけ実行され、オーバーライドできない

同様に、static初期化ブロックの例も示します。こちらはインスタンスの生成時ではなく、クラスのロード時に一回だけ実行される点が違います。いずれにしても、サブクラスでオーバーライドするということはできません。

class Parent {
    // static初期化ブロック
    static {
        System.out.println("Parent static initilizer block");
    }

    public Parent() {
        System.out.println("Parent no args constructor");
    }
}

class Child extends Parent {
    // static初期化ブロック
    static {
        System.out.println("Child static initilizer block");
    }

    public Child() {
        System.out.println("Child no args constructor");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        // Parent static initilizer block
        // Parent no args constructor
        Parent parent = new Parent();

        // Parent no args constructor
        Parent parent2 = new Parent();

        System.out.println();

        // Child static initilizer block
        // Parent no args constructor
        // Child no args constructor
        Child child = new Child();

        // Parent no args constructor
        // Child no args constructor
        Child child2 = new Child();
    }
}

まとめ

通常、会社の研修や入門書ではオブジェクト指向の考え方と一緒に継承の説明を一気にされることが多いと思います。そのため、なかなか本質を理解しにくかったり、重要なポイントを見逃してしまうということもあると思います。ここでは、カプセル化や関連などオブジェクト指向に関する他の説明はいったん無視して、Java言語における継承の意味に絞って説明を試みてみました。

  • 継承は親クラスの定義を子クラスで再利用するための手段
  • 親クラス(インターフェース)の型に子クラスを代入することでポリモーフィズムを利用して拡張性の高いライブラリーを作成する手段

という二つの側面があるという点を理解することが大切です。特に、後者はなかなか分かりにくいところがあると思いますので、すぐに理解できなくても悲観することはありません。私も最初そうでしたが、ふとある時突然「そういうことだったのか」という瞬間が来るものです。
SI業界では、プログラミングの研修に十分な時間がとられないことも多く、こうしたごく基本的なことを理解しないまま、設計を行ったり、コードを書いたりするということも多いかもしれません。しかし、より高い生産性のプログラマーを目指すために、こうした基本的な知識を正しく理解しておくことは、プロフェッショナルのプログラマーとして最低限の義務なのではないでしょうか。
なお、今回は最短の説明でJava言語の継承の働きを説明するため、オブジェクト指向の説明はあえて省略してしまいましたが、オブジェクト指向については、以下もご参照ください。
ドラゴンボールで学ぶオブジェクト指向 改 - 達人プログラマーを目指して
(追記)
本エントリでは、オブジェクト指向の概念モデルやクラスライブラリーとして意味のないParentやChildといったクラスを説明に使いました。もちろん、入門書などでは普通は図形や実世界の物などを使って継承関係を説明することが多いと思います。今回の説明のアプローチに対して、非難もあるかと思いますが、オブジェクト指向の意味やモデリングということを同時に理解しようとすると、考えることが多すぎて、逆に理解を妨げるということもあると思うのです。また、クラス設計というのは実際にモデル化すべき対象となる問題やプログラム(すなわちコンテキスト)を考えないと、あまり意味がないということもあります。
したがって、まず、初心者の方はオブジェクト指向の考え方はいったん後回しにして、プログラミング言語の機能としての継承の使い方を形から理解するというところから入るというアプローチもあると思います。その後で、JDKOSSのクラスライブラリーやフレームワークの使い方を理解しながら、デザインパターンリファクタリングなどを学習し、その過程で自然にオブジェクト指向的なモデリングの考え方ができるようになれば、DDDなどの本を勉強して実際の問題を適切にクラスで設計できるようになるという流れもあると思います。
多くのオブジェクト指向の説明ではそういった段階を飛び越していきなり概念モデルに入ろうとするため、なかなか理解しにくいところがあるのではと思うのです。(自分のドラゴンボールの説明はそういうアプローチですが。)特に、インターフェースとポリモーフィズムの威力を理解するには、まずデザインパターンの中で、StrategyパターンかCommandパターンあたりから勉強することをお勧めします。以下の本はデザインパターンについて、初心者にもわかりやすく書かれていると思います。

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

(追記)
ブクマで

Parent parent = new Child(); の変数名はchildを使いたいなあ。

というコメントをいただいています。確かに、ポリモーフィズムによって呼び出し時のメソッドはオーバーライドされていればChildクラスとして振る舞うので気持ちがわからなくないのですが、あくまでも呼び出し元はParentという型として認識しているという点もポイントかと思います。ポリモーフィズムを説明する時に私がよく使うたとえとして「筆記用具で文字を書く」という文があります。筆記用具は抽象親クラスであり、文字を書く人はあくまでも筆記用具を使っていると考えているのですが、実際に使っているのは筆記用具の子クラスのオブジェクトである鉛筆だったりボールペンだったりするということがあります。文字の太さや色は実際に使っているオブジェクトによって決まるのですが、そのオブジェクトを使う人は特に両者の違いを意識しなくても文字を書けます。変数名にparentを使っているのはそういう気持ちからすれば、一般的に自然だと思います。たとえ話も危険な場合がありますが、ポリモーフィズムは本来は日常我々が行っている抽象化をプログラミングで実現する手段としてはすごく自然な考え方だと思います。

*1:ここではアクセス修飾子の問題はあえて考えなくて済むようにすべてpublicとしています。もちろん、通常、フィールドはprivateなどにしてカプセル化すべきです。

*2:コンパイル時に呼び出し先のメソッドが解決できないため、実行時に判断する必要があるということ。

*3:単に話を聞いたり本を読んだりするだけでなく、エディタやIDEを使って実際に例題を書き写し、動作を確認しながら理解すること。

*4:Java言語はC++など多くのオブジェクト指向言語と同様に、フィールドとメソッドを明確に区別しています。つまり、統一アクセスの原理が言語上はサポートされていません。よって、フィールドをpublicにせず、getterとsetterを定義するということが通常は行われます。C#VBではプロパティというしくみがあり、また、Scalaではそもそもフィールドとメソッドの区別はよりあいまいです。

*5:この辺りはオブジェクト指向言語によって違いもあるところがあるので、注意が必要です。たとえば、Delphiなどではコンストラクタは継承されます。

Javaプログラミング能力認定試験の問題がかなり改善されていました

以前、本ブログでJavaプログラミング能力認定試験の1級のサンプルプログラムがあまりにも旧態依然とした設計でひどいという指摘をさせていただきました。
SI業界(日本)のJavaプログラマーにはオブジェクト指向より忍耐力が求められている? - 達人プログラマーを目指して
実際、例題のプログラムがあまりにも理解しにくかったので、そこでは、

日本のSI業界でJava PGとして仕事をするためには、オブジェクト指向的にきれいなあるべき姿でコーディングできるスキルではなく、このようにオブジェクト指向をまったく理解していない上流のSEが作成した異常な設計書に忠実にしたがってコードを書き、また、その複雑なスパゲッティコードを長期にわたってメンテナンスする根性と忍耐が最重要のスキルとして試験で試されているということなのかと私は理解しました。

と書いたのですが、冗談は抜きにして、実際、この例題のようなひどいプログラムが大量に作成されて長いこと保守されているということは、少なくとも私の経験上本当にまぎれもない事実なのです。必ずしも常に最新の技術を知っていることが最重要ではないとは思いますが、最低限オブジェクト指向プログラミングのような基本的な技術は、SI開発の現場で活用し、品質や生産性の向上に役立てていかなくてはならないと思うのです。
プログラマーの成長を考えないSIerの仮説は間違っている - 達人プログラマーを目指して
それで、先日この試験の話題が再びTwitterで少し話題となったため、今どうなっているのかなと確認してみたのですが、素晴らしいことに、いつのまにか全面的にリファクタリングされて書き直されていました。
サンプル問題 - 試験を知る - 個人の方 - Java™プログラミング能力認定試験│資格検定のサーティファイ│あなたのスキルアップを応援します|

  • インターフェースの使い方
  • UIとロジックの分離(ConsoleStatusという表示状態を抽象化した型を継承して、各処理を実装しているところはよくない。表示とロジックの関心事が混在して、変更の発散コードスメルがあります。)
  • 総称型の使い方(RecordListは総称化した方がよい)
  • コピペの存在(DisplayPersonsByNameStatus と DisplayPersonsByTypeStatusなど。ちなみにPersonsの英語は誤りで正しくはPeopleとすべき?)

などにおいて、今後まだまだ、改善の余地はありますが、何よりもまず、わかりやすい英語のクラス名が与えられていますし、可読性の上でも以前とは比較にならないほど向上して、パッと見にはずっとJavaらしい設計になっていました。
以前のバージョンは、

という状態で、何人もの勇者がこの怪物に対して果敢にリファクタリングに挑戦しても、まったく手におえる状態ではなく、あっけなくやられてしまうという状態だったのですが、ここからなら、TDDでリファクタリングすることも容易であると思います。基本的には以前とほぼ同じユースケースを実装しているようですが、コード記述量は圧倒的に改善されています。以前のひどいコードをご覧になった方は比較してみると、プログラムの設計が保守性に与える影響の大きさを知ることができるのではないでしょうか。
私のブログの影響なのかわかりませんが、これほどの短期間で対応していただいた関係者の方々に感謝いたします。