hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

オブジェクト指向 参考資料

オブジェクト指向とは

オブジェクト指向とは、複数のオブジェクトを組み合わせてプログラムを構築する考え方。
オブジェクトとはデータと処理の集まり。漠然とした集まりではなく、ひとつのテーマを持った集まりである。
人をオブジェクトとして考えてみると、人には「名前」や「生年月日」、「年齢」、「住所」などのデータがある。
オブジェクトはこれらのデータ(状態)の他に振る舞いも持つ。振る舞いは自分自身に対する操作である。 オブジェクトに対して「名前を教えて」と伝えたときにオブジェクト自身が保持している名前を返す、などの動作を指す。
たとえば「鈴木さん」と「高橋さん」がいるとすると、「鈴木さん」と「高橋さん」は別々の異なるオブジェクトとなる。
鈴木さんは「名前が鈴木である」という状態を保持していて、「名前を教えて」と問えば「鈴木」と答える振る舞いを持っている。同様に高橋さんの場合は、「名前を教えて」と問えば「高橋」と答える振る舞いを持っている。

オブジェクト指向とクラス

クラスはオブジェクトの設計図である。先ほどの人をオブジェクトとして考える例をクラスで表現してみる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Person
  def initialize(name, birthday, age, address)
    @name = name
    @birthday = birthday
    @age = age
    @address = address
  end

  def answer_name
    puts @name
  end
end


//実行結果
tanaka = Person.new("tanaka", "1993/5/5", "23", "東京都渋谷区道玄坂")
=> #<Person:0x007fc55c1604f8 @name="tanaka", @birthday="1993/5/5", @age="23", @address="東京都渋谷区道玄坂">
irb(main):016:0> tanaka.answer_name
tanaka

解説

人(Person)クラスを定義して、名前や生年月日などのデータを持つようにしている。answer_nameという自分の名前を答える振る舞いも持っている。クラスの中にそのクラスとして必要なデータや振る舞いを持たせることで、そのオブジェクトの仕事はそのオブジェクトに任せることができる。

オブジェクト指向を取り入れた場合

オブジェクト指向を知っているだけではオブジェクト指向をプログラミングができるようになるわけではない。オブジェクト指向を全く取り入れずにアプリケーションを作ることもできる。アプリケーションがオブジェクト指向で作られたかどうかはユーザーに気づかれることはなく、サービスの体験が変わるわけでもない。オブジェクト指向はユーザーのための知識ではなく、開発者同士の円滑なコミュニケーションのためにある。

手続き型とオブジェクト指向

オブジェクト指向を学び始めると、まず「手続き型とオブジェクト指向は何が違うのか」というテーマにあたる。手続き型(手続き型言語)とは、「プログラムを順序どおりに実行させるような書き方」のことを言う。
例えば、「Rubyを使ってあるファイルから品物の合計金額を表示する」というタスクを考えた時に以下のように書くことができる。

出力結果
1
2
3
4
5
りんご,100,3
ばなな,180,2
ぶどう,240,1
みかん,300,5
いちご,500,2

手続き型の場合

手続き型で記述をすると以下のようなコードになる。

手続き型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# ファイルを読み込む
filename = 'sample.txt'
file = File.open(filename)

while text = file.gets
  # 1行ずつ解析する
  name, price, count = text.chomp.split(',')
  price = price.to_i
  count = count.to_i

  puts "#{name}: 合計#{price * count}円"
end

file.close

オブジェクト指向の場合

オブジェクト指向で記述すると以下のようなコードになる。Itemクラスを作成し、読み込んだファイルの値をもとにインスタンスを作成し、インスタンスメソッドで合計金額を計算するようにする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 品物クラス
class Item
  def initialize(name, price, count)
    @name = name
    @price = price
    @count = count
  end

  def total
    @price * @count
  end
end

# ファイルを読み込む
filename = 'sample.txt'
file = File.open(filename)

while text = file.gets
  # 1行ずつ解析する
  name, price, count = text.chomp.split(',')
  price = price.to_i
  count = count.to_i

  item = Item.new(name, price, count)

  puts "#{name}: 合計#{item.total}円"
