🔧

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

2022/08/02に公開

本題の前に

まず「オブジェクト指向」という言葉を知っただけのまったくの初心者の方のために例を出してイメージをつかんでもらいます。あくまで例なので色々不完全なのはご了承ください。言語はJavaですがJavaを知らなくても何となく分かるように書きます。

オブジェクト指向とは (即物的に言えば) データとそれに関わるメソッドを クラス(class) にまとめてプログラミングすることです。
例えば友達の情報を管理するために以下のような「友人」クラスを考えてみましょう。

Friend.java
class Friend {
    String name;//名前
    String phoneNumber;//電話番号
    String mailAddress;//メールアドレス

    Friend(String name, String phoneNumber, String mailAddress) {
        //thisは自身を意味する
        this.name = name;
        this.phoneNumber = phoneNumber;
        this.mailAddress = mailAddress;
    }

    /**
     * 連絡先を出力
     */
    void outputContact() {
        System.out.println(name);
        System.out.println("電話番号:" + phoneNumber);
        System.out.println("メール:" + mailAddress);
    }
}

このクラスではname, phoneNumber, mailAddressという3つの フィールド(メンバ変数) を持っていて、それを用いた メソッド outputContact()が存在します。
クラス名と同じ名前のメソッドFriend(String name, String phoneNumber, String mailAddress)コンストラクタ と呼ばれ以下のように使います。

Friend tanaka = new Friend("田中一郎", "03-444-444", "tanaka@qmail.com");

Friendコンストラクタを見ればわかるように、このコードではnameフィールドに"田中一郎"、phoneNumberフィールドに"03-444-444"、mailAddressフィールドに"tanaka@qmail.com"をセットしています。
そして以下のようにメソッドを呼び出すと連絡先が表示されます。

tanaka.outputContact();
/*
田中一郎
電話番号:03-444-444
メール:tanaka@qmail.com
*/

このようにクラスというのは「それがどんな情報を持つべきか」(ここでは名前と電話番号とメールアドレス)、「それがどんな機能を持つか」(ここでは連絡先の出力)を定義した 設計図 で、それに対してnewでコンストラクタを呼び出して実際にデータを入れて実体化したものを インスタンス と言います。
このようにクラスを作ることの利点の一つはデータと機能をひとまとめにしていくつも作れることです。もしクラスを使わない場合、友人が何人もいるとするとこんなコードになってしまいます。

String tanakaName = "田中一郎";
String tanakaPhoneNumber = "03-444-444";
String tanakaMailAddress = "tanaka@qmail.com";
String yamadaName = "山田太郎";
String yamadaPhoneNumber = "03-555-555";
String yamadaMailAddress = "yamada@qmail.com";
String suzukiName = "鈴木花子";
String suzukiPhoneNumber = "03-666-666";
String suzukiMailAddress = "suzuki@qmail.com";
//以下続く...

System.out.println(tanakaName);
System.out.println("電話番号:"+tanakaPhoneNumber);      
System.out.println("メール:"+tanakaMailAddress);
System.out.println(yamadaName);
System.out.println("電話番号:"+yamadaPhoneNumber);      
System.out.println("メール:"+yamadaMailAddress);
System.out.println(suzukiName);
System.out.println("電話番号:"+suzukiPhoneNumber);     
System.out.println("メール:"+suzukiMailAddress);
//以下続く...

わかりにくくて面倒ですね。

Friend tanaka = new Friend("田中一郎", "03-444-444", "tanaka@qmail.com");
Friend yamada = new Friend("山田太郎", "03-555-555", "yamada@qmail.com");
Friend suzuki = new Friend("鈴木花子", "03-666-666", "suzuki@qmail.com");
//以下続く...
tanaka.outputContact();
yamada.outputContact();
suzuki.outputContact();
//以下続く...

と書ければスッキリします。

はじめに

ではいよいよオブジェクト指向の核心を解説をしたいところですが、このオブジェクト指向、かなり古くからあり現在でも大勢の人が使用しているにも関わらず何を意味しているかイマイチわかりません。あるページを見ると「カプセル化」「継承」「ポリモーフィズム」の要素を持つものと書いてある。しかしオブジェクト指向の提唱者はメッセージングと言っているWikipediaを見ても歴史が長々と続き、提唱されたあと他の人もいろいろ付け加えたりしたせいでそもそもハッキリした定義がないと書いてある。

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

リーダブルコード

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

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("B");
System.out.println(list.get(0));//C

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

カプセル化

オブジェクト指向のもっとも重要な特長がカプセル化です。これは「ユーザーが使うのに必要な部分だけ可視化してそれ以外は隠す」ということです。プログラミング言語的に言えば public(公開)private(非公開) を適切に設定することです(Pythonでは通常はpublicになり、先頭に「__」(アンダーバー2つ)をつけると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日」と出力します。

オブジェクト指向なしにプログラムを書くと以下のようになります(Javaでは整数同士の割り算は整数部のみが返ります)。

int monthSum = 0;
int daySum = 0;

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

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

System.out.println((monthSum/12)+"年"+(monthSum%12)+"ヶ月"+daySum+"日");//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日は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日は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()というメソッドは共通していてどのように表示するかはクラスによって実装が異なるので一種の ポリモーフィズム です。

最後に

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

Discussion