はじめに
プログラミングにおける一つの考え方として「カプセル化」があります。カプセル化は上手に使えばプログラムの可読性・保守性など品質向上に役立ちます。しかし、うっかりカプセルの機密性を損なってしまうようなコードの書き方をしてしまうと、せっかくのカプセル化の効用が薄れてしまいます。
この記事では、カプセルの機密性を損なってしまうようなケースとそれによって生じる課題・その解決策の一例をお伝えしたいと思います。
サンプルコードはRubyです。
カプセル化とは?
Wikipediaには「コンピュータプログラミングで用いられる概念であり、特定のデータ構造とアルゴリズムなどをまとめたソフトウェア複合体の内側の詳細を外側から隠蔽すること」と書かれています。
内部データ構造に対する外部からの想定外のアクセスを防ぐことで誤りが発生し難いプログラムとなることがメリットとして挙げられます。
Rubyの場合、オブジェクト内のインスタンス変数をアクセッサーメソッドを通じてのみデータを操作できるようにする書き方が基本かと思います。
この記事で伝えたいこと
- カプセル内にある変更可能な参照型の変数を不用意に外部に公開すると、外部でデータを変更されてしまいカプセル化が崩れてしまう
- 内部データ構造を公開すると、オブジェクトとしてひとまとめにしているデータ構造とそのデータ構造に対する手続が分離されてしまい可読性が低下する
- カプセル化が崩れると、プログラムの可読性・保守性向上などのメリットが享受できなくなる
サンプルプログラム説明
下記のサンプルプログラムにはBasketというオブジェクトがあります。このオブジェクトはフルーツが入ったバスケットを表現しています。
最初バスケットの中身は空っぽですが、add_itemメソッドによってアイテムを追加するこができます。
add_itemメソッドでは追加しようとするアイテムがフルーツであるかどうかをチェックします。アイテムがフルーツであればフルーツの配列へアイテムを追加し、フルーツでなければその他の配列へとアイテムを追加します。
バスケットにいくつかアイテムを追加した後どんなフルーツが追加されているか、外から中身を確認しようとしています。
# バスケットクラス
class Basket
attr_accessor :fruit_list
attr_accessor :other_list
def initialize
@fruit_list = []
@other_list = []
@fruit_names = ['apple', 'banana', 'orange']
end
def add_item(item)
# フルーツかどうか判定してるつもり
if @fruit_names.include?(item)
@fruit_list.push(item)
else
@other_list.push(item)
end
end
end
# バスケットクラスを利用しているところ
basket = Basket.new
basket.add_item('apple')
basket.add_item('apple')
basket.add_item('orange')
basket.add_item('chocolate')
puts '----- 登録されたフルーツを表示 -----'
basket.fruit_list.each do |item|
puts item
end
puts '----- 登録されたフルーツ以外を表示 -----'
basket.other_list.each do |item|
puts item
end
実行結果
----- 登録されたフルーツを表示 -----
apple
apple
orange
----- 登録されたフルーツ以外を表示 -----
chocolate
カプセル化が損なわれるケース
【1】参照型の変数を外部公開してしまう
サンプルプログラムはアトリビュートとして内部の配列をそのまま公開してしまっています。
そのため次のようなコードによってデータの整合性が崩れてしまうことがあります。
# バスケットクラスを利用しているところ
basket = Basket.new
basket.add_item('apple')
basket.add_item('apple')
basket.add_item('orange')
basket.add_item('chocolate') # フルーツじゃない!
# ここまではさっきのサンプルと同じ
basket.fruit_list.push('ice cream')
puts '----- 登録されたフルーツを表示 -----'
basket.fruit_list.each do |item|
puts item
end
puts '----- 登録されたフルーツ以外を表示 -----'
basket.other_list.each do |item|
puts item
end
実行結果
----- 登録されたフルーツを表示 -----
apple
apple
orange
ice cream
----- 登録されたフルーツ以外を表示 -----
chocolate
basket.fruit_list.push('ice cream')
参照型の変数をアトリビュートとして外部公開したことで、フルーツ配列に直接要素を追加することができてしまっています。
その結果「ice cream」がフルーツとして表示されています。
この書き方では防ぐことはできません。
class Basket
attr_accessor :fruit_list
↓
attr_reader :fruit_list
:
:
上記の変更で防ぐことができるのは次のように参照先を変更してしまうようなケースです。
basket.fruit_list = ['cake']
freezeメソッドはどうでしょうか?add_itemで要素が増えるので、今回のような場合にはfreezeは使えません。dupなどで強引にコピーを作れば一応防ぐことはできます。
【2】データ構造と手続が引き離されてしまう
# listだけ分離されて利用されてしまうという例
list = basket.fruit_list
hash = {'fruit_list', list}
something_method(hash)
折角バスケットクラスがデータ構造と手続をひとまとまりにしていたのに、データ構造と手続が引き離されてしまってカプセル化が崩れています。これではカプセル化のメリットを享受することができません。
変数名の命名についても課題が生じます。
basket.fruit_list
これは「バスケットのフルーツリスト」と読むことができます。
オブジェクトに従属しているものは、オブジェクト自身が一種のプレフィックス(接頭辞)や、名前空間であるかのように読み進めることができます。
# basketがとれた例
fruit_list = basket.fruit_list
# 変数名にbasketというプレフィックスを足した例
basket_fruit_list = basket.fruit_list
データ構造から分離するということは、オブジェクト変数が補っていたプレフィックスや名前空間的な性質が失われることになります。そのため改めてプレフィックスなどを付加しないと可読性が下がります。(と言いますか、そもそも分離しなければいいのですが。。。)
対策
カプセル化を守るためには、参照型の変数は公開せずにアクセス経路を用意するのが吉です。
# バスケットクラス
class Basket
# アトリビュートの公開を止める
# attr_accessor :fruit_list
# attr_accessor :other_list
def initialize
@fruit_list = []
@other_list = []
@fruit_names = ['apple', 'banana', 'orange']
end
def add_item(item)
# フルーツかどうか判定してるつもり
if @fruit_names.include?(item)
@fruit_list.push(item)
else
@other_list.push(item)
end
end
# フルーツアイテムのEnumratorを返却する
def each_fruit(&block)
@fruit_list.each(&block) if block_given?
end
# その他アイテムのEnumratorを返却する
def each_other(&block)
@other_list.each(&block) if block_given?
end
end
# バスケットクラスを利用しているところ
basket = Basket.new
basket.add_item('apple')
basket.add_item('apple')
basket.add_item('orange')
basket.add_item('chocolate')
puts '----- 登録されたフルーツを表示 -----'
# basket.fruit_list.each do |item|
basket.each_fruit do |item|
puts item
end
puts '----- 登録されたフルーツ以外を表示 -----'
# basket.other_list.each do |item|
basket.each_other do |item|
puts item
end
# basket.each_fruit.add('test')
# エラーとなる
def each_fruit(&block)
@fruit_list.each(&block) if block_given?
end
このように記述することでarrayそのものを返却するのではなく、arrayのeach文を呼び出すことができます。またblock_given?によりブロックが指定されていない場合は動作しないようしています。
これでarrayを直接返却することで生じる外部からのデータ操作を防いでいます。
終わりに
参照型の取り扱いについては初級者が一度はつまずくところだと思います。「これが理解できれば中級?」というタイトルにしてみましたがいかがでしたでしょうか。
今回のサンプルでは、参照型の中に入っているデータはただの文字列でしたが、参照型の中にさらに別の参照型が入っていたらどうなるでしょうか?そうなるとさらにカプセル化を防ぐのが大変になりますね。
こういった問題を防ぐためにImmutableオブジェクトやValueオブジェクトといった考え方がありますので、興味のある方はさらに知識を深めていっていただければと思います。
おまけ
「データ構造を分離しないで!」という話しをしてきましたが、オブジェクト+メソッド名で記述するとどうしても文字数が多くなってしまい、一見すると冗長に見えることがあります。
super_high_price_basket.fruit_list
super_high_price_basket.add_item('melon')
super_high_price_basket.add_item('mango')
そんな時はtapを使うことで見た目をシンプルに保つことができます。do 〜 end の間で短い表記を使えます。
super_high_price_basket.tap do |b|
b.fruit_list
b.add_item('melon')
b.add_item('mango')
end