Header
2020-10-02
2020-10-14

Rubyで設計を学ぶ|単一責任の原則を解説

2020 10 02 3250120 s min

今回は設計を学ぶために、Rubyで単一責任の原則を解説していきます。

なお、以下の書籍を参考にしております。

単一責任の原則とは?

オブジェクト指向を用いた設計原則で用いられます。

端的にいうと、1つのクラスには、1つの役割を持たせましょうという原則です。

※メソッドやモジュールなど、全てに適用される

1つのクラスやモジュールに複数の役割を持たせると、

・変更に脆くなる

・密結合が発生し、バグが発生しやすくなる

と言った弊害が発生します。

では、以下からサンプルコードを利用して解説します。

サンプルコードの処理内容

■コードで実行する事■

・前年度の成績から毎月の給与を算出

■給与計算式■

年齢*前年度結果*役職

コードの動作は大まかには以下です

コードの動作
  • 1:社員NOを入力し取得
  • 2:年齢・前年度結果・役職を取得
  • 3役職を取得し、director なら3それ以外は1を設定
  • 3:前年度結果から前年度結果が1以上ならその値、1未満なら0.8を設定
  • 4:年齢を設定
  • 5:取得した値を掛け算する

なおサンプルコードでは、エラーキャッチは考えないのと 別途説明が必要なので、 Struct(構造体クラス) は利用しません。

詳細な動作よりは、各クラスとメソッドの内容に注目してください

リファクタリング前のコード

class Salary
  attr_reader :employee_id

  def initialize(employee_id)
    @employee_id = employee_id
  end


  def self.get_employee(employee_id)
    employees = [{id: "1", performance: "1.2", age: "30", role:"director"},
                 {id: "2", performance: "0.2", age: "40", role:"employee"},
                 {id: "3", performance: "0.4", age: "40", role:"director"}]

     employees.find { |x| x[:id].include?(employee_id) }
  end

  def self.get_role(role)
    if role[:role] == "director"
      3
    else
      1
    end
  end


  def self.get_performance(performance)

    if performance[:performance].to_f  > 1
       performance[:performance]
    else
       "0.8"
    end

  end

  def self.get_age(results)
    results[:age].to_i
  end

  def calculation
    results = self.class.get_employee(employee_id)
    grade = self.class.get_role(results)
    salary_results = self.class.get_performance(results).to_f
    age = self.class.get_age(results)

    (grade * salary_results * age).floor
  end
end

puts Salary.new("3").calculation

このSalaryクラスの中に

Salaryクラスの動作
  • 1:従業員を取得するget_employeeメソッド
  • 2:役職を取得するget_roleメソッド
  • 3:前年度の成績を取得get_performanceメソッド
  • 4:年齢を取得するget_ageメソッド
  • 5:全ての要素を計算するcalculationメソッド

があります。

会社員経験がある方ならわかると思いますが、

給与計算などは複雑な要素が絡み、計算式も数年ごとに変わります。

例えば、この給与計算ロジックへ

・在籍年次

・所属部署

・役職により、算出ロジックが変わる

など、給与の算出項目が増加・変化すると

Salaryクラスはどんどん肥大していきます。

これではSalaryクラスの中に給与計算ロジックがあり、クラスの名前と動作も分かりにくい印象もあります。

さらに、各項目絡み合ってきますので、メソッドが増加すると メソッド間が密結合を起こし、

・改修や機能追加をしにくくなる

・バグの温床になる

と言った弊害が発生します。

この状態を回避するために、オブジェクト指向を用いた設計原則の単一原則を適用します。

リファクタリング後のコード

class EmployeResultFinder
  def self.find(employee_id)
    employee_results = [
        {id: "1", employee_id: 1, performance: "1.2", age: "30", role: "director"},
        {id: "2", employee_id: 2, performance: "0.2", age: "40", role: "employee"},
        {id: "3", employee_id: 3, performance: "0.4", age: "40", role: "director"}
    ]

    employee_results.find do |x|
      x[:employee_id] === employee_id
    end
  end
end

class Employee
  attr_reader :employee_id, :age, :role, :performance

  def initialize(employee_id, age, role, performance)
    @employee_id = employee_id
    @age = age
    @role = role
    @performance = performance
  end

end

class Role
  attr_reader :role

  def initialize(role)
    @role = role
  end

#   def director?
#     @role === 'director'
#   end

#   def employee?
#     @role === 'employee'
#   end
end

class PointEvaluator
  attr_reader :role

  def evaluate_by(role)
    table[:"#{role}"]
  end

  def table
    {
        'director': 3,
        'employee': 1,
    }
  end
end

class Performance
  attr_reader :result

  def initialize(performance)
    @performance = performance

  end

  def get_performance
    if @performance.to_i > 1
      @performance
    else
      @performance = 0.8
    end
  end
end

class MonthlySalaryCalculation
  attr_reader :age, :grade, :role

  def initialize(age, grade, role)
    @age = age
    @grade = grade
    @role = role
    @pointEvaluator = PointEvaluator.new
  end

  def calculate
    (@age.to_i * @grade * @pointEvaluator.evaluate_by(role).to_i).floor
  end
end

employee_id = 3
employee_result = EmployeResultFinder.find(employee_id)

employee = Employee.new(employee_result[:employee_id], employee_result[:age], employee_result[:role], employee_result[:performance])
role = Role.new(employee.role)
performance = Performance.new(employee.performance)

calculation = MonthlySalaryCalculation.new(employee.age, performance.get_performance, role.role)
puts calculation.calculate

単一責任の原則は1つのクラスには、1つの役割を持たせましょうという原則です。

そのため、

Salaryクラスの中で全ての処理を行わず、処理毎にクラスを別々に分ける事を行いました。

各クラスの動作
  • 1:従業員の情報を取得するEmployeResultFinderクラス
  • 2:従業員のid、年齢、役職、成績を取得するEmployeeクラス
  • 3:従業員の役職を取得するRoleクラス
  • 4:役職から計算、処理するPointEvaluatorクラス
  • 5:成績から計算、処理するPerformanceクラス
  • 6:全ての項目を計算するMonthlySalaryCalculationクラス

という形で6クラスに分けています

例えば役職によって、給与項目が変わるなら、 Roleクラスで、directorかemployeeかを判断するメソッドを追加すればいいですし、

役職によって、算出項目が変わるなら、PointEvaluatorのtableへ項目を追加すればいいだけです。

こうして、各クラスを分けて、処理をより抽象化すれば、 各処理の密結合を防げ

可読性が高く、変更に強いコードを設計できます。

以上になります。

関連記事

paizaのスキルチェックの難易度|各ランクの特徴・ランクアップのコツ

Rubyの引数で使えるテクニック|キーワード引数

初心者・独学者向け|Rubyのハッシュ入門とよく使うメソッド

エンジニアの3つの転職方法|現役エンジニアが解説

初心者・独学者向け|Rubyの配列処理の入門とよく使うメソッド

初心者・独学者向け|Rubyのinitializeメソッドとは

初心者・独学者向け|RubyのStringクラスとよく使うメソッド

Rubyの繰り返し処理の解説|初心者・独学者向け

paiza(パイザ)Cランク練習問題|5以上の整数の合計のRubyサンプルコードと解説

あなたにお勧めの記事
前の記事
次の記事