Edited at

オブジェクト指向とは結局何なのか あるいはプログラミングで気をつけるべきたった一つのこと


はじめに

プログラミングを勉強していればオブジェクト指向という言葉を1度は聞いたことがあるでしょう。しかしこのオブジェクト指向、かなり古くからあり現在でも大勢の人が使用しているにも関わらず何を意味しているかイマイチわかりません。あるページを見ると「カプセル化」「継承」「ポリモーフィズム」の要素を持つものと書いてある。しかしオブジェクト指向の提唱者はメッセージングと言っているWikipediaを見ても背景が長々と続きいくつも種類があると書いてあってハッキリしない。

結局以下のような変数とメソッドが固まってるものという漠然とした理解になってる人が多いと思います(全く初耳という人は補足を先にお読み下さい)。

public class SandBox {

int hoge = 10;
int fuga = 20;

int foo(){
return hoge;
}
int bar(){
return fuga;
}
}

しかし自分が趣味でも業務でもプログラミングを続けてきてわかったのは、結局オブジェクト指向、というか「プログラミングは○○であるべき」系の言説というのはある一つの目的のために存在しており、それがわかればオブジェクト指向や「わかりやすい変数名をつけよう」などのプログラミング技法はたちどころに導かれるということです。


リーダブルコード

自分が趣味でプログラミングを始めた頃なぜオブジェクト指向で作ったりする必要があるのかわかりませんでした。また、「わかりやすい変数名をつけよう」などの注意書きも理解しつつも重要とは思いませんでした。実際のところ、これらは短くて自分だけが使うプログラムなら必要ありません。例えばあるフォルダ内のファイル名を一括変更(先頭に[既読]とつける)したい場合こんなコードで十分です。

File a = new File("C:\\Users\\admin\\Documents\\ss");

for(File f : a.listFiles()){
File b = new File(a, "[既読]"+f.getName());
f.renameTo(b);
}

わざわざオブジェクトを作る必要はありません。わかりやすい変数名を付ける必要もありません。1回使うだけで自分にさえわかればいいからそんなことをしても面倒なだけです。

ではなぜオブジェクト指向や変数名の付け方などが侃々諤々の議論になるかというと、それは長期間多人数で開発するプログラムを対象としているからです。業務で開発されるプログラムというのは、大きな会社で多人数で作られ何年間にもわたって調整を加えられます。だからこそ他の人が読んでも理解できるコードで、変更を加えても思わぬところに影響が出ないように疎結合(後述)な必要があるのです。なお、「自分は趣味で一人で作ってるだけだから関係ないな」と思ったらそれは間違いで、半年も空けば過去の自分が書いたコードの意味を忘れてしまうのでやっぱりわかりやすく疎結合に書く必要があります。

要するにオブジェクト指向や「わかりやすい変数名をつけよう」などのプログラミング技法は「他人(将来の自分含む)が読み書きしやすいコードを書く」ためにあるのです。


ではオブジェクト指向とは

自分なりに説明すると、オブジェクト指向とは「他人(将来の自分含む)が読み書きしやすいコードを書く」ためにカプセル化などのルールを守った上でデータや機能のまとまり(クラス)を作ることです。

これだけだとわからないと思うので例としてJavaのArrayListクラスを見てみましょう。

List<String> list = new ArrayList<String>();

list.add("C");
list.add("A");
System.out.println(list.get(0));//C

Javaを知らない人でも(英語がわかるなら)このコードを読めば「listに"C"と"A"を加えて0番目の要素を取得してるんだな」とわかると思います。こんな風にクラスの中身を見なくてもコードの意味していることがわかることが重要で以下で説明するものも全てこのためにあります。なぜなら大会社が作ってるプログラムは場合によっては数百万行とかになるのでいちいちすべてのコードを把握するのは不可能だから(そしてもちろん小規模なプログラムでも把握しなければいけないコードが少ないほうが望ましいから)です。


カプセル化

オブジェクト指向のもっとも重要な特長がカプセル化です。これは「ユーザーが使うのに必要な部分だけ可視化してそれ以外は隠す」ということです。プログラミング言語的に言えばpublic(公開)private(非公開)を適切に設定することです。

例えばArrayListクラスは実は内部ではensureCapacityInternal()やgrow()などのメソッドがあり複雑な処理をしてるのですが、リストを使う側には関係ないのでprivate(非公開)になっていて、add()やget()などのユーザーが使うメソッドのみpublic(公開)になってます。これによりArrayListは内部を知らなくても簡単に使える安全なクラスになっているのです。

もっとわかりやすく言えば、例えば電卓です。あなたは電卓内部の電子回路がどんなふうになっているか知っているでしょうか?

