[オブジェクト指向 ]なぜクラスを使うのか~理由やメリットは?~

スポンサーリンク

対象読者

以下に飽き飽きした方

  • 「猫クラス、犬クラス」等の説明
  • 抽象的で難しい用語

結論

主流な言語やオープンソースのライブラリ、フレームワークにはオブジェクト指向が当たり前のように取り入られている。今やオブジェクト指向はプログラマにとって必修科目だ。

クラスはオブジェクト指向の土台である。しかし、クラスを使う動機やメリットを理解することは容易ではない。抽象的で難しい用語が溢れていて、初心者は圧倒されてしまうからだ。

例えば、オブジェクト指向の3大要素(カプセル化、継承、ポリモーフィズム)、単一責任の原則、デザインパターンなどだ。

確かに、難解でしゃらくさいオブジェクト指向の原則を理解することは大事だ。しかし、そんなことは棚上げしても、クラスを使うことには十分なメリットがある。

そこで本記事では、クラスの最低限のメリットを解説し、クラスを使う動機の理解を通して、オブジェクト指向へ馴染んでいくための基礎を提供する。

まずは、クラスは手続き型プログラミングの限界を超えるために生まれたことを見る。

オブジェクト指向は難しい

オブジェクト指向は難しい。なぜなら、プログラムに対する考え方が不自然だからだ。

自然に考えるならば、プログラムは処理を順番に並べることで表現することになる。同様に、学校の運動会のプログラムも時刻順に競技を箇条書きする。このようなプログラミングの捉え方を「手続き型プログラミング」という。

何の前提知識もない初心者は十中八九、手続き型プログラミングによってコーディングを行う。私もそうだった。「まずはこういうデータを用意して、その値がこうならば、こう処理して…」みたいな感じで手続きを箇条書きしていくことでプログラムを作っていく。

手続き型プログラミングで主役となる構成要素は以下だ。

  • グローバル変数
  • 関数(サブルーチン、プロシージャとも言う)
  • 条件分岐(if文など)
  • 繰り返し文(for文、while文など)

main文はこれらを駆使して、いろんな処理を順次実行していくことでプログラムは目的を達成することになる。

手続き型プログラミングの限界 ~グローバル変数~

しかし、手続き型プログラミングでは、main文がグローバル変数や実効順序、条件分岐、関数の呼び出し等を全て管理しなければならない

故に、main文にはたくさんの記述が必要となり、理解することが難しくなる。その結果、バグが生まれやすく、機能追加、変更が難しくなってしまう。

main文の複雑さを緩和するために、まとまった処理を切り出す仕組みが関数である。関数を適切に使用すれば、理解しやすいコードを書くことができる。

こうして、main文は多くの細かい関数の組み合わせへと還元される。しかし、ここに相反する要求が生じる。すなわち、

  • 関数を分離・独立させたい
  • 関数を組み合わせたい

という矛盾だ。これを解決(妥協)するためには、グローバル変数が必要となる。

複数の関数がプログラムの目標達成に向けて協力するためには、関数同士が共通のデータをやり取りする必要がある。グローバル変数によって関数間での情報をやり取りできる。しかし、グローバル変数は関数同士を結び付け、独立性を損ねてしまうし、どこからでも値を変更できてしまうが故に、コードを理解しづらくしてしまう。

グローバル変数は値の推移を理解しづらくし、main文を汚してしまう。これが手続き型プログラミングの限界だ。

クラスとは関連する変数と関数をまとめる仕組み

手続き型では、グローバル変数と関数は別々に定義されていた。しかし、グローバル変数と関数は独立しているのだろうか。

実は、グローバル変数と関数には関連があることが多い。すなわち、複数の関数は同じグローバル変数を共有することがある。

それらの関連あるグローバル変数と関数をまとめることができれば、グローバル変数の問題を回避しつつも、関数間でのデータのやり取りが可能となる。

例えば、三角形の計算に関連する関数があるとする。グローバルな座標p1,p2,p3は以下の3つの関数と関連している。

function calc_triangle_area(){
  global p1, p2, p3  //グローバルな座標
  //面積の計算
}

function calc_degree(){
  global p1, p2, p3
  //p1,p2の線分とp1,p3の線分が成す角度の計算
}

function normal_vector(){
  global p1, p2, p3
  //三角形の法線ベクトルの計算
}

これをクラスで書き換えてみる。

class Triangle {
  constructor(p1, p2, p3){
    this.p1 = p1
    this.p2 = p2
    this.p3 = p3
  }

  calc_area(){
    //this.p1, this.p2, this.p3を用いて面積を計算する
  }
  calc_degree(){/*省略*/}  
  normal_vector(){/*省略*/}
}

つまり、グローバル変数と関数を関連によって分類(class)し、まとめるための仕組みがクラスなのだ。

コンストラクタ(constructor)とは、クラスをインスタンス化する時に用いる特殊なメソッドだ。

この程度の関数ならば、引数として座標を与えれば良いと思うかもしれないし、実にその通りだと思う。

クラス(というかインスタンス)は様々な変数を持つことができ、これをインスタンス変数という。

プロパティ、メンバ変数、フィールド、インスタンス変数

クラスはユーザ定義型(構造体)の拡張として生まれた。

