До версии 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_missing
TryGetMember
Я повторю еще раз: есть несколько других способов, каждый со своими преимуществами и недостатками, реализовать что-то подобное в Ruby. Я только показываю вам тот, который должен выглядеть наиболее знакомым для вас. После того, как вы некоторое время поигрались с Ruby on Rails, я настоятельно рекомендую вам прочитать Метапрограммирование Ruby , так как оно объясняет большую часть «магии», которую вы найдете в Rails, ActiveRecord и других гемах.
Что со всеми этими тестами?
Если вы — разработчик .NET, который не привык писать тесты для вашего кода, вы, вероятно, задаетесь вопросом, что же происходит со всеми этими тестами в этом посте. Я обязательно включил их, потому что они также сыграли огромную роль в моем изучении Ruby. Когда я начал писать код на C #, использующий динамические функции, я сразу понял, что мне просто нужно написать тесты для всего этого кода, поскольку компилятор не помог бы мне в этом. Это не было большой проблемой, так как я уже писал тесты для статически типизированного кода.
Переходя на Ruby, имело смысл придерживаться только написания тестов, поскольку там все динамично. Кроме того, будучи нубистом из Ruby, я знаю, что не всегда пишу лучший код, поэтому я пишу все, что могу, только чтобы мои тесты прошли. В большинстве случаев мой Ruby-код выглядит как код C # ( «я говорю на Ruby с акцентом на C #» , или, так сказать). Это нормально, потому что раз в месяц или два я смотрю на этот код, скорее всего, я найду лучший способ, чтобы я мог реорганизовать свой код и иметь тесты, чтобы поддержать меня на случай, если я что-то испорчу.
Резюме
Динамическое программирование — это одна из вещей, которая может помочь разработчику .NET освоить «путь Ruby». Как только вы увидите удивительные вещи, которые люди создали с его помощью, вернитесь к C # и посмотрите, как часть этих знаний может быть передана. Хороший пример этого можно увидеть в массовом проекте Роба Коннери, где он использовал динамическую функцию C # для создания инструмента доступа к данным, который заимствует идеи из того, что он изучил в Ruby и ActiveRecord.
Полный код этого поста доступен здесь .