ほとんどの人はそんなもの知りません。しかし電卓は誰でも使うことができます。内部の電子回路はprivate(非公開)でボタンや表示画面はpublic(公開)だからです。もし電卓がケースに覆われておらず電子回路が露出していたらどうでしょう。ものすごく電子回路に詳しい人なら自分なりに改造して自分用の特殊な電卓にする事もできるかもしれません。しかしほとんどの人にとってそれは理解不能で、うっかり触ったりして不具合の原因になるのが関の山です。だから外部の操作を受け付ける部分だけpublic(公開)にして電子回路など内部の直接触れる必要がない、触れるべきではない部分はprivate(非公開)にしているのです。これがカプセル化です。

あえて電卓をプログラム風に書くならこんな感じになるでしょう。

public class Calculator{

/** ボタン「1」はpublic(公開) */
public void button1(){
//処理
}
/** ボタン「2」はpublic(公開) */
public void button2(){
//処理
}

...//以下続く

/** 内部で行う計算処理はprivate(非公開) */
private double calculate(){
//処理
}

/** 結果の表示はpublic(公開) */
public double showResult(){
//処理
}
}

こう考えていくと「カプセル化」は日常生活にあまねく存在していることがわかります。テレビのリモコンがどんな信号を発信しているか知らなくてもテレビは操作できる。エンジンの設計図を知らなくても車は運転できる。年末調整の詳細を知らなくても経理部に書類を提出すれば年末調整してもらえる。こんな風に「内部は知らなくても何をすればどういう結果が返ってくるかわかる」というブラックボックスにすることで複雑な社会でも生きていけるし、大規模なプログラムも簡単に理解可能な形になるのです。

冒頭でオブジェクト指向の提唱者は「メッセージング」と言っているという話をしましたが、外部の人が「メッセージ」を送って返ってくる「メッセージ」を受け取るだけで内部を知らなくてもプログラミングできることが肝なのです。


密結合と疎結合

各要素の結びつきが強いのが密結合、弱いのが疎結合です。一般に密結合のものをクラスにまとめてクラス間は疎結合なのが良いとされます。

例えば電卓クラスに日付計算のためのコードがあったらどうでしょう。「なんでもできるクラスのほうがいいじゃん!」と思う人もいるかもしれませんが日付計算は年月日を分けて計算したり曜日を出したりうるう年を考慮したりと通常の数値計算とはかなり異なります。そのため電卓クラスの中に日付計算のためのコードもあるとクラス内部を編集するとき互いに無関係なメソッドや変数が出てきて混乱してしまいます。使う側も同様です。素直に電卓クラスと日付計算クラスに分けたほうがいいでしょう。

そしてクラス間は疎結合にします。例えば電卓クラスの状態が日付計算クラスの挙動に影響を与えるようになっていたら電卓クラスを直しただけのつもりなのに日付計算クラスにまで影響を与えてしまい思わぬバグが出てしまうかもしれません。もしどうしても電卓クラスの状態によって日付計算クラスの挙動を変えたいなら電卓クラスから必要なパラメータを取得するメソッドを作り、日付計算クラスにそれを受け取るメソッドを作って連携を「見える化」しましょう。


ポリモーフィズム(多態性)

ポリモーフィズムとは外面は同じにして内部の処理は多様に変更できることです。例えばテレビのリモコンを考えてください。シャープやパナソニックなどいろんな会社がテレビを作っていてリモコンも内部の電子回路などは異なりますが、チャンネル切り替えボタンや音量の上下ボタンが有ることは共通していて、動作も同じです。このように内部の実装は異なっても「どんな機能を持つのか」という外面は共通させられるのがポリモーフィズムです。

もしリモコンをプログラム風に書くなら以下のようなインターフェース(interface)になるでしょう。

public interface RemoteController{

public void channel1();
public void channel2();
...//以下続く
public void volumeUp();
public void volumeDown();
...//以下続く
}

こうすると書く人には「内部の実装は各自変えて良いがこういう機能を持つことは約束してくれ」と伝えられますし、ユーザーはどのメーカーかを気にせずリモコンとして同じように使えます。

具体的なプログラムで言うと、例えばJavaのArrayListクラスとHashSetクラスは要素の持ち方がかなり異なりますが、どちらもCollectionインターフェースを継承してるので内部の実装と関係なくiterator()メソッドで要素を順に取得していけることがわかるのです。

ところでオブジェクト指向をやっている人は「インターフェースの継承はわかったけどクラスの継承は?」と思うかもしれませんが、クラスの継承は親クラスの詳細を知らないと思わぬ挙動をしたり、階層が深くなると変数が多階層に散らばって追うのが大変になったり、同じ名前の変数が重複して混乱するなどの問題があるのであまり使わないほうがいいです。実際Javaの生みの親もクラスの継承(extends)はできるだけ避けるべきだと言っています


実践編

