Прокси-Проекта Ruby Коаны


Я только что закончил Рубин коаны и одним из последних проектов было создание прокси-класса, который передает методы в другой класс. Мне было интересно, хотите ли вы изменить что-нибудь о моем решении (если что-то неправильно или лучший способ сделать это). Я еще новичок и хочу сделать лучше.

require File.expand_path(File.dirname(__FILE__) + '/edgecase')

# Project: Create a Proxy Class
#
# In this assignment, create a proxy class (one is started for you
# below).  You should be able to initialize the proxy object with any
# object.  Any messages sent to the proxy object should be forwarded
# to the target object.  As each message is sent, the proxy should
# record the name of the method send.
#
# The proxy class is started for you.  You will need to add a method
# missing handler and any other supporting methods.  The specification
# of the Proxy class is given in the AboutProxyObjectProject koan.

class Proxy
  attr_reader(:messages)

  def method_missing(method_name, *args, &block)
    if @methods_to_pass.include?(method_name.to_s)
      @messages << method_name
      @object.__send__(method_name, *args, &block)
    else
      super(method_name, *args, &block)
    end
  end

  def called?(method_name)
    @messages.include?(method_name)
  end

  def number_of_times_called(method_name)
    @messages.count(method_name)
  end

  def initialize(target_object)
    @methods_to_pass, @messages = [], []
    @object = target_object
    @object.class.instance_methods(true).each { |method_name| @methods_to_pass << method_name; method_name }
  end

end

# The proxy object should pass the following Koan:
#
class AboutProxyObjectProject < EdgeCase::Koan
  def test_proxy_method_returns_wrapped_object
    # NOTE: The Television class is defined below
    tv = Proxy.new(Television.new)

    assert tv.instance_of?(Proxy)
  end

  def test_tv_methods_still_perform_their_function
    tv = Proxy.new(Television.new)

    tv.channel = 10
    tv.power

    assert_equal 10, tv.channel
    assert tv.on?
  end

  def test_proxy_records_messages_sent_to_tv
    tv = Proxy.new(Television.new)

    tv.power
    tv.channel = 10

    assert_equal [:power, :channel=], tv.messages
  end

  def test_proxy_handles_invalid_messages
    tv = Proxy.new(Television.new)

    assert_raise(NoMethodError) do
      tv.no_such_method
    end
  end

  def test_proxy_reports_methods_have_been_called
    tv = Proxy.new(Television.new)

    tv.power
    tv.power

    assert tv.called?(:power)
    assert ! tv.called?(:channel)
  end

  def test_proxy_counts_method_calls
    tv = Proxy.new(Television.new)

    tv.power
    tv.channel = 48
    tv.power

    assert_equal 2, tv.number_of_times_called(:power)
    assert_equal 1, tv.number_of_times_called(:channel=)
    assert_equal 0, tv.number_of_times_called(:on?)
  end

  def test_proxy_can_record_more_than_just_tv_objects
    proxy = Proxy.new("Code Mash 2009")

    proxy.upcase!
    result = proxy.split

    assert_equal ["CODE", "MASH", "2009"], result
    assert_equal [:upcase!, :split], proxy.messages
  end
end


# ====================================================================
# The following code is to support the testing of the Proxy class.  No
# changes should be necessary to anything below this comment.

# Example class using in the proxy testing above.
class Television
  attr_accessor :channel

  def power
    if @power == :on
      @power = :off
    else
      @power = :on
    end
  end

  def on?
    @power == :on
  end
end

# Tests for the Television class.  All of theses tests should pass.
class TelevisionTest < EdgeCase::Koan
  def test_it_turns_on
    tv = Television.new

    tv.power
    assert tv.on?
  end

  def test_it_also_turns_off
    tv = Television.new

    tv.power
    tv.power

    assert ! tv.on?
  end

  def test_edge_case_on_off
    tv = Television.new

    tv.power
    tv.power
    tv.power

    assert tv.on?

    tv.power

    assert ! tv.on?
  end

  def test_can_set_the_channel
    tv = Television.new

    tv.channel = 11
    assert_equal 11, tv.channel
  end
end


5478
11
задан 21 апреля 2011 в 02:04 Источник Поделиться
Комментарии
3 ответа

@methods_to_pass, @messages = [], []
@object = target_object
@object.class.instance_methods(true).each { |method_name| @methods_to_pass << method_name; method_name }

Пара вещей, чтобы отметить здесь:

В первую очередь ; имя_метода бит совершенно ненужными. Я не знаю, почему ты туда положил, но это не служит никакой цели и только запутывает читателя.

Во-вторых фу = []; - бар.каждый {|х| ФОО << х} - это слишком трудоемкий способ скопировать массив. Вы можете просто сказать фу = бар.ДУП. Однако в этом случае нет причин, чтобы сделать копию, так что вы можете просто сказать фу = бар, т. е. @methods_to_pass = @предмет.класс.instance_methods(правда) (а не - см. ниже).

