Ruby Inox Part 3.1: Actions

2009
05.21

Here is a short update for the Ruby Inox Part3: Actions article. Some stuff worked out just fine, some stuff had to be changed to facilitate subclassing.

Behavioral changes

when an action is added to a class:

	class A
		include Actions

		action :clicked

the following methods are created: clicked=, on_clicked, clicked!, clicked each of these methods have a specific role.

clicked=

Assigns a Method, a Proc, or nil to the action. When action_assigned? than it executes the method or the block. If the action is nil action_assigned? returns false, and nothing is executed.

	clicked= lambda { do_something; }
	clicked= other_obj.method(sym)

on_click

This method is syntactic sugar for assign a Proc to the action. For a simple UI the MVC paradigm is simple overkill, and I want the code to be ass readable as possible. Si if I write a simple script that only displays a dialog with two buttons on it I don’t what to go through MVC.

	button.on_click { do_something; }

clicked!

This method has a bang to denote its importance and difference from clicked. This method is for subclasses to override and not meant to be called directly, I figured out that a subclass which wants to perform something when an action is called had to initialize the action with a block( complicating the action assignment for the user, as it should chain the actions ) or overriding the default clicked method which could lead to inconsistency and also complication with the original action.

Subclasses can safely override their subclassing-friendly action bang and do what they please without breaking any related code.

	class Component
		include Actions

		action :destroy
	end

	class Button < Component
		def destroy!
			# destroy the native button
			@qt_button.release
		end
	end

clicked

Carry out the clicked action. If the action is assigned calls the assigned method or block and then calls the override-friendly clicked!.

Inheritance cleaned

I had some trouble with inheritance, it is now definitely working better than the first couple of tries.

	class A
		include Actions
	end

	class B < A
		action :foo
	end

	# B.has_actions? => true

	class C < B
	end

	# B.has_action?(:foo) => true
	# C.has_action?(:foo) => true

At this state this behavior is enough. There is one todo left for the next update of the Actions module. Make it meta_programming friendly, changes to the parent class should reflect down the inheritance tree.

	B.action :baa
	C.has_action?(:baa) => false  # should be true

Current Code

Here is the source code for the Action module at the time of this writing:

	  module Actions
    def self.included(base)
      # add class methods
      with base do
        extend ClassMethods
      end

      # add instance methods
      with base do
        include InstanceMethods
      end
    end

    ##########
    # Class methods
    ##########
    module ClassMethods
      attr_accessor :actions

      def has_actions?; true; end
      def has_action?(sym); self.actions.include?(sym); end

      def actions(*args)
        return @actions ||= [] if args.empty?
        args.each { |a| self.define_action(a) }
      end

      def define_action(sym)
        assert2(self.actions).not.include?(sym).end

        self.actions << sym

        self.class_eval %{
          def #{"on_#{sym}".to_sym}(&block)
            self.action_assign("#{sym}".to_sym, block)
          end

          def #{sym}=(p)
            assert2(p).nil?.or.kind_of?(Proc).or.kind_of?(Method).end
            action_assign("#{sym}".to_sym, nil)
          end

          def #{sym}(*args, &block)
            s = "#{sym}".to_sym
            if self.has_action?(s) and self.action_assigned?(s)
              self.action_call("#{sym}".to_sym, *args, &block)
            end
            self.#{sym}!(*args,&block)
          end

          def #{sym}!(*args, &block)
          end
        }
        action_added(sym)
      end
      alias action define_action

      def remove_action(sym)
        res = actions.delete(sym)
        unless res.nil?
          with self do
            remove_method(sym)
            remove_method("#{sym}=".to_sym)
          end
          action_removed(sym)
        end
      end

      #
      # callbacks
      #
      def action_added(sym); end
      def action_removed(sym); end
      ##

      def inherited(subclass)
        if has_properties?
          subclass.properties = self.properties.clone
        end
        if has_actions?
          subclass.actions = self.actions.clone
        end
      end  

    end #Class Methods

    ##########
    # Instance methods
    ##########
    module InstanceMethods
      # returns a list of defined actions
      def actions; self.class.actions; end

      # set all the actions to call only the methods of one
      # delegate object
      # if obj if nil than resets all the actions to nil
      def actions=(obj)
        actions.each { |a| action_assign(a, obj.method(a)) }
      end

      # return the block/proc/method associated with the action
      def action(sym); @actions.nil? ? nil : @actions[sym] end

      # associates an action with a proc/block/method
      def action_assign(sym, proc)
        (@actions ||= Hash.new)[sym] = proc
        send! :action_assigned, sym, proc
      end

      # returns true or false whenever a block/proc/method is
      # assicated with the action
      def action_assigned?(sym); not action(sym).nil?; end

      # returns if this object has a given action
      def has_action?(sym); self.class.has_action?(sym); end

      # callback(s)
      def action_assigned(sym, block); end
      def action_unassigned(sym)
        raise "Action #{sym} not assigned"
      end
      def action_missing(sym)
        raise "Action #{sym} not defined"
      end

      protected

      # executes the action if assigned
      #
      # obj.actions[:foo].call
      def action_call(sym, *args)
        return action_missing(sym) unless has_action?(sym)
        action = self.action(sym)
        return action_unassigned(sym) if action.nil?
        return instance_exec(*args, &action)
      end

    end # Instance Methods

  end # Module Actions

Ruby Inox on GitHub

This file is part of the Ruby Inox project. Inox is a work in progress and this file might not be up to date. The status and latest files are available on GitHub

Related posts:
  1. Ruby Inox Part 3: Actions Here is the pursuit of a series of related articles...
  2. Ruby Inox Part 5: Component Something promised is something due. Only two days have passed...
  3. Ruby Inox Part 4.1: Property In Ruby Inox Part 4: Property I described I would...
  4. Ruby Inox Part 4: Property Last time, we took a look at Actions. A standard...
  5. Module structure Following the article Keyword With I present here how I...

Tags: , , , , , , ,

Your Reply


Ironic Wolf is Digg proof thanks to caching by WP Super Cache