では以上のことを踏まえて簡単なオブジェクト指向プログラミングをしてみましょう。言語はJavaですがJavaを知らない人にもわかるように書きます。作るのは「期間」を計算するプログラムです。例えば「2ヶ月18日」働いた後「1ヶ月4日」働いたら合計「3ヶ月22日」働いたと出力するプログラムです。

ただし、30日を1ヶ月と換算します。つまり、「1ヶ月24日」働いて「3ヶ月15日」働いたら、合計は「4ヶ月39日」ですが、30日は1ヶ月と換算するので日数から30を引いて月に1を足し、「5ヶ月9日」と出力します。

オブジェクト指向なしにプログラムを書くと以下のようになります。

int monthNum = 0;

int dayNum = 0;

//1ヶ月24日働く
monthNum = monthNum + 1;
dayNum = dayNum + 24;
////30日を1ヶ月換算
monthNum = monthNum + dayNum/30;
dayNum = dayNum%30;

//3ヶ月15日働く
monthNum = monthNum + 3;
dayNum = dayNum + 15;
////30日を1ヶ月換算
monthNum = monthNum + dayNum/30;
dayNum = dayNum%30;

System.out.println((monthNum/12)+"年"+(monthNum%12)+"ヶ月"+dayNum+"日");//0年5ヶ月9日

これでも計算はできますが、コードがごちゃごちゃしてますしコピペミスでバグが発生するかもしれません。そこでDuration(「期間」という意味)クラスを使って以下のようなコードにしてみましょう。

Duration durationSum = new Duration(0, 0);

//1ヶ月24日働く
Duration duration1 = new Duration(1, 24);
durationSum.add(duration1);

//3ヶ月15日働く
Duration duration2 = new Duration(3, 15);
durationSum.add(duration2);

//自動的にtoString()が呼ばれる
System.out.println(durationSum);//0年5ヶ月9日

どうでしょうか。(英語がわかれば)読むだけでわかるスッキリしたコードになったと思います。Durationクラス自体は以下のようにします。


Duration.java

/** 「期間」クラス。DAY_OF_MONTH日数は1ヶ月と自動的に換算する */

public class Duration {
/** 1ヶ月とみなす日数 */
public static final int DAY_OF_MONTH = 30;

//ユーザーが勝手に変更できないようにprivate(非公開)にする
private int monthNum;//月数
private int dayNum;//日数

/** monthNumヶ月dayNum日 */
public Duration(int monthNum, int dayNum){
//DAY_OF_MONTH(30)日は1ヶ月換算
this.monthNum = monthNum + dayNum/DAY_OF_MONTH;
this.dayNum = dayNum % DAY_OF_MONTH;
}

public void add(Duration duration){
this.monthNum = this.monthNum + duration.getMonthNum();
this.dayNum = this.dayNum + duration.getDayNum();

//DAY_OF_MONTH(30)日は1ヶ月換算
this.monthNum = this.monthNum + this.dayNum/DAY_OF_MONTH;
this.dayNum = this.dayNum % DAY_OF_MONTH;
}

//ユーザーはセットはできないが取得はできるようにpublic(公開)にする
public int getMonthNum(){
return monthNum;
}
public int getDayNum(){
return dayNum;
}

@Override
public String toString(){
return (monthNum/12)+"年"+(monthNum%12)+"ヶ月"+dayNum+"日";
}
}


コード中にも書いてますが、解説を加えていきます。

まず期間を表すクラスなので何ヶ月何日かを保持するためmonthNum, dayNumフィールドを保持してますが、コンストラクタやadd()メソッドではdayNumが30を超えてもそれを自動的に1ヶ月換算するように制御をかけています。そのため、ユーザーがこの制御を破って勝手にdayNumに30以上の数を入れないようにprivate(非公開)にしています。一方取得自体は自由にしてもらって構わないため、getMonthNum(), getDayNum()メソッドはpublic(公開)にしています(カプセル化)。

次に、「1ヶ月とみなす日数」は期間にかかわらず30日固定で共通のため、DAY_OF_MONTHfinalstatic(静的)にしています。そして何日間を1ヶ月と換算するかはユーザーにわかるようにしたいため、public(公開)にしています。

最後に、toString()メソッドを実装しています。Javaは全てのクラスがObjectクラスを継承していて、toString()はそれが持っているメソッドです。これを実装することで自分が作成したクラスでもデバッグモードSystem.out.println(obj);で人間が見たときにわかりやすいように表示されます。toString()というメソッドは共通していてどのように表示するかはクラスによって実装が異なるので一種のポリモーフィズムです。


最後に

最初の方にも書いたようにプログラミングで重要なのは「他人(将来の自分含む)が読み書きしやすいコードを書く」ことです。オブジェクト指向に限らずプログラミング技法を学ぶときやプログラムを書くときは「それは本当に他人(将来の自分含む)が読めて改変が容易なコードか?」を考えるようにしてください。