Тестовое приложение для тестирования сериализаторов было сделано на библиотеке NFX. Это Unistack библиотека. Честно говоря, я затрудняюсь назвать другой пример Unistack библиотеки. Может быть что-то похожее есть в виде ServiceStack. Хотя ServiceStack, в отличии от NFX, размазан по нескольким dll. Но самое главное, ServiceStack не является Uni, так как его части сделаны немножко по-разному, и он не покрывает такого глобального пространства, как NFX. Но целью данной статьи не является обсуждение концепции Unistack, а одна из особенностей использования NFX.
Как использование NFX повлияло на наше тестовое приложение? Давайте посмотрим.
Тестовое приложение — это консольное приложение. Мы запускаем его и в конце получаем результаты тестов. Тестов может быть много и прогонять все тесты во всех комбинациях за один проход будет глупо. Что бы я сделал без NFX? Скорее всего добавил бы несколько параметров в командную строку, чтобы запускать только нужные мне сейчас тесты. Немножко поработав, я бы добавил конфигурационные параметры в Xml config файл и читал бы их оттуда. Я бы, скорее всего использовал простой массив параметров, имя — значение, в appSettings секции конфигурационного файла. Можно построить более сложную структуру конфигурации, но поддержка этого в .NET не так проста, и я бы на время забыл про сами тесты, а разрабатывал бы и отлаживал этот конфигурационный файл. Нет, я бы не стал делать сложную структуру, потому что — это сложно, а полученные с его помощью преимущества не так велики.
В NFX сделать сложную конфигурацию — просто. Это настолько просто, что это кардинально меняет дизайн нашего тестового приложения.
Предположим, я не знаю ничего о NFX и пытаюсь понять, как это работает, на примере нашего приложения.
Открою конфигурационный файл objgraph.laconf. Вот секция, описывающая сами тесты:
Очевидно, секция tests содержит внутри коллекцию секций test, каждая из которых определяет параметры одного теста. Первый параметр — type, опять же очевидно, что он указывает на тип (класс) в assembly. В первом тесте — это соответственно, Serbench.Specimens.Tests.ObjectGraph класс в Serbench.Specimens assembly. Все остальные параметры тоже понятны без дополнительных разъяснений.
Вот секция, описывающая сериалайзеры:
Ничего нового, все понятно, разве что появилась конструкция _include, указывающая на файл.
Все это сильно похоже на JSON. Пока что самое большое отличие от него в использовании ‘=’ вместо ‘:’. Еще коллекции не выделяются особым образом, в JSON — это ‘[]’, здесь это — те же ‘{}’.
Хорошо, теперь пойду в сам код и посмотрю, какой API используется, чтобы добраться до этих конфигурационных параметров.
Вот Testкласс, который был указан в config файле:
В конфигурации мы имеем
а в коде — немножко измененные параметры. К примеру, из m_SerIterations получилось ser-iterations. То есть переменные в конфигурации все пишутся маленькими буквами. Если встречается заглавная буква, то она становится маленькой, но перед ней ставится ‘-’. И префикс ‘m_’ просто отбрасывается.
Стоп, а как же мы поймем, что переменная из кода становится конфигурируемой? Очевидно, что с помощью атрибута [Config].
Хорошо, понятно, как задаётся конфигурация. А как она используется? Попробую разобраться с секцией serializers.
Нахожу ее в в классе TestingSystem:
Теперь я вижу работу с контейнером. Регистрируется отдельный Serializer класс для каждого serializer из конфигурации.
А что такое m_Serializers?
Похожий код — для m_Tests, та же регистрация, но уже Test классов.
doTestRun() метод — для запуска одного тестового прохода (runs=1). Он запускает сначала нужное количество итераций сериализации (ser-iterations=100), потом нужное количество итераций десериализации (deser-iterations=100). Все эти параметры задаются в конфигурации.
Хорошо, с деталями вроде все понятно. Вернусь назад.
Если теперь заново взглянуть на приложение, то увидим, что это уже не типичное консольное приложение с парой строчек конфигурации. Теперь конфигурация разрослась и стала по размеру соизмерима непосредственно с кодом на С#. Конфигурация стала похожа на интерфейсы к приложению. У нас нет UI, это по-прежнему консольное приложение, но насколько сильно бизнес логика переместилась из кода в конфигурацию!
Насколько интересной стала конфигурация. Здесь есть и настройки всего приложения, и настройки отдельных классов. Теперь классы больше похожи на бизнес-объекты.
И — да, вы правы. Теперь все приложение можно разрабатывать, начиная с определения конфигурации, которая теперь является нашим бизнес-интерфейсом. Уже после этого можно приступать непосредственно к дизайну классов и кодированию.
Давайте еще раз посмотрим, что мы добавили и что получили взамен.
Мы стали использовать конфигурационную систему из NFX. Мы сначала создали конфигурационный файл с бизнес-интерфейсами:
Другими словами, мы сначала описали модель нашего тестового приложения в конфигурации.
Следующим шагом мы создали конкретные классы и связали их с конфигурацией.
Здесь мы связываем конфигурацию с конкретными классами не во время компиляции приложения, а во время его работы. Как сильно это увеличит вероятность run-time ошибок?
Весь NFX находится в одном assembly. В нем масса классов, которые я не буду использовать. Мешаются ли они?
А можно ли использовать NFX в IoT устройствах? Возможностей у нее много, и все это — в одном файле.
Как я вижу, конфигурация задается в каком-то новом языке. Честно говоря, не хочу изучать еще один язык. Есть какие-нибудь альтернативы?
Как использование NFX повлияло на наше тестовое приложение? Давайте посмотрим.
Тестовое приложение — это консольное приложение. Мы запускаем его и в конце получаем результаты тестов. Тестов может быть много и прогонять все тесты во всех комбинациях за один проход будет глупо. Что бы я сделал без NFX? Скорее всего добавил бы несколько параметров в командную строку, чтобы запускать только нужные мне сейчас тесты. Немножко поработав, я бы добавил конфигурационные параметры в Xml config файл и читал бы их оттуда. Я бы, скорее всего использовал простой массив параметров, имя — значение, в appSettings секции конфигурационного файла. Можно построить более сложную структуру конфигурации, но поддержка этого в .NET не так проста, и я бы на время забыл про сами тесты, а разрабатывал бы и отлаживал этот конфигурационный файл. Нет, я бы не стал делать сложную структуру, потому что — это сложно, а полученные с его помощью преимущества не так велики.
В NFX сделать сложную конфигурацию — просто. Это настолько просто, что это кардинально меняет дизайн нашего тестового приложения.
Предположим, я не знаю ничего о NFX и пытаюсь понять, как это работает, на примере нашего приложения.
Открою конфигурационный файл objgraph.laconf. Вот секция, описывающая сами тесты:
tests
{
test
{
type="Serbench.Specimens.Tests.ObjectGraph, Serbench.Specimens"
name="Warmup ObjectGraph"
order=000
runs=1
ser-iterations=1
deser-iterations=1
}
test
{
type="Serbench.Specimens.Tests.ObjectGraph, Serbench.Specimens"
name="Conferences: 1; Participants: 250; Events: 10"
order=000
runs=1
ser-iterations=100
deser-iterations=100
...
Очевидно, секция tests содержит внутри коллекцию секций test, каждая из которых определяет параметры одного теста. Первый параметр — type, опять же очевидно, что он указывает на тип (класс) в assembly. В первом тесте — это соответственно, Serbench.Specimens.Tests.ObjectGraph класс в Serbench.Specimens assembly. Все остальные параметры тоже понятны без дополнительных разъяснений.
Вот секция, описывающая сериалайзеры:
serializers
{
// Stock serializers: they use only Microsoft .NET libraries
serializer
{
type="Serbench.StockSerializers.MSBinaryFormatter, Serbench"
name="MS.BinaryFormatter"
order=10
}
serializer
{
type="Serbench.StockSerializers.MSDataContractJsonSerializer, Serbench"
name="MS.DataContractJsonSerializer"
order=20
_include { file="knowntypes.Conference.laconf"} //include file contents
}
...
Ничего нового, все понятно, разве что появилась конструкция _include, указывающая на файл.
Все это сильно похоже на JSON. Пока что самое большое отличие от него в использовании ‘=’ вместо ‘:’. Еще коллекции не выделяются особым образом, в JSON — это ‘[]’, здесь это — те же ‘{}’.
Хорошо, теперь пойду в сам код и посмотрю, какой API используется, чтобы добраться до этих конфигурационных параметров.
Вот Testкласс, который был указан в config файле:
public abstract class Test : TestArtifact
…
[Config(Default=100)]
private int m_SerIterations;
[Config(Default=100)]
private int m_DeserIterations;
[Config(Default=1)]
private int m_Runs;
В конфигурации мы имеем
runs=1
ser-iterations=100
deser-iterations=100
а в коде — немножко измененные параметры. К примеру, из m_SerIterations получилось ser-iterations. То есть переменные в конфигурации все пишутся маленькими буквами. Если встречается заглавная буква, то она становится маленькой, но перед ней ставится ‘-’. И префикс ‘m_’ просто отбрасывается.
Стоп, а как же мы поймем, что переменная из кода становится конфигурируемой? Очевидно, что с помощью атрибута [Config].
Хорошо, понятно, как задаётся конфигурация. А как она используется? Попробую разобраться с секцией serializers.
Нахожу ее в в классе TestingSystem:
public const string CONFIG_SERIALIZERS_SECTION = "serializers";
public const string CONFIG_SERIALIZER_SECTION = "serializer";
...
foreach(var snode in node[CONFIG_SERIALIZERS_SECTION].Children.Where(cn => cn.IsSameName(CONFIG_SERIALIZER_SECTION)))
{
var item = FactoryUtils.Make<Serializer>(snode, args: new object[]{this, snode});
m_Serializers.Register( item );
log(MessageType.Info, "conf sers", "Added serializer {0}.'{1}'[{2}]".Args(item.GetType().FullName, item.Name, item.Order));
}
if (m_Serializers.Count==0)
...
Теперь я вижу работу с контейнером. Регистрируется отдельный Serializer класс для каждого serializer из конфигурации.
А что такое m_Serializers?
private OrderedRegistry<Serializer> m_Serializers = new OrderedRegistry<Serializer>();
Похожий код — для m_Tests, та же регистрация, но уже Test классов.
doTestRun() метод — для запуска одного тестового прохода (runs=1). Он запускает сначала нужное количество итераций сериализации (ser-iterations=100), потом нужное количество итераций десериализации (deser-iterations=100). Все эти параметры задаются в конфигурации.
Хорошо, с деталями вроде все понятно. Вернусь назад.
Резюме
Если теперь заново взглянуть на приложение, то увидим, что это уже не типичное консольное приложение с парой строчек конфигурации. Теперь конфигурация разрослась и стала по размеру соизмерима непосредственно с кодом на С#. Конфигурация стала похожа на интерфейсы к приложению. У нас нет UI, это по-прежнему консольное приложение, но насколько сильно бизнес логика переместилась из кода в конфигурацию!
Насколько интересной стала конфигурация. Здесь есть и настройки всего приложения, и настройки отдельных классов. Теперь классы больше похожи на бизнес-объекты.
И — да, вы правы. Теперь все приложение можно разрабатывать, начиная с определения конфигурации, которая теперь является нашим бизнес-интерфейсом. Уже после этого можно приступать непосредственно к дизайну классов и кодированию.
Давайте еще раз посмотрим, что мы добавили и что получили взамен.
Мы стали использовать конфигурационную систему из NFX. Мы сначала создали конфигурационный файл с бизнес-интерфейсами:
- Вот тесты, которые мы хотим выполнять.
- Вот сериалайзеры, которые мы хотим тестировать.
- Вот тестовые данные.
- Вот итоговые данные и их форматы.
Другими словами, мы сначала описали модель нашего тестового приложения в конфигурации.
Следующим шагом мы создали конкретные классы и связали их с конфигурацией.
Вопросы от программиста
Здесь мы связываем конфигурацию с конкретными классами не во время компиляции приложения, а во время его работы. Как сильно это увеличит вероятность run-time ошибок?
Да, теперь если мы ошибемся, к примеру, в имени класса в конфигурации, то эта ошибка будет обнаружена не при компиляции, я только во время работы приложения. NFX загружает конфигурацию при старте приложения. Поэтому большая часть ошибок обнаруживается сразу же при старте, а не во время работы отдельного класса. При этом диагностика однозначно локализует ошибки. В результате вероятность run-time ошибок повышается незначительно.
Весь NFX находится в одном assembly. В нем масса классов, которые я не буду использовать. Мешаются ли они?
Первое, что вы заметите, когда первый раз скомпилируете приложение под NFX, это — как быстро пройдет компиляция. NFX — библиотека, сделанная программистами для программистов. И предназначена она для самых критических случаев: тысячи серверов, миллионы сообщений и т.п. Все, что тормозит, было переработано или полностью заменено. Кроме того, NFX — это библиотека для серверов, размер для нее не так важен. Хотя не думаю, что 1.5 МБ (размер NFX) будет велика и для клиентских приложений.
А можно ли использовать NFX в IoT устройствах? Возможностей у нее много, и все это — в одном файле.
Мы над этим, честно говоря, не думали. Все же NFX, не забывайте, работает на .NET. Если будут устройства с загруженным .NET, то — почему бы и нет.
Как я вижу, конфигурация задается в каком-то новом языке. Честно говоря, не хочу изучать еще один язык. Есть какие-нибудь альтернативы?
Да, есть. В NFX конфигурации можно описывать еще на Laconic или на XML. При этом при переходе между Laconic и XML вам не придется менять абсолютно ничего в коде.
Почему для конфигураций был использован язык Laconic, а не JSON? Он не сложнее JSON и выучить его можно за 5 минут. К сожалению, JSON, по ряду конкретных причин, плохо подходит для конфигурационных файлов.