Статьи

Динамические возможности C # помогли мне выучить Ruby

До версии 3.0 C # был статическим языком. Динамические функции были введены в язык в версии 4.0 как попытка улучшить поддержку COM и взаимодействие между C # и динамическими языками, такими как JavaScript. Я использовал эти динамические функции по разным причинам.

Добавление динамических данных в объекты C #

Приложениям часто нужны динамические данные. Распространенным случаем является необходимость отслеживать разные биты данных о пользователе. Например, приложение может поставляться с возможностью отслеживать как имена, так и фамилии клиентов, но пользователи могут также захотеть отслеживать «комментарии» и «количество иждивенцев». Поскольку C # является объектно-ориентированным языком, разработчики хотели бы видеть эти вещи как свойства объекта. Создать основной класс Customer легко:

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Использовать этот класс также будет легко:

 var customer = new Customer();
var fullName = string.Format("{0} {1}", customer.FirstName, customer.LastName);

Но что делать со «свойствами», которые пользователь должен динамически добавлять? Разработчики не могут знать об этих свойствах или обо всех других, которые пользователь может создать в будущем.

Мы увидим один из возможных способов реализовать такое требование, используя динамические возможности C #. Сначала мы начнем со спецификации, которая тестирует поведение, которое мы ищем:

 [Specification]
public void dynamic_properties()
{
    "Given additional dynamic data for a customer"
            .Context(() => make_data_for_a_customer());
    "When constructing a customer object"
            .Do(() => { _customer = GetCustomer(); });
    "Then the object should expose the additional data as properties"
            .Assert(properties_are_exposed);
    " And getters are available"
            .Assert(getters_are_exposed);
    " And setters are available"
            .Assert(setters_are_exposed);
}

Если вы не привыкли писать этот стиль тестов, посмотрите SubSpec .

Далее, методы, которые подготавливают контекст, действия и утверждения нашей спецификации:

 void make_data_for_a_customer()
{
    _additionalData.Clear();
    _additionalData.Add("Comments", "Some comments..."); 
    _additionalData.Add("NumberOfDependents", 3);
}

Customer GetCustomer()
{
    return new Customer(_additionalData);
}

void properties_are_exposed()
{
    (_customer as DynamicEntity).RespondTo("Comments").ShouldBeTrue();
    (_customer as DynamicEntity).RespondTo("NumberOfDependents").ShouldBeTrue();
}

void getters_are_exposed()
{
    ((string) _customer.Comments).ShouldEqual("Some comments...");
    ((int) _customer.NumberOfDependents).ShouldEqual(3);
}

void setters_are_exposed()
{
    _customer.Comments = "New comments";
    _customer.NumberOfDependents = 9;

    ((string)_customer.Comments).ShouldEqual("New comments");
    ((int)_customer.NumberOfDependents).ShouldEqual(9);
}

Но как может приведенный выше код ( _customer.Comments ) успешно скомпилироваться, если класс Customer не имеет явного свойства Comments во время компиляции? Код компилируется, потому что поле _customer объявлено в классе как динамическое :

 dynamic _customer;

Если вас интересует поле _additionalData, это всего лишь словарь:

 Dictionary<string, object> _additionalData = new Dictionary<string, object>();

Используя эти функции и злоупотребляя ими для поддержки приложений, которые растут динамически, мне было довольно приятно наблюдать, как классы моделей в Rails (точнее ActiveRecord) не имеют объявленных свойств, но, тем не менее, они были полностью заполнены на основе схема базы данных при запуске приложения. Возьмем этот класс Customer в Ruby, например:

 class Customer < ActiveRecord::Base
end

Такой класс может быть создан и использован следующим образом:

 customer = Customer.new
customer.comments = "Some comments..."
customer.number_of_dependents = 3

Опять же, класс получает свои свойства динамически на основе столбцов таблицы Customers.

Хеши, хэши, везде …

Несмотря на то, что можно создать экземпляр класса и задать для него свойства в Ruby, как и в C #, большинство Rubyists предпочитают использовать хэши. Вот как это выглядит:

 customer = Customer.new(:comments => "Some comments...", 
                              :number_of_dependents => 3)

