Jes Jes - 25 days ago 10
Ruby Question

Ruby - metaprogramming, define_method, DRYing code

I created project, in which I have many classes, and every class has a similiar structure:

module Project
class ExampleClass
attr_accessor :title, :body, :elements_1, :elements_2

def initialize(attributes = {})
self.title = attributes[:title]
self.body = attributes[:body]
self.elements1 = attributes[:elements1] || []
self.elements2 = attributes[:elements2] || []
end

def ==(other)
title == other.title && body == other.body && elements1 == other.elements1 && elements2 == other.elements2
end
end
end


Only differences between classes are its name and names of
elements1
and
elements2
arrays.
My mentor gave me code to DRY my project:

module Project
class Node
extend ActiveModel::Naming
include ActiveModel::Model

attr_accessor :body, :title
cattr_accessor :element_names
self.element_names = []

def element
element_names.reduce([]) do |name, memo|
memo + send(name)
end
end

def ==(other)
body == other.body && title == other.title && element_names.all? { |name| element_by_name(name) == other.element_by_name(name) }
end

def element_by_name(name)
instance_variable_get("@#{name}") || instance_variable_set("@#{name}", [])
end

module ClassMethods
def element(name)
elements_names << name

attr_writer name
define_method(name) do
element_by_name(name)
end
end
end
extend ClassMethods
end
end


It's quite above my level, I'm trying to make it work - every class should inherit after
Node
. I must somehow pass attributes -
elements1
and
elements2
. I was experimenting with

class ExampleClass < Node
cattr_accessor :element_names
self.element_names = [:elements1, elements2]
end


in my
ExampleClass
to pass names of arrays.

I also tried with
initialize
, but I can't make it work. I'll be grateful for any help.

Max Max
Answer

The element class method is used to define new elements.

You want something like

class ExampleClass < Node
  element :elements1
  element :elements2
end

Though there is a serious issue with the code your mentor gave you: the element names are stored in a class variable, which means they will be shared among all subclasses of Node. What you want are class instance variables.

Replace this

cattr_accessor :element_names
self.element_names = []

with this

def self.element_names
  @element_names ||= []
end

that way each subclass stores its own array of element names (and then as a bonus you no longer need any of that ActiveModel cruft).

Comments