k100
k100 личный блог
26 октября 2017, 12:32

Торговая система своими руками. Часть 10. IoC, защита от сбоев, логгирование.

     Привет всем! В предыдущих статьях я описывал свой тестер, разработанный на C#, и, несколько раз подчёркивал, что переключение между двумя режимами (тестирование/торговля) может быть простым. Код стратегий не должен зависеть от того, кто поставщик маркет-даты и куда уходят заявки – в тестовую базу или на сервер брокера. Конечно, это лишь один из подходов, и кому-то он покажется странным, но, главное его достоинство заключается в том, что тестирование приближается к реальности, что даёт более достоверные результаты. Вопрос в следующем: как, имея один и тот же код, получать разные по функциональности программы? Один из вариантов – использовать инверсию управления и внедрение зависимостей! Об этом сегодня и пойдёт речь.

    Приведу пример нехорошего (иногда, говорят – с запашком) кода:

class Strategy
{
   public Strategy()
   {
     var mgr = new TestOrderManadger();
     mgr.PlaceOrder(...);
   }
}

     Здесь плохо то, что класс Strategy зависит от класса TestOrderManadger. В такой реализации нельзя начать использовать какой-нибудь другой менеджер заявок (AnotherOrderManadger) без перекомпиляции библиотеки с классом Strategy. Тем более тут нарушается принцип единства ответственности – класс Strategy, помимо своей прямой обязанности, также, создаёт внутри себя зависимости. Чтобы исправить ситуацию, можно использовать интерфейсы:

interface IOrderMandger
{
   void PlaceOrder();
}

class TestOrderManadger : IOrderMandger
{
   public void PlaceOrder(){}
}

class Strategy
{
   public Strategy(IOrderMandger orderMandger)
   {
     var mgr = orderMandger;
     mgr.PlaceOrder(...);
   }
}

     И, где-то далее в коде:

Strategy mySuperStrategy = 
  new Strategy(new TestOrderManadger());

     В этом варианте Strategy зависит не от конкретного класса TestOrderManadger, а от абстракции (от интерфейса IOrderMandger), и не важно, какая будет реализация. Класс Strategy больше не занимается построением зависимостей (больше не создаёт внутри себя объекты). Это и есть инверсия управления (Inversion of Control, IoC) – модули верхнего уровня не должны зависеть от модулей нижнего уровня. Но всё-таки у такого кода ещё есть “запашок”. Проблема в строчке:

Strategy mySuperStrategy = 
   new Strategy(new TestOrderManadger());

     Точнее, в операторе new. Используя оператор new, мы всё ещё явно создаём конкретные объекты. Хорошо бы кому-то делегировать, всё, что связанно с построением зависимостей и с созданием объектов. В идеале, в коде, вообще не должно быть оператора new (за исключением простых, скалярных типов). Механизм, реализующий это, и называется  “внедрение зависимостей” (Dependency Injection, DI). Где-то, в самом начале программы (или даже во внешнем файле) регистрируются все интерфейсы и их реализации, а дальше, разрешение зависимостей и создание объектов берёт на себя IoC контейнер.

     Под .Net есть целая куча IoC контейнеров, я использую Autofac, но принципы регистрации и применения у всех схожи. Сначала, нужно настроить контейнер – указать, какой класс реализует тот или иной интерфейс. Вот, примерно так выглядит настройка:

static class AutofacContainer
{
   public static IContainer ConfigureContainer(
     bool isTestMode)
   {
     ContainerBuilder builder =  GetContainerBuilder();

     if (isTestMode)
     {
       builder
       .RegisterType<TestOrderManadger()
       .As<IOrderManadger>()
       .SingleInstance();
       ...
     }
     else
     {
       builder
       .RegisterType<AnotherOrderManadger()
       .As<IOrderManadger>()
       .SingleInstance();
       ...
     }

     return builder.Build();
   }
}

     Смысл в том, что от переданного в конфигуратор аргумента (isTestMode) зависит, будет ли программа работать в режиме бэктеста или в режиме реальных торгов. Таким же образом регистрируются все необходимые классы: сервисы, репозитории и пр., и даже здесь не используется оператор new, и нигде в коде. В контейнере можно провести дополнительные настройки, связанные с механизмом созданием объекта и его жизненным циклом. Например, в коде выше, команда SingleInstance(), указывает, что объект должен быть создан в единственном экземпляре.

     Теперь, после настройки контейнера, код, приведенный чуть выше:

Strategy mySuperStrategy =
   new Strategy(new TestOrderManadger());

     Можно заменить на:

var container = AutofacContainer
     .ConfigureContainer(isTestMode: false);