end

file.close

手続き型とオブジェクト指向の比較

オブジェクト指向は手続き型に比べコード行数が増えていて、メリットが少ないように感じられる。しかし、このコードに機能追加が増えてくると、「ファイルを読み込む部分」と「品物に関して処理する部分」が分離されているので見通しが良くなることが期待できる。

このように、簡単に記述するなら手続き型風の記述は便利だが、ロジックが増えることが予想される部分はオブジェクト指向型の記述方法を取り込む。

オブジェクト指向の例

Ruby on Railsオブジェクト指向活かしたアプリケーションを作成する例を見ていく。

前提

名刺管理アプリケーションを開発していると想定してみる。
機能は以下の通り:

  • 個人の名前、メールアドレスを登録できる
  • 企業の名前を登録できる
  • 個人は企業と紐づく(所属する)

モデルについて

Ruby on Railsでは、以下のようなモデルになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Person < ActiveRecord::Base
  belongs_to :company

  # DBにname, emailカラムを持つ
end

class Company < ActiveRecord::Base
  has_many :people

  # DBにnameカラムを持つ
end

人物情報の表示

例えばあるページで次のような表示をしたいとする。

1
山田 太郎 (株式会社◯◯ 所属)

このためにViewに次のような記述を行う。

1
<%= @person.name %> (<%= @person.company.name %>)

このような表示に変えたい箇所がアプリケーション内に複数あることがわかる。
もし、すべての箇所でこの記述に変更すると、同じコードがたくさんの場所にコピーされることになる。

これだけなら、特に大きな問題にはならない。

サービスの仕様変更

サービスの仕様変更により、企業に紐付かない個人も登録することができるようになったらどうだろうか?
個人が企業に紐づくかどうかによって以下のように場合分けが必要になる。

1
2
山田 太郎 (株式会社◯◯ 所属)  <- 所属がある場合
山田 太郎 (フリーランス)     <- 所属が無い場合

Viewは以下のように変更する必要がある。

1
2
3
4
5
<% if @person.company %>
  <%= @person.name %> (<%= @person.company.name %>)
<% else %>
  <%= @person.name %>
<% end %>

変更箇所が1つなら良いが、既にアプリケーション内の多くの場所に変更を加えなければいけなくなる。
これでは、変更すべき箇所が抜け漏れたり、多くの箇所が変更されることによりバグが混入するかもしれない。

WEBサービスの不具合のうち一定数はこのような仕様変更によって発生している。

どうするべきだったか

このような仕様変更はサービスの品質向上に欠かせないものなので、「初めからそのようにViewを書いておけばよかった」だけが原因ではない。

どちらかと言うと、「このサービスには今後このような仕様変更の可能性があるな」と予測を立て、変更に強いアプリケーションの設計をしておくべきだった。これは開発者の責任である。

具体的には、以下のように変更することで今回のような大規模なViewの変更という事態は防げた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Person < ActiveRecord::Base
  belongs_to :company

  def display_info
    if company
      "#{name} (#{company.name} 所属)"
    else
      "#{name} (フリーランス)"
    end
  end
end


# view
<%= @person.display_info %>

このように共通部分を Personクラスのメソッドとして実装すれば、所属情報を変更したくなってもView側は一切変更する必要がなくなった。

オブジェクト指向のメリット

このように、「人物情報を出力する」という処理をまるごと「人物 (Person)」の中に入れてしまうことで、将来発生するだろう面倒を避けることができる。

これが、オブジェクト指向を活用して得られる多くのメリットの内の1つ。

世の中のアプリケーションやサービスは唯一無二なものを目指して開発されるが、何十年にも渡るソフトウェア開発の歴史なかで本当に新しい概念を使って開発されるものはほとんどない。
おおよそ、今までに数多く作られた仕組みを部分的に再利用して開発していることが多い。

その叡智の一つがオブジェクト指向

オブジェクト指向に則ったアプリケーションを開発することで、開発時に現れる課題が自動的に解決される。

どこまで予測するべきか