Хотя это может выглядеть как именованные параметры для разработчика на C #, это не так. Метод initialize (который вызывается при вызове new для класса) в модели ActiveRecord принимает хеш со значениями, которые мы хотим установить для свойств этого объекта. Если мы вернемся к нашему примеру C #, мы могли бы также создать экземпляр класса Customer следующим образом:

 customer = new Customer(new Dictionary<string, object>
                      {
                          { "Comments", "Some comments..." }, 
                          { "NumberOfDependents", 3 }
                      });

Поскольку я использовал много таких словарей в C #, эти вещи в Ruby не казались совершенно странными.

А как насчет добавления динамического поведения?

До сих пор я говорил о динамических данных, добавляемых к объектам; а как насчет добавления динамического поведения? Это реализовано в C # аналогичным образом. Давайте рассмотрим следующий пример: представим, что наш объект Customer необходимо использовать в таком контексте, где он должен предоставлять свойство FullName. Имейте в виду, что метод получения свойства — это не что иное, как метод, который возвращает значение, поэтому он может иметь определенное поведение. Теперь давайте предположим, что форматирование FullName будет предоставлено пользователем через какую-то конфигурацию, и мы никогда не знаем, как именно каждый пользователь хочет это сделать.

Мы могли бы проверить такое поведение следующим образом:

 [Specification]
public void dynamic_behavior()
{
    "Given additional dynamic behavior for a customer"
            .Context(() => make_behavior_for_a_customer());
    "When constructing a customer object"
            .Do(() => _customer = GetCustomer());
    "Then the object should expose the additional behavior"
            .Assert(behaviors_are_exposed);
    "And the behavior should be accessible"
            .Assert(behaviors_are_accessible);
        }

Далее я покажу, как определяется динамическое поведение в этом примере:

 void make_behavior_for_a_customer()
{
    _additionalBehavior.Clear();
    _additionalBehavior.Add("FullName", c => string.Format("{0} {1}", c.FirstName, c.LastName));
}

Наш словарь _additionalBehavior немного сложнее, чем словарь _additionalData ; вместо того, чтобы принимать строку в качестве ключа и объект в качестве значения, мы теперь берем строку в качестве ключа и лямбду в качестве значения (а поскольку лямбда является способом реализации отложенного выполнения , на самом деле это не так. укажите «значение», но вместо этого «означает получение значения при необходимости».

Объявление этого поля также немного сложнее:

 Dictionary<string, Func<dynamic, dynamic>> _additionalBehavior =
            new Dictionary<string, Func<dynamic, dynamic>>();

Наша лямбда представлена Func делегировать. Другими словами, делегат, который принимает один параметр динамического типа и возвращает значение динамического типа.

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

Вот последняя часть наших тестов:

 void behaviors_are_exposed()
{
    (_customer as DynamicEntity).RespondTo("FullName").ShouldBeTrue();
}

void behaviors_are_accessible()
{
    ((string)_customer.FullName).ShouldEqual("Jon Jones");
}

Мы просто проверяем, отвечает ли наш объект на отправленное ему сообщение FullName и возвращает ли он то, что мы ожидаем от него.

Еще одно быстрое замечание: вы можете спросить себя, почему я приводил _customer.FullName к строке, чтобы получить доступ к ее значению. Причина этого в том, что метод ShouldEqual является методом расширения, и этот тип метода в настоящее время не работает с динамическими типами.

Так как же нам пройти эту спецификацию? Просто вернувшись к классу DynamicEntity, добавьте поле для хранения словаря с поведениями, а затем вызовите его из метода TryGetMember:

 public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    if (_additionalData.ContainsKey(binder.Name))
    {
        result = _additionalData[binder.Name];
        return true;
    }
    if (_additionalBehavior.ContainsKey(binder.Name))
    {
        result = _additionalBehavior[binder.Name].Invoke(this);
        return true;
    }
    return base.TryGetMember(binder, out result);
}

Поскольку значение, хранящееся в словаре, является делегатом, мы вызываем для него метод Invoke, передавая сам наш динамический объект ( this ), чтобы он был доступен для лямбды, которая с ним работает.

Как это будет выглядеть в Ruby?