Strategy mySuperStrategy = 
     container.Resolve<Strategy>();

     Контейнер сам подставит в конструктор класса Strategy нужные зависимости (нужную реализацию интерфейса IOrderMandger).

     В предыдущих постах, я рассказывал, что использую фабрику стратегий – класс, который по некоему ID стратегии выдаёт ту или иную реализацию интерфейса IStrategy. Чтобы это заработало, в IoC контейнере, нужно зарегистрировать все стратегии и присвоить им уникальный идентификатор:

builder.RegisterType<MySuperStrategy>()
   .Keyed<IStrategy>(1);

builder.RegisterType<AnoterSimpleStrategy>()
   .Keyed<IStrategy>(2);

builder.RegisterType<MagicStrategy>()
   .Keyed<IStrategy>(3);
...

     И, код самой фабрики:

public class StrategyFactory
{
   private IComponentContext container;
   ...
   
   public IStrategy CreateStrategy(
     int strategyID)
   {
     IStrategy strategy =
       container
       .ResolveKeyed<IStrategy>(strategyID);
     return strategy;
   }
}

     Фабрика берёт на себя ответственность за создание объектов стратегий. По сути, это небольшая обёртка над контейнером. Тут же могут быть какие-то дополнительные настройки стратегии.

     Сам контейнер инициализируется в момент старта программы, и, в зависимости от значения переменной isTestMode, будут использоваться те или иные объекты, и от этого будет зависеть режим работы – тестовый или торговый.


     Теперь, хотел сказать по поводу логгирования. По сути, это протоколирование того, что программа делает. В простейшем случае можно было бы выводить сообщения в консоль. Но, если использовать отдельную библиотеку можно получить ряд преимуществ: настраиваемый формат лога, вывод в отдельный фай (или архив, БД), настраиваемые уровни лога (критическая ошибка, просто сообщение и т.п.), поддержка многопоточности. Я использую NLog под .Net, но вариантов много (вплоть до аспектно-ориентированных логгеров, реализующих сквозную функциональность).

     Вот так может выглядеть настройка логгера внутри IoC контейнера:

var fileName = @"C:\Logfile\Log.txt";

var config = new LoggingConfiguration();
var fileTarget = new FileTarget();
config.AddTarget("file", fileTarget);
fileTarget.FileName = fileName;
fileTarget.Layout = @"${newline}${level} 
   ${longdate} ${callsite} ${message}
   ${exception:format=tostring} ${newline}";

var rule = new LoggingRule("*", 
                LogLevel.Debug, 
                fileTarget);

config.LoggingRules.Add(rule);
LogManager.Configuration = config;

builder
 .RegisterInstance<ILogger(LogManager
      .GetLogger("Logger"))
      .SingleInstance();

     Далее, в коде программы, можно осуществлять логгирование:

try
{
   ...
}
catch (Exception e)
{
   logger.Error(e, "Ошибка работы программы");
}

     При этом, если произойдёт ошибка, и программа попадёт в блок catch, в лог запишется подробная информация об этой ошибке: когда она произошла, код ошибки, в каком модуле и т.д.

     Контроль за ботами и за работой программы в целом – это важная составляющая архитектуры системы. На первых парах лучше перестараться, чем потерять деньги на какой-нибудь ерунде. Важно использовать блоки try..catch и логгирование. Также, я думаю, в серьёзных системах есть независимая от ботов программа, которая может быть даже запускается на другом терминале. Эта программа контролирует работу ботов, и у неё есть возможность отключить их, или даже вырубить сеть, если что-то пошло не так: полилось слишком много заявок, или сильные изменения в портфеле, или пришла аномальная марке-дата. В любом случае, своим ботам надо доверять, а это достигается, в том числе, за счёт необходимого программного контроля за ними.

     На этом всё! Всем спасибо! ;-)

9 Комментариев
  • crazyFakir
    26 октября 2017, 17:45


  • Vasek
    26 октября 2017, 18:18
    Пробовал изучать С++ когда-то. Для меня оказалось слишком сложно. Автор, ты гений)))
  • Иван Петров
    29 октября 2017, 21:13
    Вопрос к автору. Есть желание научиться самому написать робота, но в программировании полный ноль. С чего начать обучение?
      • Иван Петров
        30 октября 2017, 12:55
        k100, Благодарю за столь объемный ответ. В принципе я не хочу становиться ни верстальщиком, ни администратором БД и т.д. Есть допустим простенькая торговая система, хочу ее автоматизировать, но не с целью заработка, а для того чтобы разобраться как это делается. Пусть это будет на языке C#. Но без теории что то начинать бессмысленно. В сети море литературы и курсов. Посоветуйте пожалуйста с какой литературы начать изучать основы и в каком направлении двигаться, чтобы понять что вы пишите в своих постах))

Активные форумы
Что сейчас обсуждают

Старый дизайн
Старый
дизайн