0
0

More than 1 year has passed since last update.

オブジェクト指向プログラミングを意識と例外処理を使った電卓機能を作ってみました!:コード説明あり

Last updated at Posted at 2023-04-20

オブジェクト指向を学習して理解を深めたい部分を共有します

今回は、題名にもある通りオブジェクト指向プログラミングを意識した電卓機能アプリを作ってみました!

とても小さなアプリですが、オブジェクト指向とデメテルの法則を意識して作ったのでアウトプットしていきます!

最初にこの記事をご覧になるとどんなことがわかるのかを紹介します

  1. 単一責任のクラス
  2. デメテルの法則
  3. 柔軟なインターフェイス
  4. 他クラスからのメソッドを使用する際に依存関係を最小限に抑えることができる
  5. カスタムエラーの作り方と例外処理のやり方

これらを意識してコードを書きました!

参考にした本を紹介

デメテルの法則について軽く説明

オブジェクトを疎結合にするためのコーディング規則の集まり。
オブジェクト指向設計において、
オブジェクトが他のオブジェクトとどのようにコミュニケーションするかに関連する原則。
「直接の隣人にのみ話しかけよう」なんて言い方もされます。

オブジェクトが他のオブジェクトの内部構造に深く依存しないようにすることが目的である

デメテルの法則に反している例とすると
バケツリレーみたいにどんどんと情報を流れるのは良くない
ドットが2個以上になることですね。

どのぐらいファイル分割してクラス分けしたのか

全部で6ファイルあります。電卓ができる機能だけなのに6ファイル!

  1. main.rb(メインの実行ファイル)
  2. calculator.rb(計算機で計算方法をそれぞれのメソッド定義したファイル)
  3. calculation.rb(計算方法を決めるファイル)
  4. input_column.rb(実際にユーザー入力方法を記述したファイル)
  5. computation_run.rb(計算を実行するファイル)
  6. custom_error.rb(カスタムエラー専用のファイル)

だいぶ小分けしましたですがその分依存関係も浅く再利用性のあるコードが書けたと思います!

今からファイル別にコード説明と何故そうしたかを書きます

main.rb

main.rb
require './calculation'
require './custom_error'
require './input_column'
require './computation_run'

ComputationRun.execute

外部ファイルを呼び込んでます。
小分けする前は120行近くあったと思います。
今は6行です。
ComputationRun.executeこの1行に全てがつまってます!

calculator.rb

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

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

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

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とほぼ一緒です
格納されたinputgets.chomp.to_fgets.chompに変更(今回は文字列が入るから!)

バリデーションチェックの内容も変わっており

inputで渡ってきた値が'+', '-', '*', '/'いずれかであれば
trueを返すので正常であり中断する為ループから抜け出します

それ以外はループから抜け出せず、NoMatchOperatorErrorが出る

computation_run.rb

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が始まりますのでちょっと変な感じになってしまいます

1.png

ちなみに例外処理とは???
予期しないエラーが起きた際に、適切な対処を行う仕組みです。
今回の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を押すと辞めれて他のボタンを押すと続けてできます'にすればいいのか!!

プライベートなインターフェイス

computation_run.rb
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乗も計算できる機能を作るとして他のクラスからアクセスできないので
変更による副作用かなり少ないと思います。

それに再利用する可能性も低いですから、プライベートなクラスメソッドにしました。

その他利点

  1. この様にプライベートと可視化することで将来、別の開発者一眼でわかる様になる
  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行も同じ様な感じです。

どんなのができたのか?

こんなのができました!
スクリーンショット 2023-04-20 17.15.14(2).png

スクリーンショット 2023-04-20 17.16.45(2).png

最後に

クラス外でインスタンス変数は使うな!!!」 と書籍に書いてありましたのでその様にしたら
こんなに分割できてしまいまいした。

個人的には1ファイル1ファイルのコード量は少ない方が好きなのでオブジェクト指向プログラミングは向いてるのかもしれません!

オブジェクト指向設計勉強初めて6日目ですがこれからも意識してコード書いていきます!

初日に比べてスラスラかける様になってきたので控えめに言って楽しいです。

長くなってしまいましたがここまで見てくださった方有難うございました!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0