オブジェクト指向を学習して理解を深めたい部分を共有します
今回は、題名にもある通りオブジェクト指向プログラミングを意識した電卓機能アプリを作ってみました!
とても小さなアプリですが、オブジェクト指向とデメテルの法則を意識して作ったのでアウトプットしていきます!
最初にこの記事をご覧になるとどんなことがわかるのかを紹介します
- 単一責任のクラス
- デメテルの法則
- 柔軟なインターフェイス
- 他クラスからのメソッドを使用する際に依存関係を最小限に抑えることができる
- カスタムエラーの作り方と例外処理のやり方
これらを意識してコードを書きました!
参考にした本を紹介
デメテルの法則について軽く説明
オブジェクトを疎結合にするためのコーディング規則の集まり。
オブジェクト指向設計において、
オブジェクトが他のオブジェクトとどのようにコミュニケーションするかに関連する原則。
「直接の隣人にのみ話しかけよう」なんて言い方もされます。
オブジェクトが他のオブジェクトの内部構造に深く依存しないようにすることが目的である
デメテルの法則に反している例とすると
バケツリレーみたいにどんどんと情報を流れるのは良くない
ドットが2個以上になることですね。
どのぐらいファイル分割してクラス分けしたのか
全部で6ファイルあります。電卓ができる機能だけなのに6ファイル!
- main.rb(メインの実行ファイル)
- calculator.rb(計算機で計算方法をそれぞれのメソッド定義したファイル)
- calculation.rb(計算方法を決めるファイル)
- input_column.rb(実際にユーザー入力方法を記述したファイル)
- computation_run.rb(計算を実行するファイル)
- custom_error.rb(カスタムエラー専用のファイル)
だいぶ小分けしましたですがその分依存関係も浅く再利用性のあるコードが書けたと思います!
今からファイル別にコード説明と何故そうしたかを書きます
main.rb
require './calculation'
require './custom_error'
require './input_column'
require './computation_run'
ComputationRun.execute
外部ファイルを呼び込んでます。
小分けする前は120行近くあったと思います。
今は6行です。
ComputationRun.execute
この1行に全てがつまってます!
calculator.rb
# 計算機クラスで計算方法をそれぞれのメソッドに入れてます
class Calculator
def initialize(num1:, num2:)
@num1 = num1
@num2 = num2
end
def add
@num1 + @num2
end
def subtract
@num1 - @num2
end
def multiply
@num1 * @num2
end
def divide
raise ZeroDivisionError, 'ゼロによる割り算は許可されていません' if @num2.zero?
@num1 / @num2
end
end
ここは四則演算にて計算方法を作ってます。
いたってシンプルですが、割り算の部分だけ特定の入力をしたらエラーが出る様にしてます。
後々説明しますが、ここで例外処理の一部ガード節を使って、簡潔に書いています。
後半の数値が0だったら、Infinityとして割り切れないのでエラーを出しましょうということですね。
ガード節とは?
処理の対象外とする条件を、関数やループの先頭に集めて return や continue/break で抜ける方法です
3行かかるところが1行でかけます。
これらを踏まえて説明すると、もしnum2が0であればコードを中断させ
例外のZeroDivisionError
を発生しエラーメッセージのゼロによる割り算は許可されていません。 と処理されます。
calculation.rb
require './calculator'
# 計算方法を決めるクラスです
class Calculation
def initialize(num1:, num2:, operator:)
@calculator = Calculator.new(num1: num1, num2: num2)
@operator = operator
end
def selection
case @operator
when '+'
@calculator.add
when '-'
@calculator.subtract
when '*'
@calculator.multiply
when '/'
@calculator.divide
end
end
end
require './calculator'
ここでcalculator.rbファイルを読み込んでます
def initialize(num1:, num2:, operator:)
@calculator = Calculator.new(num1: num1, num2: num2)
@operator = operator
end
次から
Calculator => 計算機クラス
Calculation => 計算方法を決めるクラス
とします。
initializeメソッド
は、計算方法を決めるクラスのインスタンスがnewされた時に実行され、3つのキーワード引数(num1:, num2:, operator:)を受け取ります。
initializeメソッド
内で計算機クラスのインスタンス生成して、それをインスタンス変数に格納しています
これにより、計算機クラスのnum1,num2を計算方法を決めるクラスに使用できる様になります。
@operator = operator
これはCalculationクラスの引数の一つで演算子を入力する引数です。
それをインスタンス変数に格納してます。
これにより、計算方法を決めるクラスのインスタンス内で演算子(+、-、*、/)を参照できます。
def selection
case @operator
when '+'
@calculator.add
when '-'
@calculator.subtract
when '*'
@calculator.multiply
when '/'
@calculator.divide
end
end
selectionメソッドを作り、@operator
に入った四則演算を割り当てそれぞれの計算方法を特定しています
例えば: @operator
の中に'+'が入れば計算機クラスのaddメソッドが使われます。
def add
@num1 + @num2
end
このメソッド↑ですね!
これにより足し算ができます!
custom_error.rb
class NoNumberError < StandardError
def initialize
super('num1とnum2は数値を入力してください、空欄も許可されていません')
end
end
class NoMatchOperatorError < StandardError
def initialize
super('演算子には +、-、*、/ のいずれかを使用してください')
end
end
カスタムエラーを作る際はStandardError
を継承しなければなりません。
initialize
メソッドのsuperエラーメッセージを渡せます。
自分でメソッド(例えば:message)作ってオーバーライドしputsで出力も可能
あまり意味ないですが。。。
def message
puts 'それは違うよ'
end
なんでZeroDivisionError
はないの???
ZeroDivisionError
はRubyが元々準備してくれてる
メソッドなので書く必要が無いです!
input_column.rb
# ユーザー入力欄クラスです。
class InputColumn
def self.get_input(description)
print description
gets.chomp
end
def self.get_number(description)
loop do
input = get_input(description)
return input.to_f if input.match(/\A-?\d+(\.\d+)?\z/)
raise NoNumberError
rescue NoNumberError => e
puts e
end
end
def self.get_operator(description)
loop do
input = get_input(description)
return input if ['+', '-', '*', '/'].include?(input)
raise NoMatchOperatorError
rescue NoMatchOperatorError => e
puts e
end
end
end
こちらのファイルはインスタンスを作成せずに使用するためのクラスです。
メソッドの先頭にself
とつける事でクラスメソッドになります。
クラスメソッドとインスタンスメソッドの違い
インスタンスメソッドはインスタンスを作成しなければ使用できませんが
クラスメソッドというのは直接クラスを
参照しインスタンスを作成しなくてもクラス内のメソッドが使えます。
クラスメソッドは、特定のインスタンスに依存せず、汎用的な処理を行う場合に使用されます。
def self.get_input(description)
print description
gets.chomp
end
get_inputクラスメソッドは引数にdescription
を受け取ります。
例えば、**get_input(説明が入るよ)**
とこの様にメソッドを読んであげると
print 説明が入るよ
gets.chomp
とこの様な感じで出力されます。
なぜputsじゃなくてprintなの???
puts
だと自動に改行されてしまい、print
だと改行されない状態で出力されます。
自分で確かめてみてください!!!
ユーザーにとって見やすいインターフェイスを心掛けましょう!
私の場合、下に入力を促すより横に入力を促した方が見やすいと思いました。
def self.get_number(description)
loop do
input = get_input(description)
return input.to_f if input.match(/\A-?\d+(\.\d+)?\z/) # ガード節のあとは空行を
raise NoNumberError
rescue NoNumberError => e
puts e
end
end
get_numberクラスメソッドは引数にdescriptionを受け取ります。
loop do
により条件が合わなければ振り出しに戻る、ループさせる様にしました
input = get_input(description)
これは先程のget_inputメソッドの戻り値(ユーザーが入力した値)をinput変数に格納しました。
return input.to_f if input.match(/\A-?\d+(\.\d+)?\z/)
ガード節を使用し、input.to_f
には実際入力されるものgets.chomp.to_f
が入り
input.match(/\A-?\d+(\.\d+)?\z/)
入力された物のバリデーションを行います。
/\A-?\d+(\.\d+)?\z/
は入力が整数または小数がマッチするかどうかをチェックする正規表現になります
要するにそれ以外の入力でマッチしないのはNoNumberError
が発生します。
raise NoNumberError
rescue NoNumberError => e
puts e
ここで出てきました例外処理です
今回の場合、自分でカスタムしたエラーを読んできています。カスタムエラーについては後ほど説明します。
get_operatorクラスメソッド
def self.get_operator(description)
loop do
input = get_input(description)
return input if ['+', '-', '*', '/'].include?(input)
raise NoMatchOperatorError
rescue NoMatchOperatorError => e
puts e
end
end
end
これはほぼ先程のget_numberとほぼ一緒です
格納されたinput
の gets.chomp.to_f
が gets.chomp
に変更(今回は文字列が入るから!)
バリデーションチェックの内容も変わっており
inputで渡ってきた値が'+', '-', '*', '/'いずれかであれば
trueを返すので正常であり中断する為ループから抜け出します
それ以外はループから抜け出せず、NoMatchOperatorErrorが出る
computation_run.rb
require './calculation'
# 計算を実行するクラスです
class ComputationRun
class << self
def result(num1, num2, operator)
calculation = Calculation.new(num1: num1, num2: num2, operator: operator)
result = calculation.selection
puts ''
puts "計算結果: #{num1}#{operator}#{num2} = #{result}"
end
def execute
loop do
num1, operator, num2 = user_input
begin
result(num1, num2, operator)
exit_prompt = prompt('続けますか?(Y/N)')
break if exit_prompt.upcase == 'N'
rescue ZeroDivisionError => e
puts e
end
end
end
private
def prompt(message)
print message
gets.chomp
end
def user_input
num1 = InputColumn.get_number('1番目の数値を入力してください: ')
operator = InputColumn.get_operator('演算子(+, -, *, /)を入力してください: ')
num2 = InputColumn.get_number('2番目の数値を入力してください: ')
[num1, operator, num2]
end
end
end
class << self
と囲ってあげる事によりメソッドの先頭にself
と付けなくてもクラスメソッドとなります。
全てクラスメソッドならこっちの方がいいかもしれません。
result
クラスメソッドについて
こちらのメソッドは計算結果を責務としています
ここでようやく、Calculationクラスのインスタンスを作成します。
名前が一緒でややこしくて申し訳ないのですがメソッド内の変数resultをcalculationクラスのselectionメソッドを
使用して計算結果を引き出しています。
execute
クラスメソッド
こちらのメソッドは、全てを果たします。
メイン実行部分です。
このメソッドは、ユーザーが入力する数値や演算子を受け取り、計算結果を表示し、
続行するかどうかを尋ねるループを実行します。
今回の電卓アプリの中枢となる部分です。
わかりやすく言いますと全てのメソッドが色々な経路で集結しています!
コード説明なのですが、見た感じそのまんまですね!
後ほど説明しますが、user_inputクラスメソッドの配列を受け取ってます。
begin
result(num1, num2, operator)
exit_prompt = prompt('続けますか?(Y/N)')
break if exit_prompt.upcase == 'N'
rescue ZeroDivisionError => e
puts e
end
先程のresultメソッドを受け取ってます。
例外処理について説明ます
begin
から始まり
rescue
でエラーを選択しています。
途中途中あったraise
はここで処理を実行しますという意味があります。
なぜZeroDivisionError
しかないと言いますと
例えば
def divide
raise ZeroDivisionError, 'ゼロによる割り算は許可されていません' if @num2.zero? # ガード節
@num1 / @num2
rescue ZeroDivisionError => e
puts e
end
この様に書いてしまうと、エラーメッセージが出てからloopが始まりますのでちょっと変な感じになってしまいます
ちなみに例外処理とは???
予期しないエラーが起きた際に、適切な対処を行う仕組みです。
今回のNoNumberErrorの場合、num1num2のどちらかに数値以外が入力されたとします
例外処理がないと、ユーザーにとって意味がわからないままプログラムが止まってしまいます。
この様な状態ですと、ユーザーには何が起こったのか理解できません。
なぜ止まったのかを教えるためにエラーメッセージを渡します。
このままでもいいのですが、今回私の場合失敗したらもう一回入力できる様にしました
exit_prompt = prompt('続けますか?(Y/N)')
break if exit_prompt.upcase == 'N'
これから紹介するメソッドにprompt
メソッドがあります
exit_prompt
変数にprompt
メソッドの引数にメッセージを格納し続けるか辞めるかの選択肢を促しています。
break if exit_prompt.upcase == 'N'
もしNを入力したらbreak
する、ループから抜け出すことができます。
upcaseメソッドを使うことで小文字のnでも大文字になる様にしました。
因みにY押すと次に進む様になっていますが、Y以外でも次に進めてしまいます。(どうしたものか)
それかメッセージを'Nを押すと辞めれて他のボタンを押すと続けてできます'にすればいいのか!!
プライベートなインターフェイス
private
def prompt(message)
print message
gets.chomp
end
def user_input
num1 = InputColumn.get_number('1番目の数値を入力してください: ')
operator = InputColumn.get_operator('演算子(+, -, *, /)を入力してください: ')
num2 = InputColumn.get_number('2番目の数値を入力してください: ')
[num1, operator, num2]
end
何故こちらをプライベートなクラスメソッドにしたのか自分なりに解釈し説明します。
共通点として、この2つのクラスメソッドは、ComputationRunクラス内でしか使われておりません。
prompt
メソッド
試しにプライベートなクラスメソッドにしました。
user_input
メソッド
万が一、今後2乗も計算できる機能を作るとして他のクラスからアクセスできないので
変更による副作用かなり少ないと思います。
それに再利用する可能性も低いですから、プライベートなクラスメソッドにしました。
その他利点
- この様にプライベートと可視化することで将来、別の開発者一眼でわかる様になる
- これから変更する場合、プライベートによりカプセル化されてるため、他のコードに影響を与えない
その結果、コードの保守性と可読性が向上します。
注意点!
メソッドに直接self
をかくとプライベートがうまく活用できません。
解決策は、class << selfで囲ってあげましょう!
そうすることで、上記でも説明したようにself
と書かなくてすみます
そして、privateの下にメソッドを書いてあげることで
プライベートクラスメソッドとして定義できます!
さてさてひとつ疑問が出てきました
みなさん覚えてますでしょうか?
InputColumnクラスのget_inputクラスメソッドはInputColumnクラスでしか使われておりません!
なぜプライベートにしないのか?
今後再利用できる可能性が大いにあるからです。
get_inputクラスメソッドはただ単に入力を受けるだけのメソッドです。
これから計算機だけでなく他の機能で使えそうです
他のクラスでも再利用する可能性があるため、プライベートにしない方がいいと私は思いました。
これがもしInputColumnクラス内だけに特化したメソッドで再利用することが考えにくい場合は
privateメソッドにした方が圧倒的良いでしょう。
本題に戻ります
prompt
メソッドはInputColumnクラスのget_inputメソッドにほぼ一緒なので省略します
user_input
メソッド
num1, operator, num2
これらを配列にしてexecuteメソッドで1行にしてかけるようにしました
InputColumn.get_number('1番目の数値を入力してください: ')
InputColumnクラスのget_numberクラスメソッドを使用していますここで
入力されたものを、バリデーションとエラーをチェックしています。
クラスメソッドなのでインスタンスを作成せずにクラスからメソッド直接呼び出すことができます。
loop do
print '1番目の数値を入力してください: '
input = gets.chomp
return input.to_f if input.match(/\A-?\d+(\.\d+)?\z/) # ガード節のあとは空行を
raise NoNumberError
rescue NoNumberError => e
puts e
end
end
この様な処理がInputColumn.get_number('1番目の数値を入力してください: ')
この1行で行われてます!
残り2行も同じ様な感じです。
どんなのができたのか?
最後に
「クラス外でインスタンス変数は使うな!!!」 と書籍に書いてありましたのでその様にしたら
こんなに分割できてしまいまいした。
個人的には1ファイル1ファイルのコード量は少ない方が好きなのでオブジェクト指向プログラミングは向いてるのかもしれません!
オブジェクト指向設計勉強初めて6日目ですがこれからも意識してコード書いていきます!
初日に比べてスラスラかける様になってきたので控えめに言って楽しいです。
長くなってしまいましたがここまで見てくださった方有難うございました!