Привет, Хабр!
У нас готовится к выходу второе издание легендарной книги Марка Симана, «Внедрение зависимостей в .NET»
Поэтому сегодня мы решили кратко освежить тему внедрения зависимостей для специалистов по .NET и C# и предлагаем перевод статьи Грэма Даунса, где эта парадигма рассматривается в контексте инверсии управления (IoC) и использования контейнеров.
Большинству программистов, пожалуй, известен феномен «Внедрение зависимостей» (Dependency Injection), но не всем понятно, какой смысл в него вкладывается. Вероятно, вам приходилось иметь дело с интерфейсами и контейнерами, и иногда работа с ними приводила вас в тупик. С другой стороны, вы, возможно, только что-то слышали о внедрении зависимостей, и открыли эту статью, так как хотите лучше разобраться в их сути. В этой статье я покажу, насколько проста концепция внедрения зависимостей, и какой мощной она может быть на самом деле.
Внедрение зависимостей — это самодостаточный подход, который можно использовать сам по себе. С другой стороны, этот подход можно применять и вместе с интерфейсами, и с контейнерами для внедрения зависимостей/инверсии управления (DI/IoC). Применяя внедрение зависимостей в таком контексте, можно столкнуться с некоторой путаницей, которую поначалу испытывал и я.
На протяжении всей карьеры (я специализируюсь на разработке в Net/C#), я привык использовать внедрение зависимостей в его чистейшей форме. При этом я реализовывал DI, вообще не прибегая ни к контейнерам, ни к инверсии управления. Все изменилось совсем недавно, когда мне поставили задачу, в которой без использования контейнеров было не обойтись. Тогда я крепко усомнился во всем, что знал ранее.
(Здесь важно отметить: интерфейсы и контейнеры используются только в контексте внедрения зависимостей. Внедрение зависимостей можно реализовать и без интерфейсов/контейнеров, но, в сущности, единственное назначение интерфейсов или контейнеров – облегчить внедрение зависимостей).
В этой статье будет показано, как делается внедрение зависимостей, как с интерфейсами и контейнерами, так и без них. Надеюсь, что, дочитав ее, вы будете четко представлять принцип работы внедрения зависимостей и сможете принимать информированные решения о том, когда и где прибегать к использованию интерфейсов и контейнеров при внедрении зависимостей.
Чтобы лучше понять внедрение зависимостей в их чистейшей форме, давайте разберем пример приложения, написанного на C#.
Для начала отметим, что такое приложение можно было бы написать без какого-либо внедрения зависимостей. Затем мы введем в него внедрение зависимостей, добавив простую возможность логирования.
По ходу работы вы увидите, как требования, предъявляемые к логированию, постепенно усложняются, и мы удовлетворяем эти требования, используя внедрение зависимостей; при этом зона ответственности класса
Рассмотрим следующий код. Он написан для простого приложения-калькулятора, принимающего два числа, оператор и выводящего результат. (Это простое рабочее приложение для командной строки, поэтому вам не составит труда воспроизвести его как C# Console Application в Visual Studio и вставить туда код, если вы хотите следить за развитием примера. Все должно работать без проблем.)
У нас есть класс
Program.cs:
Главная программа запускается, запрашивает у пользователя два числа и оператор, а затем вызывает класс
Calculator.cs:
Приложение работает отлично, но только представьте: вашему начальнику вздумалось, что теперь все операции должны логироваться в файл на диске, чтобы было видно, чем люди занимаются.
Кажется, что не так это и сложно, верно? Берете и добавляете инструкции, в соответствии с которыми все операции, производимые в
Calculator.cs:
Прекрасно работает. Всякий раз, когда в
Но, возможен вопрос: а в самом ли деле уместно, чтобы класс Calculator отвечал за запись в текстовый файл?
Нет. Разумеется, это не его зона ответственности. Поэтому, чтобы не нарушался принцип единственной ответственности, все, что касается логирования информации, должно происходить в файле логов, и для этого требуется написать отдельный самодостаточный класс. Давайте этим и займемся.
Первым делом создаем совершенно новый класс, назовем его
FileLogger.csh:
Теперь все, что касается создания файла логов и записи информации в него обрабатывается в этом классе. Дополнительно получаем и одну приятную мелочь: что бы ни потреблял этот класс, не требуется ставить между отдельными записями пустые строки. Записи должны просто вызывать наш метод
Чтобы использовать класс, нам нужен объект, который его инстанцирует. Давайте решим эту проблему внутри класса
Calculator.cs:
Итак, теперь нашему калькулятору не важно, как именно новый логгер записывает информацию в файл, или где находится этот файл, и происходит ли вообще запись в файл. Однако, все равно существует одна проблема: можем ли мы вообще рассчитывать на то, что класс
Очевидно, ответ на последний вопрос отрицательный!
Именно здесь, уважаемый читатель, в дело вступает внедрение зависимости. Давайте изменим конструктор нашего класса
Calculator.cs:
Вот и все. Больше в классе ничего не меняется.
Внедрение зависимостей – это элемент более крупной темы под названием «Инверсия управления», но ее подробное рассмотрение выходит за рамки этой статьи.
В данном случае вам всего лишь требуется знать, что мы инвертируем управление классом логгера, либо, выражаясь метафорически, делегируем кому-то проблему создания файла
Итак, чья же это ответственность?
Как раз того, кто инстанцирует класс
Чтобы это продемонстрировать, изменим метод Main в нашем классе Program.cs следующим образом:
Program.cs
Таким образом, требуется изменить всего две строки. Мы не рассчитываем, что класс
В сущности, это и есть внедрение зависимостей. Не нужны ни интерфейсы, ни контейнеры для инверсии управления, ни что-либо подобное. В принципе, если вам доводилось выполнять что-либо подобное, то вы имели дело с внедрением зависимостей. Круто, правда?
Несмотря на вышесказанное, у интерфейсов есть свое место, и по-настоящему они раскрываются именно в связке с Внедрением Зависимостей.
Допустим, у вас есть клиент, с точки зрения которого логирование каждого вызова к
Как вы считаете, придется ли ради этого делать изменения внутри
Вот здесь нам и пригодятся интерфейсы.
Давайте напишем интерфейс. Назовем его
ILogger.cs
Как видите, он определяет единственный метод:
FileLogger.cs
Это единственное изменение, которое мы внесем в этот файл. Все остальное будет как прежде.
Итак, отношение мы определили – что нам теперь с ним делать?
Для начала изменим класс Calculator таким образом, чтобы он использовал интерфейс
Calculator.cs
На данном этапе код по-прежнему компилируется и выполняется без всяких проблем. Мы передаем в него
Поскольку все, что бы вы ни получили, реализует интерфейс
Теперь давайте добавим еще одну реализацию интерфейса
NullLogger.cs
На этот раз нам вообще ничего не потребуется менять в классе
Нам потребуется изменить только лишь метод Main в нашем файле Program.cs, чтобы передать в него иную реализацию. Давайте этим и займемся, чтобы метод
Program.cs
Опять же, изменить нужно только ту строку, которую я откомментировал. Если мы хотим использовать иной механизм логирования (например, такой, при котором информация записывается в журнал событий Windows, либо с применением SMS-уведомлений или уведомлений по электронной почте), то нам потребуется всего лишь передать иную реализацию
интерфейса
Как видите, интерфейсы – очень сильный инструмент, но они привносят в приложение дополнительный уровень абстракции, а, значит, и лишнюю сложность. В борьбе со сложностью могут помочь контейнеры, но, как будет показано в следующем разделе, приходится регистрировать все ваши интерфейсы, а при работе с конкретными типами это делать не требуется.
Существует так называемая «обфускация при помощи абстракции», которая, в сущности, сводится к переусложнению проекта ради реализации всех этих различных уровней. Если хотите, то можете обойтись и без всех этих интерфейсов, если только нет конкретных причин, по которым они вам нужны. Вообще не следует создавать такой интерфейс, у которого будет всего одна реализация.
Пример, прорабатываемый в этой статье, довольно прост: мы имеем дело с единственным классом, у которого всего одна зависимость. Теперь предположим, что у нас множество зависимостей, и каждая из них связана с другими зависимостями. Даже при работе над умеренно сложными проектами вполне вероятно, что вам придется иметь дело с такими ситуациями, и будет не просто держать в уме, что нужно создать все эти классы, а также помнить, какой из них от какого зависит – особенно, если вы решили пользоваться интерфейсами.
Знакомьтесь с контейнером для внедрения зависимостей. Он упрощает вам жизнь, но принцип работы такого контейнера может показаться весьма запутанным, особенно, когда вы только начинаете его осваивать. На первый взгляд эта возможность может отдавать некоторой магией.
В данном примере мы воспользуемся контейнером от Unity, но на выбор есть и много других, назову лишь наиболее популярные: Castle Windsor, Ninject. С функциональной точки зрения эти контейнеры практически не отличаются. Разница может быть заметна на уровне синтаксиса и стиля, но, в конечном итоге, все сводится к вашим персональным предпочтениям и опыту разработки (а также к тому, что предписывается в вашей компании!).
Давайте подробно разберем пример с использованием Unity: я постараюсь объяснить, что здесь происходит.
Первым делом вам потребуется добавить ссылку на Unity. К счастью, для этого существует пакет Nuget, поэтому щелкните правой кнопкой мыши по вашему проекту в Visual Studio и выберите Manage Nuget Packages:
Найдите и установите пакет Unity, ориентируйтесь на проект Unity Container:
Итак, мы готовы. Измените метод
Program.cs
Опять же, менять требуется только те строки, которые отмечены. При этом, нам не придется ничего менять ни в классе Calculator, ни в одном из логгеров, ни в их интерфейсе: теперь они все внедряются во время исполнения, поэтому, если нам понадобится иной логгер, то мы должны будем всего лишь изменить конкретный класс, регистрируемый для
При первом запуске этого кода вы можете столкнуться с такой ошибкой:
Вероятно, это одна из причуд с той версией пакета Unity, которая была актуальна на момент написания этой статьи. Надеюсь, что у вас все пройдет гладко.
Все дело в том, что при установке Unity также устанавливается неверная версия другого пакета,
Теперь, когда этот проект работает, что именно он делает? Когда вы впервые пишете код таким образом, тянет предположить, что здесь замешана какая-то магия. Сейчас я расскажу, что мы здесь видим на самом деле и, надеюсь, это будет сеанс магии с разоблачением, поэтому далее вы не будете остерегаться работы с контейнерами.
Все начинается со строки
Обратите внимание: если бы у
Также обратите внимание, что в пункте 2 разработчикам не приходится явно регистрировать конкретные типы. При желании это можно сделать, например, чтобы изменить жизненный цикл либо передать производные классы и т.д. Но это еще одна причина избегать создания интерфейсов, пока в них нет очевидной нужды.
Вот и все. Ничего таинственного и особо мистического.
Стоит отметить, что контейнеры позволяют делать еще некоторые вещи, которые было бы довольно сложно (но не невозможно) реализовать самостоятельно. Среди таких вещей – управление жизненным циклом и внедрение методов и свойств. Обсуждение этих тем выходит за рамки данной статьи, поскольку, маловероятно, что такие техники понадобятся начинающим. Но я рекомендую вам, освоившись с темой, внимательно почитать документацию, чтобы понимать, какие еще возможности существуют.
Если вы хотите сами поэкспериментировать с примерами, приведенными в этой статье: смело клонируйте с Гитхаба репозиторий, в котором они выложены github.com/GrahamDo/OfferZenDiTutorial.git. Там семь веток, по одной на каждую рассмотренную нами итерацию.
У нас готовится к выходу второе издание легендарной книги Марка Симана, «Внедрение зависимостей в .NET»
Поэтому сегодня мы решили кратко освежить тему внедрения зависимостей для специалистов по .NET и C# и предлагаем перевод статьи Грэма Даунса, где эта парадигма рассматривается в контексте инверсии управления (IoC) и использования контейнеров.
Большинству программистов, пожалуй, известен феномен «Внедрение зависимостей» (Dependency Injection), но не всем понятно, какой смысл в него вкладывается. Вероятно, вам приходилось иметь дело с интерфейсами и контейнерами, и иногда работа с ними приводила вас в тупик. С другой стороны, вы, возможно, только что-то слышали о внедрении зависимостей, и открыли эту статью, так как хотите лучше разобраться в их сути. В этой статье я покажу, насколько проста концепция внедрения зависимостей, и какой мощной она может быть на самом деле.
Внедрение зависимостей — это самодостаточный подход, который можно использовать сам по себе. С другой стороны, этот подход можно применять и вместе с интерфейсами, и с контейнерами для внедрения зависимостей/инверсии управления (DI/IoC). Применяя внедрение зависимостей в таком контексте, можно столкнуться с некоторой путаницей, которую поначалу испытывал и я.
На протяжении всей карьеры (я специализируюсь на разработке в Net/C#), я привык использовать внедрение зависимостей в его чистейшей форме. При этом я реализовывал DI, вообще не прибегая ни к контейнерам, ни к инверсии управления. Все изменилось совсем недавно, когда мне поставили задачу, в которой без использования контейнеров было не обойтись. Тогда я крепко усомнился во всем, что знал ранее.
Поработав в таком стиле несколько недель, я осознал, что контейнеры и интерфейсы не осложняют внедрение зависимостей, а, наоборот, расширяют возможности этой парадигмы.
(Здесь важно отметить: интерфейсы и контейнеры используются только в контексте внедрения зависимостей. Внедрение зависимостей можно реализовать и без интерфейсов/контейнеров, но, в сущности, единственное назначение интерфейсов или контейнеров – облегчить внедрение зависимостей).
В этой статье будет показано, как делается внедрение зависимостей, как с интерфейсами и контейнерами, так и без них. Надеюсь, что, дочитав ее, вы будете четко представлять принцип работы внедрения зависимостей и сможете принимать информированные решения о том, когда и где прибегать к использованию интерфейсов и контейнеров при внедрении зависимостей.
Подготовка
Чтобы лучше понять внедрение зависимостей в их чистейшей форме, давайте разберем пример приложения, написанного на C#.
Для начала отметим, что такое приложение можно было бы написать без какого-либо внедрения зависимостей. Затем мы введем в него внедрение зависимостей, добавив простую возможность логирования.
По ходу работы вы увидите, как требования, предъявляемые к логированию, постепенно усложняются, и мы удовлетворяем эти требования, используя внедрение зависимостей; при этом зона ответственности класса
Calculator
сводится к минимуму. Внедрение зависимостей также избавит нас от необходимости видоизменять класс Calculator
всякий раз, когда мы захотим поменять устройство логирования. Приложение
Рассмотрим следующий код. Он написан для простого приложения-калькулятора, принимающего два числа, оператор и выводящего результат. (Это простое рабочее приложение для командной строки, поэтому вам не составит труда воспроизвести его как C# Console Application в Visual Studio и вставить туда код, если вы хотите следить за развитием примера. Все должно работать без проблем.)
У нас есть класс
Calculator
и основной класс Program
, использующий его.Program.cs:
using System;
using System.Linq;
namespace OfferZenDiTutorial
{
class Program
{
static void Main(string[] args)
{
var number1 = GetNumber("Enter the first number: > ");
var number2 = GetNumber("Enter the second number: > ");
var operation = GetOperator();
var calc = new Calculator();
var result = GetResult(calc, number1, number2, operation);
Console.WriteLine($"{number1} {operation} {number2} = {result}");
Console.Write("Press any key to continue...");
Console.ReadKey();
}
private static float GetNumber(string message)
{
var isValid = false;
while (!isValid)
{
Console.Write(message);
var input = Console.ReadLine();
isValid = float.TryParse(input, out var number);
if (isValid)
return number;
Console.WriteLine("Please enter a valid number. Press ^C to quit.");
}
return -1;
}
private static char GetOperator()
{
var isValid = false;
while (!isValid)
{
Console.Write("Please type the operator (/*+-) > ");
var input = Console.ReadKey();
Console.WriteLine();
var operation = input.KeyChar;
if ("/*+-".Contains(operation))
{
isValid = true;
return operation;
}
Console.WriteLine("Please enter a valid operator (/, *, +, or -). " +
"Press ^C to quit.");
}
return ' ';
}
private static float GetResult(Calculator calc, float number1, float number2,
char operation)
{
switch (operation)
{
case '/': return calc.Divide(number1, number2);
case '*': return calc.Multiply(number1, number2);
case '+': return calc.Add(number1, number2);
case '-': return calc.Subtract(number1, number2);
default:
// Такого произойти не должно, если с предыдущими валидациями все было нормально
throw new InvalidOperationException("Invalid operation passed: " +
operation);
}
}
}
}
Главная программа запускается, запрашивает у пользователя два числа и оператор, а затем вызывает класс
Calculator
для выполнения простой арифметической операции над этими числами. Затем выводит результат операции. Вот класс Calculator
.Calculator.cs:
namespace OfferZenDiTutorial
{
public class Calculator
{
public float Divide(float number1, float number2)
{
return number1 / number2;
}
public float Multiply(float number1, float number2)
{
return number1 * number2;
}
public float Add(float number1, float number2)
{
return number1 + number2;
}
public float Subtract(float number1, float number2)
{
return number1 - number2;
}
}
}
Логирование
Приложение работает отлично, но только представьте: вашему начальнику вздумалось, что теперь все операции должны логироваться в файл на диске, чтобы было видно, чем люди занимаются.
Кажется, что не так это и сложно, верно? Берете и добавляете инструкции, в соответствии с которыми все операции, производимые в
Calculator
, должны заноситься в текстовый файл. Вот как теперь выглядит ваш Calculator
:Calculator.cs:
using System.IO;
namespace OfferZenDiTutorial
{
public class Calculator
{
private const string FileName = "Calculator.log";
public float Divide(float number1, float number2)
{
File.WriteAllText(FileName, $"Running {number1} / {number2}");
return number1 / number2;
}
public float Multiply(float number1, float number2)
{
File.WriteAllText(FileName, $"Running {number1} * {number2}");
return number1 * number2;
}
public float Add(float number1, float number2)
{
File.WriteAllText(FileName, $"Running {number1} + {number2}");
return number1 + number2;
}
public float Subtract(float number1, float number2)
{
File.WriteAllText(FileName, $"Running {number1} - {number2}");
return number1 - number2;
}
}
}
Прекрасно работает. Всякий раз, когда в
Calculator
что-либо происходит, он записывает это в файл Calculator.log
, расположенный в той же директории, откуда он запускается.Но, возможен вопрос: а в самом ли деле уместно, чтобы класс Calculator отвечал за запись в текстовый файл?
Класс FileLogger
Нет. Разумеется, это не его зона ответственности. Поэтому, чтобы не нарушался принцип единственной ответственности, все, что касается логирования информации, должно происходить в файле логов, и для этого требуется написать отдельный самодостаточный класс. Давайте этим и займемся.
Первым делом создаем совершенно новый класс, назовем его
FileLogger
. Вот как он будет выглядеть.FileLogger.csh:
using System;
using System.IO;
namespace OfferZenDiTutorial
{
public class FileLogger
{
private const string FileName = "Calculator.log";
private readonly string _newLine = Environment.NewLine;
public void WriteLine(string message)
{
File.AppendAllText(FileName, $"{message}{_newLine}");
}
}
}
Теперь все, что касается создания файла логов и записи информации в него обрабатывается в этом классе. Дополнительно получаем и одну приятную мелочь: что бы ни потреблял этот класс, не требуется ставить между отдельными записями пустые строки. Записи должны просто вызывать наш метод
WriteLine
, а все остальное мы берем на себя. Разве не круто? Чтобы использовать класс, нам нужен объект, который его инстанцирует. Давайте решим эту проблему внутри класса
Calculator
. Заменим содержимое класса Calculator.cs
следующим:Calculator.cs:
namespace OfferZenDiTutorial
{
public class Calculator
{
private readonly FileLogger _logger;
public Calculator()
{
_logger = new FileLogger();
}
public float Divide(float number1, float number2)
{
_logger.WriteLine($"Running {number1} / {number2}");
return number1 / number2;
}
public float Multiply(float number1, float number2)
{
_logger.WriteLine($"Running {number1} * {number2}");
return number1 * number2;
}
public float Add(float number1, float number2)
{
_logger.WriteLine($"Running {number1} + {number2}");
return number1 + number2;
}
public float Subtract(float number1, float number2)
{
_logger.WriteLine($"Running {number1} - {number2}");
return number1 - number2;
}
}
}
Итак, теперь нашему калькулятору не важно, как именно новый логгер записывает информацию в файл, или где находится этот файл, и происходит ли вообще запись в файл. Однако, все равно существует одна проблема: можем ли мы вообще рассчитывать на то, что класс
Calculator
будет знать, как создается логгер? Внедрение зависимости
Очевидно, ответ на последний вопрос отрицательный!
Именно здесь, уважаемый читатель, в дело вступает внедрение зависимости. Давайте изменим конструктор нашего класса
Calculator
:Calculator.cs:
public Calculator(FileLogger logger)
{
_logger = logger;
}
Вот и все. Больше в классе ничего не меняется.
Внедрение зависимостей – это элемент более крупной темы под названием «Инверсия управления», но ее подробное рассмотрение выходит за рамки этой статьи.
В данном случае вам всего лишь требуется знать, что мы инвертируем управление классом логгера, либо, выражаясь метафорически, делегируем кому-то проблему создания файла
FileLogger
, внедряя экземпляр FileLogger
в наш калькулятор, а не рассчитывая, что класс Calculator
сам будет знать, как его создать.Итак, чья же это ответственность?
Как раз того, кто инстанцирует класс
Calculator
. В нашем случае это – основная программа.Чтобы это продемонстрировать, изменим метод Main в нашем классе Program.cs следующим образом:
Program.cs
static void Main(string[] args)
{
var number1 = GetNumber("Enter the first number: > ");
var number2 = GetNumber("Enter the second number: > ");
var operation = GetOperator();
// Следующие две строки изменены
var logger = new FileLogger();
var calc = new Calculator(logger);
var result = GetResult(calc, number1, number2, operation);
Console.WriteLine($"{number1} {operation} {number2} = {result}");
Console.Write("Press any key to continue...");
Console.ReadKey();
}
Таким образом, требуется изменить всего две строки. Мы не рассчитываем, что класс
Calculator
инстанцирует FileLogger
, это за него сделает Main
, а затем передаст ему результат. В сущности, это и есть внедрение зависимостей. Не нужны ни интерфейсы, ни контейнеры для инверсии управления, ни что-либо подобное. В принципе, если вам доводилось выполнять что-либо подобное, то вы имели дело с внедрением зависимостей. Круто, правда?
Расширение возможностей: сделаем другой логгер
Несмотря на вышесказанное, у интерфейсов есть свое место, и по-настоящему они раскрываются именно в связке с Внедрением Зависимостей.
Допустим, у вас есть клиент, с точки зрения которого логирование каждого вызова к
Calculator
– пустая трата времени и дискового пространства, и лучше вообще ничего не логировать. Как вы считаете, придется ли ради этого делать изменения внутри
Calculator
, что потенциально потребует перекомпилировать и перераспределить ту сборку, в которой он находится? Вот здесь нам и пригодятся интерфейсы.
Давайте напишем интерфейс. Назовем его
ILogger
, поскольку его реализацией будет заниматься наш класс FileLogger
.ILogger.cs
namespace OfferZenDiTutorial
{
public interface ILogger
{
void WriteLine(string message);
}
}
Как видите, он определяет единственный метод:
WriteLine
, реализованный FileLogger
. Сделаем еще шаг и формализуем эти отношения, сделав так, чтобы этот класс официально реализовывал наш новый интерфейс:FileLogger.cs
public class FileLogger : ILogger
Это единственное изменение, которое мы внесем в этот файл. Все остальное будет как прежде.
Итак, отношение мы определили – что нам теперь с ним делать?
Для начала изменим класс Calculator таким образом, чтобы он использовал интерфейс
ILogger
, а не конкретную реализацию FileLogger
:Calculator.cs
private readonly ILogger _logger;
public Calculator(ILogger logger)
{
_logger = logger;
}
На данном этапе код по-прежнему компилируется и выполняется без всяких проблем. Мы передаем в него
FileLogger
из главного метода программы, того, который реализует ILogger
. Единственное отличие заключается в том, что Calculator
не просто не требуется знать, как создавать FileLogger
, но и даже логгер какого рода ему выдается. Поскольку все, что бы вы ни получили, реализует интерфейс
ILogger
(и, следовательно, имеет метод WriteLine
), с практическим использованием проблем не возникает.Теперь давайте добавим еще одну реализацию интерфейса
ILogger
. Это будет класс, который ничего не делает при вызове метода WriteLine
. Мы назовем его NullLogger
, и вот как он выглядит:NullLogger.cs
namespace OfferZenDiTutorial
{
public class NullLogger : ILogger
{
public void WriteLine(string message)
{
// Ничего не делаем в этой реализации
}
}
}
На этот раз нам вообще ничего не потребуется менять в классе
Calculator
, если мы соберемся использовать новый NullLogger
, поскольку тот уже принимает что угодно, реализующее интерфейс ILogger
.Нам потребуется изменить только лишь метод Main в нашем файле Program.cs, чтобы передать в него иную реализацию. Давайте этим и займемся, чтобы метод
Main
принял следующий вид:Program.cs
static void Main(string[] args)
{
var number1 = GetNumber("Enter the first number: > ");
var number2 = GetNumber("Enter the second number: > ");
var operation = GetOperator();
var logger = new NullLogger(); // Эту строку нужно изменить
var calc = new Calculator(logger);
var result = GetResult(calc, number1, number2, operation);
Console.WriteLine($"{number1} {operation} {number2} = {result}");
Console.Write("Press any key to continue...");
Console.ReadKey();
}
Опять же, изменить нужно только ту строку, которую я откомментировал. Если мы хотим использовать иной механизм логирования (например, такой, при котором информация записывается в журнал событий Windows, либо с применением SMS-уведомлений или уведомлений по электронной почте), то нам потребуется всего лишь передать иную реализацию
интерфейса
ILogger
.Небольшая оговорка об интерфейсах
Как видите, интерфейсы – очень сильный инструмент, но они привносят в приложение дополнительный уровень абстракции, а, значит, и лишнюю сложность. В борьбе со сложностью могут помочь контейнеры, но, как будет показано в следующем разделе, приходится регистрировать все ваши интерфейсы, а при работе с конкретными типами это делать не требуется.
Существует так называемая «обфускация при помощи абстракции», которая, в сущности, сводится к переусложнению проекта ради реализации всех этих различных уровней. Если хотите, то можете обойтись и без всех этих интерфейсов, если только нет конкретных причин, по которым они вам нужны. Вообще не следует создавать такой интерфейс, у которого будет всего одна реализация.
Контейнеры для внедрения зависимостей
Пример, прорабатываемый в этой статье, довольно прост: мы имеем дело с единственным классом, у которого всего одна зависимость. Теперь предположим, что у нас множество зависимостей, и каждая из них связана с другими зависимостями. Даже при работе над умеренно сложными проектами вполне вероятно, что вам придется иметь дело с такими ситуациями, и будет не просто держать в уме, что нужно создать все эти классы, а также помнить, какой из них от какого зависит – особенно, если вы решили пользоваться интерфейсами.
Знакомьтесь с контейнером для внедрения зависимостей. Он упрощает вам жизнь, но принцип работы такого контейнера может показаться весьма запутанным, особенно, когда вы только начинаете его осваивать. На первый взгляд эта возможность может отдавать некоторой магией.
В данном примере мы воспользуемся контейнером от Unity, но на выбор есть и много других, назову лишь наиболее популярные: Castle Windsor, Ninject. С функциональной точки зрения эти контейнеры практически не отличаются. Разница может быть заметна на уровне синтаксиса и стиля, но, в конечном итоге, все сводится к вашим персональным предпочтениям и опыту разработки (а также к тому, что предписывается в вашей компании!).
Давайте подробно разберем пример с использованием Unity: я постараюсь объяснить, что здесь происходит.
Первым делом вам потребуется добавить ссылку на Unity. К счастью, для этого существует пакет Nuget, поэтому щелкните правой кнопкой мыши по вашему проекту в Visual Studio и выберите Manage Nuget Packages:
Найдите и установите пакет Unity, ориентируйтесь на проект Unity Container:
Итак, мы готовы. Измените метод
Main
файла Program.cs
вот так:Program.cs
static void Main(string[] args)
{
var number1 = GetNumber("Enter the first number: > ");
var number2 = GetNumber("Enter the second number: > ");
var operation = GetOperator();
// Следующие три строки необходимо изменить
var container = new UnityContainer();
container.RegisterType<ILogger, NullLogger>();
var calc = container.Resolve<Calculator>();
var result = GetResult(calc, number1, number2, operation);
Console.WriteLine($"{number1} {operation} {number2} = {result}");
Console.Write("Press any key to continue...");
Console.ReadKey();
}
Опять же, менять требуется только те строки, которые отмечены. При этом, нам не придется ничего менять ни в классе Calculator, ни в одном из логгеров, ни в их интерфейсе: теперь они все внедряются во время исполнения, поэтому, если нам понадобится иной логгер, то мы должны будем всего лишь изменить конкретный класс, регистрируемый для
ILogger
.При первом запуске этого кода вы можете столкнуться с такой ошибкой:
Вероятно, это одна из причуд с той версией пакета Unity, которая была актуальна на момент написания этой статьи. Надеюсь, что у вас все пройдет гладко.
Все дело в том, что при установке Unity также устанавливается неверная версия другого пакета,
System.Runtime.CompilerServices.Unsafe
. Если вы получаете такую ошибку, то должны вернуться к менеджеру пакетов Nuget, найти этот пакет под вкладкой “Installed” и обновить его до новейшей стабильной версии:Теперь, когда этот проект работает, что именно он делает? Когда вы впервые пишете код таким образом, тянет предположить, что здесь замешана какая-то магия. Сейчас я расскажу, что мы здесь видим на самом деле и, надеюсь, это будет сеанс магии с разоблачением, поэтому далее вы не будете остерегаться работы с контейнерами.
Все начинается со строки
var calc = container.Resolve<Calculator>();
, поэтому именно отсюда я изложу смысл этого кода в форме «диалога контейнера с самим собой»: о чем он «думает», когда видит эту инструкцию.- “Мне задано разрешить что-то под названием
Calculator
. Я знаю, что это такое?” - “Вижу, в актуальном дереве процессов есть класс под названием
Calculator
. Это конкретный тип, значит, у него всего лишь одна реализация. Просто создам экземпляр этого класса. Как выглядят конструкторы?” - “Хм, а конструктор всего один, и принимает он что-то под названием
ILogger
. Я знаю, что это такое?” - “Нашел, но это же интерфейс. Мне вообще сообщалось, как его разрешать?”
- “Да, сообщалось! В предыдущей строке сказано, что, всякий раз, когда мне требуется разрешить
ILogger
, я должен передать экземпляр классаNullLogger
.” - “Окей, значит тут есть
NullLogger
. У него непараметризованный конструктор. Просто создам экземпляр.” - “Передам этот экземпляр конструктору класса
Calculator
, а затем верну этот экземпляр к var calc.”
Обратите внимание: если бы у
NullLogger
был конструктор, который запрашивал бы дополнительные типы, то конструктор просто повторил бы для них все шаги, начиная с 3. В принципе, он просматривает все типы и пытается автоматически разрешить все найденные типы в конкретные экземпляры.Также обратите внимание, что в пункте 2 разработчикам не приходится явно регистрировать конкретные типы. При желании это можно сделать, например, чтобы изменить жизненный цикл либо передать производные классы и т.д. Но это еще одна причина избегать создания интерфейсов, пока в них нет очевидной нужды.
Вот и все. Ничего таинственного и особо мистического.
Другие возможности
Стоит отметить, что контейнеры позволяют делать еще некоторые вещи, которые было бы довольно сложно (но не невозможно) реализовать самостоятельно. Среди таких вещей – управление жизненным циклом и внедрение методов и свойств. Обсуждение этих тем выходит за рамки данной статьи, поскольку, маловероятно, что такие техники понадобятся начинающим. Но я рекомендую вам, освоившись с темой, внимательно почитать документацию, чтобы понимать, какие еще возможности существуют.
Если вы хотите сами поэкспериментировать с примерами, приведенными в этой статье: смело клонируйте с Гитхаба репозиторий, в котором они выложены github.com/GrahamDo/OfferZenDiTutorial.git. Там семь веток, по одной на каждую рассмотренную нами итерацию.
pawlo16
Пример крайне неудачный. FileLogger продвигает в явном виде антипаттерн, противоречащий 12 factor apps. А именно — логгирование должно производится только в консоль и никуда более.
Во-2, у вас один интерфейс и у него одна зависимость. Для этого нужен DI контейнер, серьёзно?
ferocactus
Как должен логировать в консоль, например, Windows Service?
pawlo16
Точно также, только при запуске службы перенаправить stdout из dev/null в системный журнал event log
Я собственно ни разу не против DI. И в контексте DI считаю вполне нормальным использовать ILogger, предоставляющий для ICalculator возможности логгирования.
ferocactus
Как именно можно перенаправить Windows Service StdOut в Event log при запуске службы, следуя принципам двенадцати факторов?
И почему кстати Event log предпочтительнее иных средств, например, тестовых файлов?
pawlo16
Элементарно. Запускать бинарник из процесса, который стартует (инсталирует, суспендит, закрывает) службу и выполняет в ней бинарник, при этом перехватывает stdout бинарника и пишет его в системный журнал
Я не говорил, что он предпочтительнее везде и всегда. Иногда в зависимости от задачи и обстоятельств предпочтительнее может быть что угодно — dev/null, консоль, файл, ELK. Но как правило для windows системный журнал — хороший выбор, потому что это стандартный и рекомендованный вендором системный компонент централизованного логгирования + фронтенд.
ferocactus
Это совсем не элементарно и потому вызывает сомнения в целесообразности.
Какую проблему решит такой подход по сравнению с изоляцией логики логирования классическими средствами модульности (абстракции, библиотеки)?
В проектах какой компании можно встретить реализацию такого подхода к Windows Service? Где-то можно почитать об опыте его использования? Ведь надёжное межпроцессное взаимодействие штука не дешёвая.
pawlo16
Код я привёл в другой ветке, он тривиальный. Там требуется ещё garcefull shutdown, но я понятия не имею как это реализуется в windows без сигналов unix.
Такой подход позволяет:
— при разработке и в рабочем окружении не используются сторонние сервисы и зависимости, связанные с логгированием
— разработчику не нужно заниматься маршрутизацией и хранением stdout, это делегируется внешним программам
— поскольку поток событий пишется в stdout гарантированно без буферизации, кеширования и прочих вещей, которые могут создавать накладные расходы, у разработчика есть гарантия, что логгирование не станет узким местом, не создаст доп. нагрузку на GC и в случае C# на асинхронный пул
«надёжное межпроцессное взаимодействие штука не дешёвая.» — это верно, но не в случае перехвата дочернего stdout родительским процессом, в этом случае всё действительно просто
«В проектах какой компании можно встретить реализацию такого подхода к Windows Service?» — не могу сказать. Много ли компаний вообще используют windows service для бизнес кода? не уверен, не видел таких ни разу. В основном для инфраструктурного и системного насколько я могу судить. Разумеется в системном и инфраструктурном программировании 12F подходит не всегда, там много старых гайдланов и различных древних хакерских практик, которые до сих пор в ходу. понятно, что при разработке и работе условного драйвера для видюхи нет практически ничего из того, что предполагает 12F
anonymous
В 12 factor apps речь идёт не о консоли, конечно. Постулируется, что «Приложение двенадцати факторов никогда не занимается маршрутизацией и хранением своего потока вывода… Вместо этого каждый выполняющийся процесс записывает свой поток событий без буферизации в стандартный вывод stdout.»
Если с первой частью я согласен, в пределах её применимости, то вторая, на мой взгляд, ошибочна. В более-менее серьёзном (веб-) сервисе логи структурированные. Помимо текстового сообщения, каждая запись несет в себе дополнительные данные и метаданные. Простым выводом в stdout тут не отделаешься. Нужен промежуточный агент со своим интерфейсом, позволяющий приложению записать структурированное событие и передающий его среде исполнения.
pawlo16
Ну понятно же, что консоль — это и есть stdout в большинстве случаев.
== вторая, на мой взгляд, ошибочна.
== Простым выводом в stdout тут не отделаешься.
Текстовое сообщение в данном случае — это json с «дополнительными данными и метаданными», для которого ни какие специальные агенты не требуются, а достаточно простой обёртки над выводом в stdut. Инфраструктурное ПО берёт сообщение из stdout и перенаправляет в logstash (или Loki). Смысл постулата — приложение никогда не должно само писать в logstash, loki, или боже упаси напрямую в elasticsearch.
anonymous
Нет, консоль — это не stdout. Хотя, действительно, чаще всего stdout перенаправляется на консоль, отождествлять их — это грубая ошибка. Консоль это инфраструктура, включающая в себя экранный буфер, c произвольным доступом, аттрибутами символов и буфер ввода, имеющие свой собственный API. Для приложения выводить на консоль и выводить в stdout — это две большие разницы.
По поводу «простой обёртки над выводом» — с фактом наличия этой обёртки мы уже определились. Если возражений о том, что у этой обёртки есть набор публичных методов aka API, нет, то у нас получается тот самый логгер, только, скажем, не FileLogger, а StdoutLogger. А дальше, до введения собственно интерфейса ILogger остаётся один шаг.
Почему нужен агент? — в реальности, среда выполнения и инфраструктура журналирования не тождественны, а в случае с Windows service, приведённом выше, такая инфраструктура журналирования, как описывается в манифесте 12 factor app, просто отсутствует. Вот мы и получаем, с одной стороны — ILogger, с другой — необходимость в промежуточном агенте. Контейнер зависимостей, динамично выстраеваемый под конкретную среду выполнения, хотя и не без серьёзных недостатков, имеет право на существование, как решение этой потребности. Правда, лично мне для .Net больше нравится Apache Log4Net.
pawlo16
Это ни какая не грубая ошибка, а нормальное отождествление наиболее распространённого случая. По умолчанию stdout для разработчика — это консоль практически всегда.
А я изначально ничего и не имел против ILoggerЭлементарно создаётся на пальцах. Докер. nssm.cc Наверняка есть и что-то ещё, просто вы не в курсе
Нет. Логгер нужно передавать в каждую функцию/метод в качестве параметра, чтобы получить в сообщении предопределённый сверху стека вызовов набор ключ-значение
anonymous
Если разработчик оговорился — это ещё можно понять, но если такое отождествление нормально, это явные проблемы в понимании архитектуры.
Да, конечно, давайте посадим Windows service в докер-контейнер, со всеми его накладными расходами, и побъёмся над тем, чтобы обеспечить нужное поведение. Есть такая штука — целесообразность применения — наверное, Вы просто не в курсе.
А теперь давайте взглянем с точки зрения манифеста 12. Среда исполнения Windows service — это таки Windows, со своим, особым взаимодействием с сервисами. То, что Вы предлагаете — это вкрутить ещё одну прослойку между сервисом и средой исполнения так, чтобы приложение можно было уложить в прокрустово ложе манифеста. Ну будет у Вас Docker этим самым агентом. Всё равно ничего не изменится.
Я так понимаю, это желание выдать частное, и спорное архитектурное решение за общий случай? Какие, простите, ключи и значения необходимо передавать в каждый метод в стеке, и почему их обязательно необходимо оборачивать в логгер?
pawlo16
anonymous
Это публичный форум, я высказываю свое мнение, и имею на это право. Вы имеете полное право с ним не соглашаться, поэтому ни о каком навязывании речи не идёт.
Это, очевидно, верно, и, именно поэтому потребовалось втащить в дискуссию Docker. И Вы же с этим согласны: «Какая ниша — такая и инфраструктура, такой и уровень соответствия современным рекомендациям».
Спасибо за диагноз, так сказать, по фотографии, давайте Вы уж тогда поясните критерии целесообразности вот этого: «Запускать бинарник из процесса, который стартует службу и выполняет в ней бинарник, при этом перехватывает stdout бинарника и пишет его в системный журнал», и вот этого в контексте Windows service: Элементарно создаётся на пальцах. Докер. nssm.cc .
Научиться чему-то новому — всегда полезно, серьёзно. Если это можно сделать в 15-20 строк кода — с интересном изучу. Опубликуете?
Это уход от ответа. Если Вы разбираетесь в этом хорошо, совершенно несложно привести конкретный пример.
Давайте перейдём от диагнозов по фотографии к конкретным примерам.
Кстати, логгер у нас уже хранит состояние, плюс поддерживает многопоточную запись. Уже есть сомнения в возможности " достаточно простой обёртки над выводом в stdut"
Ну и напоследок добавлю: да, Windows services это очень старая инфраструктура. Ну а Azure Application Services, Azure Functions, etc.? Тоже объявим дремучими и маргинальными?
pawlo16
anonymous
Давайте Вы будете называть поток вывода как Вам удобно, и не будете мне затыкать рот, когда я высказываю по этому поводу своё мнение?
Это, очевидно, не так. Вы думаете исключительно о том, _как_ логгировать в поток вывода, и, именно поэтому Вам приходится изобретать способ обернуть своё консольное приложение так, чтобы оно выглядело как Windows service, наплевав при этом на производительность, отказоустойчивость, простоту обслуживания. И Вы прекрасно иллюстрируете, почему постулирование записи в поток вывода — это ошибка.
Зона ответственности приложения заканчивается на вызове методов ILogger.Log*whatever*. Что происходит с логами дальше — это уже не забота разработчика.
Запись логов в поток вывода не является необходимым условием для этого. Более того, Вам не наплевать — Вам приходится выкручиваться, чтобы приспособить среду выполнения к бесмысленному требованию.
Хорошо, всё понятно. Вы — не можете.
Спасибо, ссылка не имеет абсолютно никакого отношения к вопросу, и примера у Вас нет. Кстати, Вы заметили, как ловко автор заметает под ковёр вопрос конвертации даты? :)
Безусловно, правильная реализация многопоточности — это уже непростой код, а шапкозакидательство не свидетельство квалификации. Обычно, наоборот.
Это второй пример дискредитации фактов, которые не укладываются в стройную теорию манифеста. Ну и не надо тогда претендовать на всеобъемлющесть, и пропихивать guidelines для частного случая везде, где только можно.
pawlo16
play.golang.org/p/yreNlk9bpoM Не можете разработать код, который пишет в консоль, и полагаете, что другие не могут? Я пишу всё в консоль и в принципе не задумываюсь о реализации логгирования. Каким образом записи из консоли попадают в итоге в журнал — мне плевать, это решается в момент запуска приложения с помощью инфраструктурного ПО. Если по каким то причинам инфраструктурного ПО не хватает (чего не бывает практически никогда), то я или другой разработчик его создают, ни каких проблем с этим нет. Вы воюете с мельницей, поскольку такой подход к логгированию используется чуть более чем везде. Докер, systemd, облачные сервисы amazon, google cloud, heroku — всё это умеет работать с консольным выводом и полностью соответствует всем рекомендациям 12f, и мне не нужно ничего изобретать. Я не пишу windows service. Я пишу бизнес фичи в соответствии с гайдлайнами, частью которых является консоль как единственный журнал. Потребуется запускать бизнес фичи в виде windows service — ни каких проблем, это бесплатно. Я в 90% случае даже представления не имею, на какой платформе будет запускаться моё приложение и в какой источник оно будет логгировать. Производительность и безопасность коня в вакууме — смешно. Попробуйте аргументировать. Да, именно так у меня и есть. Благодаря следованию принципам 12f мои приложения не содержат плотформенных зависимостей, связанных с логгированием. А вот у вас если приложение использует event log windows для логгирования, то оно ни при каких обстоятельствах не будет работать на google cloude. И наоборот — если логгирует в google cloude, то не запустится под windows.
ссылка про структурное логгирование не имеет ни какого отношения к структурному логгированию? ну ну Скорее свидетельсво узковатого кругозора у вас. Вы наверное имеете ввиду, что одновременно правильная и простая реализация многопоточности — это не про C#. На C# нельзя просто залочить мьютексом shared mutable state и одидать что это заработает. Ну так я с этим и не спорил. К счастью, я пишу на Go, и мне не сотавляет труда поставить мьютекс там, где есть доступ к стейту, и радоваться жизни. Понятия не имею про azure, но и ничего не имею против неё — мне она фиолетова. Но вы опять не разобрались в вопросе. Azure умеет в докер, следовательно полностью соответсвует 12f в части логгирования. Скорее всего умеет и без докера (но кто в здравом уме будет деплоить C# без докера), просто вы не в курсе (а мне не интересно)
anonymous
Приветствую усилия по написанию примера. Получилось, правда, заметно больше, чем 15-20 строк, никаких структурированных логов (да и вообще всё валится в одну кучу), и это мы его ещё не тестировали. При этом мы потеряли часть функционала Windows Service — просто, потому, что консольное приложение так не может, с безопасным завершением сервиса по команде остановки есть вопросы, куда пишутся логи самой обёртки тоже не очень понятно (ок, это может быть моё незнание golang, и они на самом деле тоже пишутся в eventLog, но я в этом не уверен). Это так, на вскидку. И всё это лишь для того, чтобы выводить логи в консоль. Не мучайтесь! Возьмите для C# Log4Net. Вы получите всё то же самое, только не нужно будет изголяться с обязательным пропусканием логов через стандартный вывод.
Как Вы там выше писали? — «Это _вы_ не можете, говорите за себя.» :) Перенаправить логи в стандартный вывод дело нехитрое, только бесмыссленное, при наличии более эффективных и надёжных способов.
Это иллюзия, ибо заказчику нужно полное работающее решение, а не бинарник, пишуший в консоль. Поэтому вот то, что Вы написали в примере — будет частью Вашей работы, и Вам придётся этот код писать, тестировать, и поддерживать.
Это просто смешно. Конечно же нет, он не используется везде. В области Вашей практики — возможно, но это не весь мир, и это не значит, что этот подход верный.
Принцип 12f имеет к этому весьма опосредованное отношение, интерфейсы не его авторы изобрели. Скажем так, он предлагает делать это через выхлопную трубу.
Мьютекс на каждую запись в логи в продакшн сервисе? Это Вы серьёзно, да? Ну и про С# конечно тоже повеселило, спасибо.
Как Вы там писали? — А! «Это _вы_ не разобрались, говорите за себя.». Azure это делеко не только Docker, и, к счастью, не страдает параноидальным желанием засунуть всё в стандартный вывод, за что ему и спасибо.
pawlo16
Усилий не потребовалось, всё тривиально. Для безопасного завершения процесса потребуется доп. протокол через stdin дочернего процесса например, поскольку windows не умеет в сигналы unix. Если не уверены, можете самостоятельно проделать простейшую гимнастику (go mod init; go run. prog.exe; см. системный журнал «my service») и убедиться, как это сделал я. Или смотреть код библиотек, например, этой — там всего лишь вызовы windows api
Структурное логгирование не имеет ни какого отношения к перенаправлению stdout. Оно реализуется библиотекой логгирования. В процессе разработки это просто вывод в консоль в формате logfmt — key=value, разделённые пробелами. В продакшене — json с добавлениями метки времени. Устанавливается в конфигурации запуска приложения.
«Принцип 12f имеет к этому весьма опосредованное отношение, интерфейсы не его авторы изобрели.» — интерфейсы и 12F — параллельные понятия.
«Мьютекс на каждую запись в логи в продакшн сервисе? Это Вы серьёзно, да? Ну и про С# конечно тоже повеселило, спасибо.» — ну а меня веселят ваши нервная реакция и синдром даннинга крюгера. В Go мьютексы бесплатны и не блокируют поток ос, поскольку все инструкции асинхронны. И да, защищать мьютексом в Go всё, что может использоваться конкурентно — это рекомендованные best practices
anonymous
Раз это windows service, то пример был скомпилирован и установлен как windows service.
Баг №1 — интуиция меня не обманула. Нет, логи самой обёртки в event log не пишутся. Только обёрнутного приложения. Что интересно, если запускать с консоли, то да, логи обёртки тоже попадают в event log. Но мы же сервис тестируем.
Баг №2 — если обёртка аварийно завершает работу, обёрнутое приложение продолжает работать, все логи уходят в никуда
Баг №3 — после Бага №2, при попытке перезапустить сервис, запускается второй экземпляр оборачиваемого приложения.
Баг №4 — если обёрнутое приложение завершается, обёртка-сервис продолжает работать. Сервиса уже нет, но никто ни сном, ни духом, и у Windows шанса перезапустить сервис или поднять предупреждение нет.
Баг №5 — Если сообщение длинное, то оно в event log не пишется. Ошибок о сохранении сообщения в логе тоже не пишется. Длинные сообщения уходят в никуда.
Баг №6 нам уже известен — при остановке сервиса обёрнутое приложение аварийно завершается, со всеми вытекающими.
Про отсутствие хотя бы катерогий сообщений в обёртке я уже молчу.
Эти ошибки я нашёл за 10 минут экспериментов, и, они отнюдь не мелочи, они могут обернуться финансовыми и репутационными потерями для заказчика.
Какими бы бесплатными не были мьютексы, какими бы асинхронными не были инструкции, тот код, что защищен мьютексом, всегда выполняется только в одном экземпляре, последовательно. А под мьютексом у Вас — операция ввода-вывода, недешевая, надо сказать, операция. Очевидно, что за одну единицу времени больше запросов, чем количество раз, которое может исполниться код под мьютексом, сервис исполнить не сможет. А учитывая, что чаще всего на один запрос пишется несколько сообщений, можно смело уменьшать число запросов ещё на порядок. И решение это не масштабируется. Это обычное бутылочное горлышко — ошибка новичка.
Ну и про парсить stdin в веб-сервисе — это уже полная бесмыслица и мартышкин труд.
pawlo16
«логи самой обёртки в event log не пишутся» — я и не собирался. зачем мне это делать? задача стояла перенаправить лог дочернего процесса а не основного
2 — да, я забыл вставить инструкцию, закрывающую запущенный сервис при выходе из main
3,4,6 — неуместные придирки. Естественно для демонстрационного примера обработка ошибок и перезапуск сервиса меня не интересуют
5 — ничего страшного и тоже в данном случае не важно.
Да, для продакшена я бы написал интеграционные тесты, проверяющие запуск, перезапуск, остановку, запись в сист. журнал и проч. — и что?
По поводу того, что запись логов в конечный журнал может тормозить — естественно может, спасибо кэп. По этому маршрутизаторы журналов, такие как promtail, Logplex, Fluent и logstash, кешируют и буферизирут свой ввод. И на приложении, из которого маршрутизатор получает ввод, эти тормоза -внезапно! — ни коим образом не отражаются. Ошибка новичка в данном случае — это святая уверенность в том, что если новичок не может что-то сделать быстро и просто, то это в принципе не было реализовано до него
«Ну и про парсить stdin в веб-сервисе — это уже полная бесмыслица и мартышкин труд.»
а само по себе использование служб виндовз в качестве веб сервисов — бесСмыслица (с двумя «с») и мартышкин труд.
anonymous
А куда они тогда пишутся? Ведь обёртка эти сообщения создаёт. Значит, замысел их выводить тоже был. Или они там для красоты? :)
А вот эта реплика — "Если не уверены, можете самостоятельно проделать простейшую гимнастику (go mod init; go run. prog.exe; см. системный журнал «my service») и убедиться, как это сделал я" очевидно, относится вот к этому моему комментарию: куда пишутся логи самой обёртки тоже не очень понятно (ок, это может быть моё незнание golang, и они на самом деле тоже пишутся в eventLog, но я в этом не уверен)
Всё это обычное переобувание на лету.
Я просто напомню, с чего возник интерес к примеру. Конкретно, вот с этого Вашего утверждения: Когда мне нужно запускать веб-сервис в виде службы windows, я напишу обычный веб-сервис и без проблем запущу его в виде службы и направлю stdout куда мне надо без посторонних «агентов» и докеров. Цена вопроса — ~15-20 строк банального кода. . На проверку оказалось, что чудес на свете не бывает, что написать обёртку займёт отнюдь не 15-20 строк кода, а на пару порядков больше, да и времени на отладку уйдёт заметное количество. Приврали ради красного словца. Ну что же, бывает.
Ну что же опускаться до подмены сказанного собеседником на то, что удобно для себя? Напомню, речь не идёт о том, как _собираются_ логи с потока. А о том, как _сам сервис_ пишет их в поток вывода. Ваше предложение: достаточно простой обёртки над выводом в stdut… К счастью, я пишу на Go, и мне не сотавляет труда поставить мьютекс там, где есть доступ к стейту, и радоваться жизни. Ваше предложение однозначно понимается как устроить бутылочное горлышко, посадив запись в поток вывода под мьютекс. Именно в Вашей обёртке и будут тормоза.
Заметьте, не я это предложил: Когда мне нужно запускать веб-сервис в виде службы windows, я напишу обычный веб-сервис и без проблем запущу его в виде службы и направлю stdout куда мне надо без посторонних «агентов» и докеров.
Если вынусть слово«веб» из этого тезиса, опять же, ничего не изменится. Парсить stdin в windows service, вместо того, чтобы принять callback от Service Control Manager — это бессмыслица, и мартышкин труд. Как и писать логи из windows service в поток вывода, потом городить обёртку, которая их будет перекладывать в event log.
Вы хотите поиграть в игру про правописание? Это было бы несложно:
Ни какие, ни какая — пишется слитно.
Не уместны — тоже слитно.
Ни кто — снова, слитно.
логии — пишется с одной «и»
и т.д. Но здесь, всё же, не занятия по правописанию, и, придираться к опечаткам в комментариях на Хабре — это несусветная глупость.
pawlo16
«А куда они тогда пишутся? Ведь обёртка эти сообщения создаёт. Значит, замысел их выводить тоже был» — они пишутся в консоль и более никуда. Это нормально для консольного приложения системного и инфраструктурного назначения. Почему это вас так напрягает? Я не ставил задачи перенаправить это в системный журнал
«15-20 строк кода, а на пару порядков больше, да и времени на отладку уйдёт заметное количество» — естественно для рабочего приложения времени уйдёт больше, чем на демонстрационный пример. Я вам показал что это в принципе просто решается, в контексте обсуждения этого достаточно.
«речь не идёт о том, как _собираются_ логи с потока. А о том, как _сам сервис_ пишет их в поток вывода» — ровно стой же скоростью, с которой строка выводится в консоль при разработке, не больше и не меньше.
anonymous
Да потому, что обёртка — это не консольное приложение, а windows service. А у windows services нет консоли :) Вы помните вообще, зачем Вы взялись её писать?
Хорошо, допустим, что не было замысла писать сообщения обёртки в event log. Тогда почему в режиме консольного приложения она как раз это и делает? Этот пример вообще Вы писали?
Снова переобуваемся на лету. Вы сказали дословно: «Когда мне нужно запускать веб-сервис в виде службы windows, я напишу обычный веб-сервис и без проблем запущу его в виде службы и направлю stdout куда мне надо без посторонних «агентов» и докеров. Цена вопроса — ~15-20 строк банального кода.». Речь идёт здесь не о примере, а именно о продукте. Вопрос про пример появился позже.
Разумеется! И это ни разу не быстро. И сидит под mutex, поэтому не масштабируется вширь вообще никак. Пока один поток пишет — все остальные ждут. Это как очередь на кассу с одним кассиром в магазине. Хоть миллион человек туда запусти — если кассир выпускает одного человека в минуту, с покупками оттуда за час выйдет ровно 60 человек.
debagger
Дико извиняюсь, но в итоге все равно данные в логи пишутся последовательно, так что где-нибудь эта очередь да возникнет. Если приложение генерирует такое количество логов, что они не влазят в stdout то надо уже разобраться, нужно ли такое количество информации именно в журналах, может надо для нее какое-то более подходящее хранилище использовать. Представляете себе миллион сообщений в час в event log'е?
debagger
В браузере, например, есть консоль, но нет stdout.