Это, как говорится хранить методы в массив, как это плохая идея по двум причинам:


  1. После проверки того, является ли элемент в массиве - О(П), ваш и method_missing метод и поэтому каждая индикатора вызова будет о(n). Это бессмысленно неэффективно.

  2. Если @объект имеет Singleton-методы или динамически определенными методами, те не будут переадресованы, как они не появляются в @объект.класс.instance_methods.

Чтобы исправить эти проблемы, вы должны избавиться от @methods_to_pass и вместо этого использовать один из следующих подходов:

Вариант а:

В и method_missing заменить если @methods_to_pass.включить?(имя_метода.to_s) С если @объект.respond_to?(имя_метода). Это будет быстрее, чем поиск по массиву и она также будет работать для динамически определенными или синглтон-методы. Однако он не будет работать, если @объект имеет нестандартную и method_missing метод и не перекроет respond_to? чтобы быть в синхронизации с и method_missing. Если вы хотите справиться с этом случае, вам потребуется использовать один из двух других вариантов.

Вариант Б:

Просто удалите все , если и всегда вперед (и записи) вызов метода. Таким образом, поведение будет отличаться от вашего кода, как недопустимые вызовы будут записаны в @сообщения. Однако поскольку они все равно будут вызывать NoMethodError и назначение на самом деле не государство ли недопустимые вызовы должны быть записаны или нет, он по-прежнему удовлетворяет предписаниям.


Теперь давайте поговорим о @сообщения такие:

Используя массив для хранения сообщений, а затем, используя подсчет , чтобы выяснить, насколько часто данное сообщение появляется снова излишне неэффективным. Что я буду делать вместо этого использовать хэш , который отображает каждое сообщение, сколько раз он был назван. Таким образом, вы пишете @сообщение = хэш.новые(0) вместо @сообщения = [], @сообщения[имя_метода] += 1 вместо @сообщения << имя_метода и @сообщения[имя_метода] вместо @сообщения.граф(имя_метода).

11
ответ дан 24 апреля 2011 в 01:04 Источник Поделиться

ответы sepp2k все хорошо, я бы добавил еще одно предложение. Прокси-класс должен быть производным от BasicObject (если вы используете Руби 1.9.х). На данный момент любой метод, определенный на прокси-сервер по определению не хватает. Например, дано:

foo = Foo.new
pfoo = Proxy.new(foo)

если я попросил pfoo.class я хотел бы получить прокси , потому что класс - это способ в наследство по доверенности. С BasicObject это вместо того, чтобы вернуть ФОО и добавьте запись для "класса" в прокси-объект.

Одна гнида ковырять, я хотел бы добавить символ подчеркивания перед названием и number_of_times_called. Поставив знак подчеркивания метод-это ключ к другим программистам, что метод за пределами нормальной эксплуатации класса. В данном случае это поможет предотвратить конфликт имен с объектом вы собираетесь прокси. В настоящий параноик будет на самом деле вызвать метод на отдельном объекте, чтобы отслеживать звонки, оставляя все пространство имен не используется. Подумайте, что произойдет, если вы пытались прокси - Прокси экземпляр. :)

3
ответ дан 29 апреля 2011 в 12:04 Источник Поделиться

На мой взгляд, проблемы были с каждым из предложенных решений, который является, почему я пошел другим путем: фантик класса динамически программ во время инициализации объекта: его просто "реализует" каждый общественный способ (за исключением нескольких системе-специфические) оборачиваемого класса, записывая его имя и переадресации звонка к обернутому объекту.

Таким образом, не только методы, которые не определены в обертку класса пересылаются, но и (в большинстве) те, которые являются определенными. Это очень важно, если обернутом класса (телевизор в данном примере) заменяет некоторые стандартные метод to_s. Во всех остальных решений выше, отправка to_s сообщение в оболочку объекта просто позвоните оболочкой to_s (наследуется от объекта). В мое решение, вызываемого метода на самом деле является обернутого объекта собственного to_s метод.

Кроме того, я думаю, что мой подход является единственно вот что делает объект-обертка ответ на respond_to? правильно, т. е. возвращения справедливо для всех методов, определенных в обернутом класса.

class Proxy
attr_reader :messages # return an array of messages called

def initialize(target_object)
@object = target_object
@methods_called = Hash.new(0);
@messages = [] # Array for storing the message names

# For each of the object's methods
methods_to_forward = @object.methods - [:__send__, :object_id, :instance_of?, :method_missing]
methods_to_forward.each do |method|
eval """ def #{method}(*args, &block)
@methods_called[:#{method}] += 1
@messages.push :#{method}
@object.send(:#{method}, *args, &block)
end
"""
end
end

def called? method
number_of_times_called(method) > 0
end

def number_of_times_called(method)
@methods_called[method]
end
end

2
ответ дан 23 ноября 2011 в 07:11 Источник Поделиться