Pull to refresh

Как конфигурация влияет на архитектуру приложения

Reading time6 min
Views7.8K
Тестовое приложение для тестирования сериализаторов было сделано на библиотеке 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="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, по ряду конкретных причин, плохо подходит для конфигурационных файлов.
Tags:
Hubs:
+3
Comments60

Articles