Building a chain of responsibility in Ruby to apply transformators on an object I try to make a middleware stack system in the rack way (but not for HTTP request).

Here is the main class: 

class MiddlewareStack

  def self.stack middleware
    @middlewares ||= []
    @middlewares &lt;&lt; middleware
  end

  def self.middlewares
    @middlewares
  end

  def self.next payload
    next_middleware = @middlewares.shift
    next_middleware.apply(payload, self)
  end

  def self.apply payload
    self.next payload
  end

end


Usage

We can setup a middleware stack this way: 

class PlusOneMiddleware
  def self.apply payload, stack
    stack.next(payload) + 1
  end
end

class DummyMiddleware
  def self.apply payload, stack
    return payload
  end
end

class PlusOneStack &lt; MiddlewareStack
  stack ::PlusOneMiddleware
  stack ::DummyMiddleware
end


Now we can call the stack like this:

PlusOneStack.apply(1) #return 2


This use case is pretty useless, but I plan to use it for filtering and caching purposes.

Questions


What do you think about the naming/the code?
Do have a better implementation in mind?
What do you think about the test suite? (above)


Test suite

describe MiddlewareStack do

  context "when we stack middlewares" do
    it "contains all the middlewares in the right order" do
      expect(DummyMiddlewareStack.middlewares).to eq [PlusOneMiddleware, DummyMiddleware]
    end
  end

  context "when we apply the middleware stack" do
    it "call all the middlewares" do
      expect(PlusOneMiddleware).to receive(:apply).ordered.and_call_original
      expect(DummyMiddleware).to receive(:apply).ordered.and_call_original
      PlusOneStack.apply(1)
    end
  end

end
I'm concerned by the interplay between the two middlewares. Why is PlusOneMiddleware.apply responsible for calling the next middleware?

It breaks the sequence implicit in the stack being a stack. In effect, the payload passes through DummyMiddleware before PlusOneMiddleware although they've been stacked in the opposite order.

It also requires PlusOneMiddleware to know that there is a "next" middleware. But why should it know or care?

I'd expect the stack to be responsible to for calling the middlewares in sequence, and the middlewares being ignorant of each other. E.g.

class MiddlewareStack
  def self.apply payload
    middlewares.reduce(payload) do |payload, middleware|
      middleware.apply(payload, self)
    end
  end
end


Your tests are misleading too. You have a spec that says


  MiddlewareStack: When we apply the middleware stack it call [sic] all the middlewares


But "it" - the middleware stack - doesn't actually do that. It only happens because the PlusOneMiddleware calls .next on the stack. Which, again, shouldn't be its responsibility. If you reverse the order of middlewares, only one gets called. If you only add the PlusOneMiddleware you get NoMethodError: undefined method ‘apply’ for nil:NilClass because @middlewares.shift returns nil.

In other words, you specs will break if you do this:

class PlusOneStack &lt; MiddlewareStack
  stack ::DummyMiddleware
  stack ::PlusOneMiddleware # doesn't get called
end


or this:

class PlusOneStack &lt; MiddlewareStack
  stack ::PlusOneMiddleware # raises NoMethodError on nil
end


which sort of negates the entire idea of a flexible stack of arbitrary middlewares.

Similarly, I should be able to do this:

class PlusOneStack &lt; MiddlewareStack
  stack ::PlusOneMiddleware
  stack ::DummyMiddleware
  stack ::AnotherMiddleware
end


But AnotherMiddleware will never be called, which doesn't really make sense: It's in the stack, right?

There's also the use of shift in MiddleWareStack. Especially as the stack's not an instance, it's strange to see the @middlewares array being manipulated when the stack is "run". Doubly so since .next can be called from the middlewares themselves.



Other small stuff:


You set a default value for @middlewares in .stack, but you should probably be doing that in your .middlewares accessor method. And you should then use said accessor in your other methods. For instance, if you never add any middleware at all, .apply will fail, since @middlewares is nil.
However, you may not want to expose a @middlewares accessor to the world, as anyone can then mess with it. So beware.
Take a cue from Rack, and name the main middleware method call. That way, you can use plain procs and lambdas as middleware in addition to custom classes and objects. And you may want to check for that with respond_to? when stacking middlewares.


A brutally simple implementation might simply look like:

class MiddlewareStack
  class &lt;&lt; self
    def use middleware # same method name as Rack
      raise ArgumentError unless middleware.respond_to?(:call)
      stack &lt;&lt; middleware
    end

    def apply payload
      stack.reduce(payload) do |payload, middleware|
        middleware.call payload
      end
    end

    private

    def stack
      @stack ||= []
    end
  end
end




Edit: After the reading the comments, something like this might be a more fitting solution. Where a stack/middleware construction would require an agreed-upon API between stack and middlewares, this is more of a linked-list construction, where there no central "stack", and the agreed-upon API is instead between the members of the linked-list. Note that this is just a quickly-written example

class ExampleLinkedHandler
  def initialize(name)
    @name = name
  end

  def append(handler)
    if @next
      @next.append(handler)
    else
      @next = handler
    end
  end

  def call(level = 0)
    if @next
      "#{@name} (level #{level}) &lt;- #{@next.call(level + 1)}"
    else
      "#{@name} (level #{level})"
    end
  end
end

chain = ExampleLinkedHandler.new("head")

chain.append(ExampleLinkedHandler.new("1st"))
chain.append(ExampleLinkedHandler.new("2nd"))
chain.append(ExampleLinkedHandler.new("tail"))

puts chain.call(0) # prints "head (level 0) &lt;- 1st (level 1) &lt;- 2nd (level 2) &lt;- tail (level 3)"


You can of course have any kind of derived or duck-typed classes/objects as longs as they respond to append and call in a similar manner.