Ruby/Railsで学ぶオブジェクト指向入門 カプセル化って何ぞや
はじめに
こんにちは!本記事では、Ruby/Railsを使ってオブジェクト指向プログラミングの基礎について記載しています。クラスとオブジェクト編の続きとなります。
3. カプセル化とアクセス修飾子
3.1 カプセル化の概念
カプセル化(Encapsulation)は、オブジェクト指向プログラミングの重要な原則の一つです。カプセル化には主に以下の2つの側面があります:
-
データと振る舞いをひとまとめにする:オブジェクトに関連するデータ(属性)とそれを操作するメソッド(振る舞い)を一つのクラスにまとめます。
-
実装の詳細を隠蔽する:オブジェクトの内部データを外部から直接アクセスできないようにし、代わりに安全に制御されたメソッドを通じてのみアクセスを許可します。
カプセル化の利点:
- データの保護:オブジェクトの内部状態を不正な変更から守ります。
- 実装の変更容易性:内部実装を変更しても、外部インターフェース(公開メソッド)が同じであれば、他のコードに影響を与えません。
- 複雑さの管理:使用者がオブジェクトの内部の複雑な実装を理解する必要がなくなります。
例えば、銀行口座を表すクラスでは、残高(@balance
)を直接変更できないようにし、入金(deposit
)や出金(withdraw
)といったメソッドを通じてのみ変更できるようにします:
class BankAccount
def initialize(account_number, owner_name, initial_balance = 0)
@account_number = account_number
@owner_name = owner_name
@balance = initial_balance
end
# 残高の読み取りは許可
def balance
@balance
end
# 直接残高を変更するメソッドは提供しない
# 代わりに、deposit/withdrawメソッドを通じてのみ変更可能
def deposit(amount)
if amount > 0
@balance += amount
puts "#{amount}円を入金しました"
true
else
puts "入金額は正の値である必要があります"
false
end
end
def withdraw(amount)
if amount > 0 && amount <= @balance
@balance -= amount
puts "#{amount}円を出金しました"
true
else
puts "出金できません(金額が不正か残高不足です)"
false
end
end
end
account = BankAccount.new("12345", "山田太郎", 10000)
puts account.balance # => 10000
account.deposit(5000)
puts account.balance # => 15000
account.withdraw(3000)
puts account.balance # => 12000
# balance=メソッドは定義されていないため、以下はエラーになる
# account.balance = 1000000 # => NoMethodError
このように、カプセル化によって、オブジェクトの使用者はクラスの実装詳細を知る必要がなく、公開されたメソッドを通じて安全にオブジェクトを操作できます。
3.2 private / protected / public の使い方
Rubyには、メソッドのアクセス制御のために3つのアクセス修飾子があります:
- public(公開):デフォルトのアクセスレベルです。クラスの外部からアクセス可能です。
- private(非公開):クラスの外部からアクセスできません。同じクラス内のメソッドからのみ呼び出せます。
- protected(保護):クラスの外部からはアクセスできませんが、同じクラスか、そのサブクラスのインスタンスからはアクセスできます。
以下の例で、それぞれの使い方を見てみましょう:
class Person
def initialize(first_name, last_name, age)
@first_name = first_name
@last_name = last_name
@age = age
end
# publicメソッド(デフォルト)
def full_name
"#{@first_name} #{@last_name}"
end
def greet
"こんにちは、#{full_name}です!"
end
def adult?
check_age # privateメソッドを内部で使用
end
def older_than?(other_person)
@age > other_person.get_age # protectedメソッドを使用
end
# protectedメソッド
protected
def get_age
@age
end
# privateメソッド
private
def check_age
@age >= 20 # 日本の成人年齢
end
end
# 使用例
person1 = Person.new("太郎", "山田", 25)
person2 = Person.new("花子", "鈴木", 18)
puts person1.full_name # => 太郎 山田
puts person1.greet # => こんにちは、太郎 山田です!
puts person1.adult? # => true
puts person2.adult? # => false
puts person1.older_than?(person2) # => true
puts person2.older_than?(person1) # => false
# エラーになる呼び出し
# puts person1.get_age # => NoMethodError (protected method)
# puts person1.check_age # => NoMethodError (private method)
アクセス修飾子の使い分け
-
public:クラスの外部インターフェースを構成するメソッドに使用します。一般的に、オブジェクトの主な機能を提供するメソッドはpublicにします。
-
private:クラスの内部実装の詳細を隠蔽するために使用します。これらのメソッドはクラス自身の内部処理のためのものであり、外部からは呼び出せません。
-
protected:同じクラスのインスタンス間での比較や協調動作が必要な場合に使用します。例えば、2つのオブジェクトを比較するメソッドなど。
Rubyのprivateメソッドの特徴
Rubyのprivateメソッドにはいくつかの特徴があります:
- 明示的なレシーバ(呼び出し元オブジェクト)を指定できません(Ruby 2.7以前)。
- 継承されます。
-
self
をレシーバにすることはできません(Ruby 2.7以前)。
Ruby 2.7以降では、privateメソッドに対してself
をレシーバとして使用できるようになりました。
class MyClass
def public_method
private_method # OK
self.private_method # Ruby 2.7以降はOK、それ以前はエラー
end
private
def private_method
puts "This is a private method"
end
end
obj = MyClass.new
obj.public_method # OK
# obj.private_method # エラー
3.3 ゲッターとセッターの活用
オブジェクトの属性(インスタンス変数)にアクセスするために、ゲッターメソッド(読み取り用)とセッターメソッド(書き込み用)を使用します。Rubyでは、これらのメソッドを簡単に定義するための以下のマクロが用意されています:
- attr_reader:読み取り専用のゲッターメソッドを定義します。
- attr_writer:書き込み専用のセッターメソッドを定義します。
- attr_accessor:読み書き両方のゲッターとセッターメソッドを定義します。
class Student
# name属性の読み書きメソッドを生成
attr_accessor :name
# id属性の読み取り専用メソッドを生成
attr_reader :id
# grade属性の書き込み専用メソッドを生成
attr_writer :grade
def initialize(id, name)
@id = id
@name = name
@grade = nil
end
# gradeの読み取り用のカスタムメソッド
def grade
if @grade.nil?
"成績はまだ登録されていません"
else
"成績: #{@grade}"
end
end
end
student = Student.new(1, "田中")
puts student.name # => 田中
puts student.id # => 1
puts student.grade # => 成績はまだ登録されていません
student.name = "田中修正"
puts student.name # => 田中修正
student.grade = "A"
puts student.grade # => 成績: A
# エラーになる操作
# student.id = 2 # => NoMethodError (呼び出せるセッターがない)
カスタムゲッターとセッター
単純にインスタンス変数の値を返す/設定するだけではなく、追加のロジックが必要な場合は、カスタムのゲッターやセッターメソッドを定義できます:
class Product
attr_reader :name, :price
def initialize(name, price)
@name = name
self.price = price # セッターを使って初期化
end
# カスタムセッター
def price=(new_price)
if new_price >= 0
@price = new_price
else
raise ArgumentError, "価格は0以上である必要があります"
end
end
# カスタムゲッター
def price_with_tax
(@price * 1.1).round # 10%の消費税を追加
end
end
product = Product.new("本", 1000)
puts product.price # => 1000
puts product.price_with_tax # => 1100
product.price = 1500
puts product.price # => 1500
puts product.price_with_tax # => 1650
# エラーになる操作
# product.price = -500 # => ArgumentError: 価格は0以上である必要があります
メソッド名の命名規則
Rubyでは、セッターメソッドは属性名=
という形式で定義します。そのため、attr_writer :name
は実際にはname=
というメソッドを定義しています。
また、真偽値を返すメソッド(predicateメソッド)は慣習的に名前の末尾に?
をつけます(例:empty?
、valid?
など)。
class User
attr_reader :name, :admin
def initialize(name, admin = false)
@name = name
@admin = admin
end
# predicateメソッド
def admin?
@admin
end
def name_valid?
!@name.nil? && @name.length >= 3
end
end
user1 = User.new("山田太郎", true)
user2 = User.new("佐藤")
puts user1.admin? # => true
puts user2.admin? # => false
puts user1.name_valid? # => true
puts user2.name_valid? # => true
3.4 実践問題と解説(2問)
問題1: 体温管理クラスの作成
健康管理アプリのために、体温を記録・管理するクラスTemperatureTracker
を作成してください。以下の要件を満たす必要があります:
- 体温データを追加できる(摂氏温度で)
- 体温データの一覧を取得できる
- 最高体温、最低体温、平均体温を取得できる
- 体温が正常範囲内(36.0℃~37.5℃)かどうかをチェックできる
- 摂氏と華氏の変換ができる(内部では摂氏で保存)
解答:
class TemperatureTracker
# 読み取り専用のゲッター
attr_reader :temperatures
# 定数
NORMAL_RANGE = 36.0..37.5 # 正常体温の範囲(摂氏)
def initialize
@temperatures = []
end
# 体温データを追加するメソッド
def add_temperature(temp, unit = :celsius)
# 華氏の場合は摂氏に変換
celsius_temp = (unit == :fahrenheit) ? fahrenheit_to_celsius(temp) : temp
if valid_temperature?(celsius_temp)
@temperatures << celsius_temp
puts "体温 #{celsius_temp.round(1)}℃ を記録しました"
return true
else
puts "無効な体温値です(#{celsius_temp.round(1)}℃)"
return false
end
end
# 最高体温を取得するメソッド
def max_temperature
return nil if @temperatures.empty?
@temperatures.max
end
# 最低体温を取得するメソッド
def min_temperature
return nil if @temperatures.empty?
@temperatures.min
end
# 平均体温を取得するメソッド
def average_temperature
return nil if @temperatures.empty?
@temperatures.sum / @temperatures.size
end
# 最新の体温が正常範囲内かチェックするメソッド
def latest_temperature_normal?
return false if @temperatures.empty?
NORMAL_RANGE.include?(@temperatures.last)
end
# 摂氏温度を華氏に変換するメソッド
def celsius_to_fahrenheit(celsius)
(celsius * 9/5.0) + 32
end
# 統計情報を表示するメソッド
def display_stats
if @temperatures.empty?
puts "記録された体温データがありません"
return
end
puts "体温記録の統計:"
puts "記録回数: #{@temperatures.size}回"
puts "最高体温: #{max_temperature.round(1)}℃ (#{celsius_to_fahrenheit(max_temperature).round(1)}°F)"
puts "最低体温: #{min_temperature.round(1)}℃ (#{celsius_to_fahrenheit(min_temperature).round(1)}°F)"
puts "平均体温: #{average_temperature.round(1)}℃"
puts "最新の体温: #{@temperatures.last.round(1)}℃ (#{celsius_to_fahrenheit(@temperatures.last).round(1)}°F)"
puts "体温状態: #{latest_temperature_normal? ? '正常' : '異常'}"
end
private
# 華氏温度を摂氏に変換するメソッド
def fahrenheit_to_celsius(fahrenheit)
(fahrenheit - 32) * 5/9.0
end
# 体温値が有効かチェックするメソッド
def valid_temperature?(temp)
temp.is_a?(Numeric) && temp >= 30 && temp <= 45
end
end
# 使用例
tracker = TemperatureTracker.new
# 体温データの追加(摂氏)
tracker.add_temperature(36.7)
tracker.add_temperature(37.2)
tracker.add_temperature(36.5)
# 体温データの追加(華氏)
tracker.add_temperature(99.5, :fahrenheit) # 約37.5℃
# 無効なデータ
tracker.add_temperature(42.0) # 高すぎる
tracker.add_temperature(25.0) # 低すぎる
# 統計情報の表示
tracker.display_stats
解説:
このTemperatureTracker
クラスでは、カプセル化の原則に従って以下のように実装しています:
-
データの保護:体温データは
@temperatures
配列に保存されますが、このデータは直接変更できません(attr_reader
のみで、attr_writer
はありません)。データの追加はadd_temperature
メソッドを通じてのみ行えます。 -
メソッドのアクセス制御:
-
publicメソッド:ユーザーが直接使用するメソッド(
add_temperature
,max_temperature
など)。 -
privateメソッド:内部実装の詳細である
fahrenheit_to_celsius
とvalid_temperature?
メソッド。これらはクラスの外部からは呼び出せません。
-
publicメソッド:ユーザーが直接使用するメソッド(
-
データの検証:
add_temperature
メソッドでは、追加される体温値が有効かどうかをvalid_temperature?
メソッドでチェックし、無効な値は追加されないようにしています。 -
単位の変換:華氏温度が入力された場合は、内部で使用される摂氏温度に変換されます。これにより、内部データの一貫性が保たれます。
このクラスは単純な体温記録機能に限定されており、そのための適切なメソッドのみを公開しています。内部実装の詳細(単位変換や値の検証)は隠蔽されており、必要に応じて変更できますが、クラスの使用者に影響を与えることはありません。
問題2: ユーザー認証システムの作成
ウェブアプリケーションのための簡易的なユーザー認証システムを作成してください。以下の要件を満たす必要があります:
- ユーザー情報(ユーザー名、メールアドレス、パスワード)を管理する
- パスワードは暗号化して保存する
- ユーザー登録、ログイン機能を提供する
- ユーザー情報の更新機能を提供する
- パスワードは外部から直接アクセスできないようにする
解答
require 'digest' # パスワードのハッシュ化に使用
class User
# 読み取り専用の属性
attr_reader :username, :email
def initialize(username, email, password)
@username = username
self.email = email # バリデーションのためにセッターを使用
set_password(password) # privateメソッドを使用
@logged_in = false
end
# メールアドレスのセッター(バリデーション付き)
def email=(new_email)
if valid_email?(new_email)
@email = new_email
else
raise ArgumentError, "無効なメールアドレスです: #{new_email}"
end
end
# パスワードの変更
def change_password(old_password, new_password)
if authenticate(old_password)
set_password(new_password)
puts "パスワードが変更されました"
return true
else
puts "現在のパスワードが正しくありません"
return false
end
end
# ユーザーの認証(ログイン)
def authenticate(password)
hashed_input = hash_password(password, @salt)
return hashed_input == @hashed_password
end
# ログイン状態を管理するメソッド
def login(password)
if authenticate(password)
@logged_in = true
puts "#{@username}さんがログインしました"
return true
else
puts "ユーザー名またはパスワードが正しくありません"
return false
end
end
# ログアウトメソッド
def logout
if @logged_in
@logged_in = false
puts "#{@username}さんがログアウトしました"
else
puts "ログインしていません"
end
end
# ログイン状態を確認するメソッド
def logged_in?
@logged_in
end
# ユーザー情報を表示するメソッド
def display_info
puts "ユーザー名: #{@username}"
puts "メールアドレス: #{@email}"
puts "ログイン状態: #{@logged_in ? 'ログイン中' : 'ログアウト'}"
end
private
# パスワードをハッシュ化して保存するメソッド
def set_password(password)
if valid_password?(password)
@salt = generate_salt # ソルトを生成
@hashed_password = hash_password(password, @salt)
else
raise ArgumentError, "パスワードは8文字以上必要です"
end
end
# ソルトを生成するメソッド
def generate_salt
SecureRandom.hex(10)
end
# パスワードとソルトを組み合わせてハッシュ化するメソッド
def hash_password(password, salt)
Digest::SHA256.hexdigest(password + salt)
end
# メールアドレスの形式を検証するメソッド
def valid_email?(email)
# 簡易的なメール形式チェック(実際のアプリケーションではもっと厳密に)
email.to_s.include?('@') && email.to_s.include?('.')
end
# パスワードの強度を検証するメソッド
def valid_password?(password)
password.to_s.length >= 8 # 8文字以上
end
end
# ユーザー管理クラス
class UserManager
def initialize
@users = {} # ユーザー名をキー、Userオブジェクトを値とするハッシュ
end
# ユーザー登録
def register(username, email, password)
if @users[username]
puts "ユーザー名 #{username} は既に使用されています"
return false
end
begin
@users[username] = User.new(username, email, password)
puts "ユーザー #{username} が登録されました"
return true
rescue ArgumentError => e
puts "ユーザー登録エラー: #{e.message}"
return false
end
end
# ユーザーを取得
def get_user(username)
@users[username]
end
# ユーザーのログイン
def login(username, password)
user = get_user(username)
if user
return user.login(password)
else
puts "ユーザー #{username} が見つかりません"
return false
end
end
# 登録ユーザー一覧(デバッグ用)
def list_users
puts "===== 登録ユーザー一覧 ====="
if @users.empty?
puts "登録ユーザーはいません"
else
@users.each_key do |username|
puts "- #{username}"
end
end
end
end
# 使用例
user_manager = UserManager.new
# ユーザー登録
user_manager.register("tanaka", "tanaka@example.com", "password123")
user_manager.register("suzuki", "suzuki@example.com", "securepw456")
# 無効なデータでの登録
user_manager.register("yamada", "invalid-email", "pw") # 無効なメールとパスワード
user_manager.register("tanaka", "another@example.com", "newpassword") # 既存ユーザー名
# ユーザー一覧
user_manager.list_users
# ログイン
user_manager.login("tanaka", "password123") # 成功
user_manager.login("tanaka", "wrongpassword") # 失敗
user_manager.login("nonexistent", "anypassword") # 存在しないユーザー
# ユーザー情報とパスワード変更
tanaka = user_manager.get_user("tanaka")
if tanaka
tanaka.display_info
tanaka.change_password("password123", "newpassword456")
tanaka.logout
tanaka.login("newpassword456") # 新しいパスワードでログイン
end
解説
このユーザー認証システムでは、User
クラスとUserManager
クラスの2つのクラスを使用して、効果的にカプセル化を実現しています:
1. User クラス
-
データの保護:パスワードは直接アクセスできないように完全に隠蔽されています。パスワードはハッシュ化して
@hashed_password
に保存され、元のパスワードは保存されません。 -
アクセス制御:
-
public メソッド:ユーザーが使用する必要があるメソッド(
authenticate
,login
,logout
など) -
private メソッド:内部実装の詳細(
set_password
,hash_password
,valid_email?
など)
-
public メソッド:ユーザーが使用する必要があるメソッド(
-
データの検証:メールアドレスとパスワードは設定前に検証され、無効な値は例外を発生させます。
2. UserManager クラス
- ユーザーの登録、取得、ログインなどの機能を提供します。
- 複数のユーザーを管理し、ユーザー名の重複チェックを行います。
カプセル化の利点
このシステムでは、以下のようにカプセル化の利点が活かされています:
-
セキュリティの向上:パスワードは外部から直接アクセスできず、ハッシュ化して保存されています。さらに、ソルトを使用して同じパスワードでも異なるハッシュになるようにしています。
-
データの整合性:メールアドレスやパスワードは設定前に検証されるため、無効なデータが保存されることはありません。
-
実装の詳細の隠蔽:パスワードのハッシュ化やソルトの生成などの実装詳細は、privateメソッドとして隠蔽されています。
これにより、安全で使いやすいユーザー認証システムが実現されています。ユーザーはパスワードを直接参照することはできませんが、authenticate
メソッドを通じて認証を行うことができます。また、パスワードの変更も、古いパスワードを正しく入力した場合にのみ許可されます。
主要なカプセル化ポイント
-
パスワードの隠蔽:
@hashed_password
と@salt
はprivateで、外部からアクセスできません。 -
検証付きセッター: メールアドレスの変更時に
valid_email?
メソッドで検証を行います。 -
ユーザー名の一意性:
UserManager
クラスがユーザー名の重複を防ぎます。 -
認証プロセス: パスワードの検証は
authenticate
メソッドを通じてのみ行われ、内部実装の詳細は隠蔽されています。
実際のアプリケーションでの拡張ポイント
実際のRailsアプリケーションでこのようなユーザー認証システムを実装する場合、以下のような拡張を検討するとよいでしょう:
-
パスワードのさらなる強化:
- BCryptなどのより強力なハッシュアルゴリズムの使用
- パスワード強度のより詳細なチェック(大文字、小文字、数字、記号の組み合わせなど)
-
アカウント管理機能:
- パスワードリセット機能
- アカウントロック機能(複数回ログイン失敗時)
- メール認証機能
-
セッション管理:
- トークンベースの認証
- 「ログイン状態を保持する」機能
-
権限管理:
- ロールベースのアクセス制御(管理者、一般ユーザーなど)
- 特定の操作に対する権限チェック
実際のRailsアプリケーションでは、devise
やsorcery
などの認証ジェムを使用することで、これらの機能を簡単に実装できます。しかし、基本的な考え方はこの例で示したカプセル化の原則に基づいています。
終わりに
ここまでお読み頂きありがとうございました!!それでは4章の記事でまたお会いしましょう!
追記(2025年月4月17日)
本記事の4章目となる継承とクラスの拡張についての記事を執筆しました!ぜひお読みくださいませ!!
Discussion