📜 ⬆️ ⬇️

Ruby Design Patterns

Zen Ruby tells us that you can accomplish a task in several ways, so the solutions given here are just a small subset of options for how to solve the problem more “beautifully”. Almost everywhere, where I read about patterns, some artificial examples were cited, I always wanted someone to show me “how to” on the already written, poorly designed code.
So, today we consider two design patterns: an abstract factory and a template method.

From far away ... About Ruby and Enum
Imagine that you were put on a project and you are trying to read someone else's code. We see the line:
LogRec.create(uid: task[:tid], lrtype: 'tsk', rc_time: rc_time, start: task[:start] ) 

Read the code further:
  LogRec.create(uid: task[:tid], lrtype: 'task', rc_time: rc_time, start: task[:start] ) 

Strange, there 'tks', here 'task'. Ok, we looked at the documentation, in the migration (somewhere else), fixed it. We read further, again a mistake ...
Conclusion: Always try to make string constants somewhere, in this case it is better to use Enums. In Ruby Enum, it looks like this:
 module ProtocolElementTypeName TASK = 'task' NOTE = 'note' EVENT = 'event' end 

Then all of your controllers and models will use a constant (ProtocolElementTypeName :: TASK) and no typos!

Almost "Factory"
We read the code further and see:
 log = Array.new log_recs.each do |log_rec| case log_rec.lrtype when 'qc' log.push({uid: log_rec.uid, type: log_rec.lrtype, parid: log_rec.parid, start: log_rec.start.gsub(" ", "T"), ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), state: log_rec.state, rcpt: log_rec.recipient}) when 'tstate', 'etsks' log.push({type: log_rec.lrtype, parid: log_rec.parid, start: log_rec.start.gsub(" ", "T"), end: log_rec.end, ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), state: log_rec.state, rcpt: log_rec.recipient}) when 'pcb', 'grps' log.push({type: log_rec.lrtype, ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), rcpt: log_rec.recipient}) when 'egrp', 'tgrp' log.push({type: log_rec.lrtype, parid: log_rec.parid, ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), state: log_rec.state, rcpt: log_rec.recipient}) when 'rprefs' log.push({type: log_rec.lrtype, parid: log_rec.parid, ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), rcpt: log_rec.recipient}) else log.push({uid: log_rec.uid, type: log_rec.lrtype, parid: log_rec.parid, start: log_rec.start.gsub(" ", "T"), end: log_rec.end, ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), state: log_rec.state, rcpt: log_rec.recipient}) end 

Here every programmer who writes in OO language should become very, very sad. If you see a large train of if-s or such a switch, you should shout: “Help! Hooligans deprived of view !!! "
How to solve this problem? Use the "Factory" template. The meaning of this template is that it provides a convenient interface for creating an object of the desired type. So, what we see from the code: in the table there are many fields from which objects of different types can be "completed". The type of the object will depend on what is written in the lrtype field.
To begin with, we will create a module, where we list all possible values ​​of the lrtype field:
 module LogRecObjType #   obj_type   LogRec EVENT = 'event' QC = 'qv' TASK = 'task' end 

Then, you need to create a Hash, which is necessary in order to create an object of a certain type by the value of lrtype:
  @@types_objects = { ObjectJournal::QC => :complect_qc, ObjectJournal::EVENT => :complect_event, ObjectJournal::TASK => :complect_task } 

And implement the functions that are responsible for staffing the object:
 def complect_qc obj = { :uid => self.id, :type => self.lrtype, :parid => self.parid, :start => self.start, :ts => self.ts, :rc_time => self.rc_time, :state => self.state, :rcpt=> self.recipient } return obj end #   ... 

Now we will write a function that will be responsible for creating an object of the required type by the value of lrtype.
  def complect_object_journal if @@types_objects.has_key?(self.lrtype) return send(@@types_objects[self.lrtype]) else return complect_another end end 

In fact - everything! Now let's rewrite that horrible switch:
 log = log_recs.map { |x| x.complect_object_journal } 


Template Method
Consider this situation: there is a certain base class Operation, which has two heirs - Goal and Task. All three classes have some similar functionality, they can form a certain complex object:
  class Operation def return_operation operation = { :goal => {:id => goal.gid, :title => goal.title, :ts => goal.ts}, :task => {:is_problem => task.is_problem, :state => task.state,:author => task.author_id} } return operation end end class Event < Operation def return_operation operation = { :goal => {:id => goal.gid, :title => goal.title, :ts => goal.ts, :author => goal..author_id, :holder => complect_goal_content_header}, :task => {:is_problem => task.is_problem, :state => task.state,:author => task.author_id} } return operation end end class Task < Operation def return_operation operation = { :goal => {:id => goal.gid, :title => goal.title, :ts => goal.ts}, :task => {:id => task.gid, :title => task.title, ts => task.ts, :author => task..author_id, :holder => complect_holder} } } return operation end end 

Here again we see code duplication (for example, we look at the goal key in Task and Operation). The functionality of the return_operation method “mutates” in each hierarchy class, but the keys (goal and task) always remain the same. To resolve this kind of situations, the Template Method pattern is best suited. The meaning of the template is that it makes it possible to determine the basis of the algorithm, allowing the heirs to override certain steps of the algorithm without changing its structure as a whole. In our case, the template implementation will look something like this:
 class Operation def return_operation operation = { :goal => complect_goal, :task => complect_task } return operation end def complect_goal goal_obj = { :id => goal.gid, :title => goal.title, :ts => goal.ts, } return goal_obj end def complect_task #task = self.task task_obj = { :is_problem =>task.is_problem, :state => task.state, :author => task.author_id } return task_obj end end class Event < Operation def complect_goal goal_obj = { :id => goal.gid, :title => goal.title, :ts => goal.ts, :author => goal.author_id, :holder => complect_goal_content_header } return goal_obj end end class Task < Operation def complect_task task_obj = { :id => task.gid, :title => task.title, :ts => task.ts, :author => task.author_id, :holder => complect_holder } return task_obj end end 

')
I would be very happy comments and suggestions. Thank !

Source: https://habr.com/ru/post/261439/


All Articles