世界が幸せで在ります様に

ITエンジニアになりたい人・エンジニアの人にとって役立ちそうな商品を紹介するブログ

Rubyでクラスマクロを作ってみよう(Class Macro with Ruby)

f:id:sinsinchang:20180717021515j:plain

例えば、POROでattr_accessorを定義したときに、便利メソッドを同時に定義したかったとする。

そういうサンプルを紹介してみた。

 

 

今回は生徒の点数を記録するようなクラスをサンプルとしてみる

作成クラス

  • ElementUtility・・・汎用的な処理を記述するモジュール
  • StudentRecord・・・生徒の成績クラス

 

サンプルソース(Module)

module ElementUtility
  def self.included(klass)
    klass.extend ClassMethods
  end

  module ClassMethods
    def attrs_with_methods(*args)
      attr_accessor *args

      define_method :initialize do |*things|
        data = things.first || {}
        args.each do |attribute|
          instance_variable_set "@#{attribute}", data[attribute]
        end
      end

      args.each do |attribute|
        define_method "average_#{attribute}" do
          send(attribute).inject(0, :+) / send(attribute).size if send(attribute).is_a?(Array)
        end
      end
    end
  end
end

 

このような形でまずはモジュールを作り、includeしたクラスから呼び出せるような構造にする。

※RailsであればActiveSupport::Concernを用いてやるのだが、POROなのでちょっと面倒くさい書き方になっている。

includedのClassMethodsには以下の処理をやらせる。

  • initializeで初期化メソッドを定義
  • average_要素名で生徒のテストを教科毎に平均値を取得できるメソッドを定義

※注意点:average~のようにdefine_methodで処理を追加すると、普通にdefで指定するよりパフォーマンスは落ちるので、使用するときはちゃんとそのあたりも考慮したほうがよい。

サンプルソース(Class)

class StudentRecord
  include ElementUtility

  POINTS = %w[japanese english mathematics science society].freeze

  def fullname
    "#{firstname} #{lastname}"
  end

  def fullaverage
    POINTS.each_with_object({}) { |l, r| r[l] = send("average_#{l}") }
  end

  attrs_with_methods :no, :firstname, :lastname,
    :japanese, :english, :mathematics, :science, :society
end

今回は、仮に生徒の成績クラスで表現してみる。

  • fullnameメソッドは文字通り生徒の氏名を返す
  • fullaverageメソッドは各教科テストの平均点をhashにして返す

先程モジュールで定義したattrs_with_methodsの引数に各カラムを指定

それぞれは、以下になる。

  • no:integer: 出席番号
  • firstname:string: 名
  • lastname:string: 姓
  • japanese:Array(integer): 国語の成績
  • english:Array(integer): 英語の成績
  • mathematics:Array(integer): 数学の成績
  • science:Array(integer): 理科の成績
  • society:Array(integer): 社会の成績

さらに、配列の場合はaverage_要素名と指定することで平均値を返してくれるようにしておいた。これは、先程定義したもので実現可能。

サンプルソース(利用例)

少し雑だが、実際に使ってみると...

point = [*0..100]
random_test = -> { [*1..10].map { point.sample }}
[%w[ken yamada], %w[hana tanaka]].each.with_index(1) do |(f, l), i|
  sr = StudentRecord.new(
    no: i, firstname: f, lastname: l,
    japanese: random_test.call, english: random_test.call,
    mathematics: random_test.call, science: random_test.call,
    society: random_test.call
  )
  puts sr.fullname
  puts sr.fullaverage
end

以下のような出力になるだろう

ただ、点数はpoint.sampleでとっているので毎回違う値になると思う。

ken yamada
japanese's average: 72points
english's average: 46points
mathematics's average: 58points
science's average: 39points
society's average: 38points

hana tanaka
japanese's average: 39points
english's average: 46points
mathematics's average: 51points
science's average: 36points
society's average: 63points

 

今回は、attr_accessorで定義する要素に共通したメソッドを定義させるやり方をメモしておいた。

実際に、Railsでattr_accessorでカラムを指定して、何らかの処理をさせたいときにも使えると思う。

 メタプロに関しては、以下の本が参考になるよ。

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

 

 

プロフィールと免責事項