Существует несколько способов реализации динамического поведения в Ruby, и я остановлюсь только на одном из них в стиле, который, вероятно, покажется более дружелюбным для разработчиков на C #. Я уверен, что каждый опытный Rubyist может предложить более чистые и лучшие способы, но эти способы, вероятно, будут выглядеть странно для разработчика на C #.

Вот наши тесты в Ruby (с использованием тестовой среды RSpec ):

 require './customer'

describe Customer do

  describe "Dynamic Behaviors" do

    context "Given additional dynamic beheavior for a customer" do

      before(:each) do
        @additional_behavior = { :full_name => 
                 lambda { |c| "#{c.first_name} #{c.last_name}" } }
      end

      describe "When object is constructed" do

        let(:customer) { Customer.new(@additional_behavior) } 

        it "should expose behaviors" do
          customer.should respond_to(:full_name)
        end

        it "should make behaviors accessible" do
          customer.first_name = "Joe"
          customer.last_name = "Jones"
          customer.full_name.should == "Joe Jones"
        end
      end
    end

  end
end

Даже если вы еще не знакомы с Ruby, вы должны быть в состоянии следовать коду в тесте ниже, во-первых, потому что я написал его таким образом, чтобы он напоминал тесты C #, которые я показал ранее, и во-вторых, потому что RSpec обеспечивает очень выразительный способ писать тесты.

Это класс Customer:

 require 'dynamic_entity'

class Customer < DynamicEntity
    attr_accessor :first_name, :last_name
end

В этом примере я объявляю свойства first_name и last_name, чтобы у нас не было никаких зависимостей от базы данных, ActiveRecord и т. Д. И, наконец, что не менее важно, класс DynamicEntity:

 class DynamicEntity

  attr_reader :additional_behavior

  def initialize(additional_behavior)
    @additional_behavior = additional_behavior
  end

  def respond_to?(message)
    self.additional_behavior.include?(message)
  end

  def method_missing(message, *args, &block)
    if self.additional_behavior.include?(message)
      self.additional_behavior[message].call(self)
    else
      super
    end
  end
end

Я в значительной степени перенес реализацию того же типа из C #, используя method_missingTryGetMember Я повторю еще раз: есть несколько других способов, каждый со своими преимуществами и недостатками, реализовать что-то подобное в Ruby. Я только показываю вам тот, который должен выглядеть наиболее знакомым для вас. После того, как вы некоторое время поигрались с Ruby on Rails, я настоятельно рекомендую вам прочитать Метапрограммирование Ruby , так как оно объясняет большую часть «магии», которую вы найдете в Rails, ActiveRecord и других гемах.

Что со всеми этими тестами?

Если вы — разработчик .NET, который не привык писать тесты для вашего кода, вы, вероятно, задаетесь вопросом, что же происходит со всеми этими тестами в этом посте. Я обязательно включил их, потому что они также сыграли огромную роль в моем изучении Ruby. Когда я начал писать код на C #, использующий динамические функции, я сразу понял, что мне просто нужно написать тесты для всего этого кода, поскольку компилятор не помог бы мне в этом. Это не было большой проблемой, так как я уже писал тесты для статически типизированного кода.

Переходя на Ruby, имело смысл придерживаться только написания тестов, поскольку там все динамично. Кроме того, будучи нубистом из Ruby, я знаю, что не всегда пишу лучший код, поэтому я пишу все, что могу, только чтобы мои тесты прошли. В большинстве случаев мой Ruby-код выглядит как код C # ( «я говорю на Ruby с акцентом на C #» , или, так сказать). Это нормально, потому что раз в месяц или два я смотрю на этот код, скорее всего, я найду лучший способ, чтобы я мог реорганизовать свой код и иметь тесты, чтобы поддержать меня на случай, если я что-то испорчу.

Резюме

Динамическое программирование — это одна из вещей, которая может помочь разработчику .NET освоить «путь Ruby». Как только вы увидите удивительные вещи, которые люди создали с его помощью, вернитесь к C # и посмотрите, как часть этих знаний может быть передана. Хороший пример этого можно увидеть в массовом проекте Роба Коннери, где он использовал динамическую функцию C # для создания инструмента доступа к данным, который заимствует идеи из того, что он изучил в Ruby и ActiveRecord.

Полный код этого поста доступен здесь .