実際の開発では時間的な制限もあり、どこまで予測すればいいのか悩むこともある。たとえば、display_infoというメソッドを作ったにもかかわらず、そのような仕様変更が発生しなかった、ということにもなりかねない。それでは、Personクラスの行数とそこにかけた開発工数が増えただけでメリットが無いようにも思える。

そのバランスはサービスを開発しているエンジニアの設計力や予測力に依存します。
この力こそが良いエンジニアとそうでない人を分ける境界とも言える。
一朝一夕で身につけることはできません。この力を早く身につけられるように意識しながら日々の開発を進めていく。

クラスとインスタンス

上記の名刺管理アプリケーションの例を使って用語の説明をします。

  • Person
    • クラス。具体的なデータに紐付かない「データの抽象的な振る舞い」を設定するもの。
  • @person
    • Personクラスの インスタンス。クラスから生成される「具体的なデータとそれに対する処理」をまとめたもの。
  • @person.name
    • プロパティ。インスタンスが持つデータのこと。Ruby on Railsの場合には、データベースのカラムと同一のものを指している事が多い。
  • @person.display_info

厳密には、Rubyという言語の性質上、プロパティとメソッドは実は同じ仕組みで成り立っており違いはないのだが、「オブジェクト指向」として考えた時には違いがあるので区別した。

役に立つ原則

オブジェクト指向を活用したプログラミング手法は長年研究されていて、いくつかの原則としてまとめられている。

ここでは、Ruby on Railsを使ったWEBアプリケーションの開発にあたって知っておくと便利な原則を幾つかピックアップして紹介する。

これ以外にも「オブジェクト指向の原則」は様々なものがあるので、気になる方は調べてみる。

DRY

DRYとは、Don't Repeat Your Selfの略で繰り返しを避けるという原則。アプリケーションのコードはどのコードも将来バグになる可能性がある。重複があると、アプリケーションのコードは不必要に大きくなり、それにつれて、バグが生じる危険性も高まる。また、システムの構造は、そう意図していないにもかかわらず複雑になってしまう。重複によりコードベースが大きくなれば、開発に携わる人間がシステム全体を完全に理解することも難しくなる。特に困るのはコードに変更を加える時である。どこかに変更を加えた場合、それとロジック等が重複している箇所にも同様の変更が必要かどうか確認しなければならない。DRY原則を守ればこのような事態を防ぐことができる。

YAGNI

YAGNIとは、You Aren't Gonna Need It の略で実際に必要になった機能だけを追加すべきという原則。漠然とした予想や見込みで追加された機能が使われないまま放置されたり、プログラムの規模が必要以上に大きくなることで設計や構造が複雑になり保守や修正が難しくなり、バグや不具合が起きやすくなる。つまり、今現在具体的な必要性がないのに、将来必要になるかもしれない、あれば便利かもしれないなどといった見込みや思い込みで機能や要素を追加するべきではないとうことになる。

単一責任の原則

単一責任の原則とは、クラスが果たす役割を一つだけにするという原則。例えば、Rectangleクラス(四角形クラス)があるとする。このクラスには四角形を描くメソッドと四角形の面積を計算するメソッドがあるとします。Rectangleクラスには描画と計算の2つの役割があることになる。計算方法を変える(単位の変更など)時と、描画方法を変える(描画する正方形に色をつけるなど)時でクラスの記述を変更する理由が2つになる。役割が増えるほど変更理由が増え、変更が起きやすくなる。このような時は役割ごとにクラスを分けるべきである。役割ごとに分けることで変更する際に該当するクラスを変更するだけで済む。

インターフェイス分離の原則

あるクラスを利用するクライアントが複数存在するような場合に、各クライアントに共通の大きなインターフェイスを使ってしまうと、各クライアント内では使用しないメソッドがあった場合、クラスが使わないメソッドにも依存してしまう。つまり、各クライアントが利用していないメソッドに対する変更の影響まで受けてしまう可能性があるということになる。そこで各クライアントが呼び出す必要のあるサービスの単位でクライアントをグループ分けし、そのグループに特化したインターフェイスを準備することで、異なるサービス間での関連性を分断することができる。