絵描くか?

クラスは構造体の拡張

グローバル変数と関数には関連性がある。

インスタンススコープ

手続き型プログラミングでは成し遂げられないクラスのメリットは以下である。

  1. グローバルなスコープを制限できる
  2. 名前の衝突を避けられ、短い名前にできる
  3. 引数を削減して、関数の呼び出しを簡素にできる
  4. たくさん作る

根拠

そもそも、今までのコーディングで何の問題もなかったし、クラス等のオブジェクト指向を敢えて取り入れるメリットが感じられない方もいるかもしれない。しかし、現在の主流はオブジェクト指向であり、オープンソースのライブラリやフレームワーク等もオブジェクト指向で書かれていることが多い。手続き型プログラミングには限界があるからだ

そこで、私がオブジェクト指向初心者の頃に感じたクラスのメリットを述べる。以下の説明は、手続き型プログラミングの延長で理解できる内容だ。オブジェクト指向でよくある説明の「猫クラス、犬クラス」がどうとか、ポリモーフィズムが何だとか、そういう解説に飽き飽きした方に、地に足のついたメリットを解説する。

1.グローバルなスコープを制限できる[1]

クラスによって、グローバルな変数や関数を分割して管理することができる。多数のファイルをフォルダに分けるのと同様のことだ。そうすることにより、一度に把握すべき情報量を削減でき、プログラムの把握を容易にできる。

クラスには値を持たせることができる(メンバ変数、インスタンス変数、プロパティなどと呼ばれるが、以後プロパティと呼ぶ)。プロパティは以下の意味で、グローバル変数とローカル変数の中間の変数だ。

  • クラス内の関数(以後、メソッドと呼ぶ)からはプロパティが自由に呼べるため、関数に対するグローバル変数みたいだ
  • クラスから生成した実体(インスタンス)を経由しない限り、外からプロパティを参照できないため、外部からのアクセスが制限されたという意味で、関数内のローカル変数みたいだ

手続き型の言語でも、モジュール化は可能であり、グローバル変数や関数をまとめることができる。しかし、そのモジュールを複数個importした場合、その制御プログラム内でモジュールの内容が全て展開されてしまうため、やはりプログラムの把握が困難になる。複数個のフォルダをマージすると、大量のファイルが1つのパスに集まり、雑多な感じがすることと同じだ。

一方、複数個のクラスをインスタンス化して呼び出しても、プロパティやメソッドはそのインスタンス内にあり、制御プログラム内でゴチャゴチャになることはない。

2.名前の衝突を避けられ、短い名前にできる

意味合い的に似たような処理(関数)が複数ある場合、手続き型プログラミングでは、どう区別するだろうか。prefixやsuffixを名前に付与するにより、区別する。しかし、クラスを用いれば、クラス名がprefixになるため、それらを区別でき、かつ、関数(メソッド)名が短くなって良い。

  • write_csv(list)  -> csv.write(list)
  • write_json(list) -> json.write(list)

3.引数を削減して、関数の呼び出しを簡素にできる

引数の数が4,5個を超えてくると、コードが読みにくくなる。一度に把握しなければならない情報が多くなるからだ。1.でも述べたように、プロパティはメソッド間で共有される。引数としてではなく、プロパティとして情報を与えることにより、引数の数を削減できる。

func_a(x, y, z, a, p)

func_b(x, y, z, b, q)

みたいに、複数の関数に同じ引数を与えるとする。

foo = new Foo(x, y, z)

foo.a(a, p)

foo.b(b, q)

こちらの方が読みやすい。

4.たくさんつくる[2]

グローバル変数はプログラム内に一意に存在する。故に、同じ意味のグローバル変数を複数個使いたい場合、その数だけ定義をしなければならない。しかし、クラスを用いれば、いちいちグローバル変数を定義しなくても済む。

ファイルオープンを同時に複数回行いたいとする。手続き型プログラミングの場合、複数個のファイル番号を変数で定義しなければならない。一方、ファイルIOのクラスにファイル番号のプロパティを持たせておけば、この問題を解決できる。クラスを実体化(インスタンス化)したときに、自動的にファイル番号を生成し、ファイル番号を個別に持たせることができるからだ。

以下に、fortran90 likeな疑似コードでの複数ファイルへの同時書き込みのコードを示す。

fileNo1 = 1
fileNo1 = 2
fileNo1 = 3

open(fileNo1, file=filename1)
open(fileNo2, file=filename2)
open(fileNo3, file=filename3)

write(fileNo1, *) "Hello1"
write(fileNo2, *) "Hello2"
write(fileNo3, *) "Hello3"

close(fileNo1)
close(fileNo2)
close(fileNo3)

一方で、クラスによる複数ファイルへの同時書き込みのコードを示す。

file1 = new File()
file2 = new File()
file3 = new File()

file1.write("Hello1")
file2.write("Hello2")
file3.write("Hello3")

file1.close()
file2.close()
file3.close()

ファイル数の増加に対して、より簡単に対処できるのはクラスによる表現である。

参考

  1. リーダブルコード [Dustion Boswell]
  2. オブジェクト指向でなぜつくるのか [平澤 章]

コメント

タイトルとURLをコピーしました