Паттерн (шаблон) проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Но сегодня мы рассмотрим не конкретный паттерн проектирования, а принцип построения объектно-ориентированных приложений Инверсия управления (IoC).

Инверсия управления (Inversion of Control, IoC) это определенный набор рекомендаций, позволяющих проектировать и реализовывать приложения используя слабо связывая отдельные компоненты. То есть, для того чтобы следовать принципам Инверсии управления нам необходимо:

  • Реализовывать компоненты, отвечающие за одну конкретную задачу;
  • Компоненты должны быть максимально независимыми друг от друга;
  • Компоненты не должны зависеть от конкретной реализации друг друга.

Одним из видов конкретной реализации данных рекомендаций является механизм Внедрения зависимостей (Dependency Injection, DI). Он определяет две основные рекомендации:

  • модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций;
  • абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

То есть, если у нас будут существовать два связанных класса, то нам необходимо реализовывать связь между ними не напрямую, а через интерфейс. Это позволит нам при необходимости динамически менять реализацию зависимых классов.

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

No DI

No DI

Проблема состоит в том, что в настоящее время алгоритмов, на которых основаны крипто валюты достаточно много и их число постоянно увеличивается. Если мы захотим добавить новые алгоритмы для майнинга других крипто валют, нам придется вносить большое количество изменений в сам класс майнера. Чтобы этого избежать, необходимо создать промежуточный интерфейс, от которого будет зависеть майнер и который должны будут реализовывать различные алгоритмы.

DI

DI

Так мы сможем не только разделить ответственность за выполнение конкретных задач между классом майнером и алгоритмами, но и сделаем задел на дальнейшее увеличение количества поддерживаемых алгоритмов.

Существует несколько конкретных паттернов Внедрения зависимостей:

  • Через конструктор (Constructor injection);
  • Через свойство класса (Setter injection);
  • Через аргумент метода (Method injection).

Давайте рассмотрим примеры реализации данных паттернов.

Для начала рассмотрим как можно реализовать данную задачу без Внедрения зависимостей. Мы создадим класс майнер, который будет запускать отдельный поток вычисления хеш-функции.

Стандартная реализация

using System;
using System.Threading;
 
namespace DependencyInjection
{
    /// <summary>
    /// Основной класс выполняющих майнинг.
    /// </summary>
    public class Miner
    {
        /// <summary>
        /// Алгоритм поиска хеша.
        /// </summary>
        private SHA256 sha256;
 
        /// <summary>
        /// Поток в котором выполняется поиск.
        /// </summary>
        private Thread thread;
 
        /// <summary>
        /// Событие нахождения хеша.
        /// </summary>
        public event EventHandler<bool> HashFound;
 
        /// <summary>
        /// Создать экземпляр майнера
        /// </summary>
        public Miner()
        {
            sha256 = new SHA256();
            thread = new Thread(Mine);
        }
 
        /// <summary>
        /// Начать майнинг.
        /// </summary>
        public void Start()
        {
            thread.Start();
        }
 
        /// <summary>
        /// Остановить майнинг.
        /// </summary>
        public void Stop()
        {
            thread.Abort();
        }
 
        /// <summary>
        /// Метод выполняющий майнинг.
        /// </summary>
        private void Mine()
        {
            while (true)
            {
                var hashResult = sha256.Hash();
                HashFound?.Invoke(this, hashResult);
            }
        }
    }
}

Так же мы создадим класс SHA256, который как раз и будет отвечать за нахождения хеша.

using System;
using System.Threading;
 
namespace DependencyInjection
{
    /// <summary>
    /// Алгоритм вычисления хеш-функции SHA256.
    /// </summary>
    public class SHA256
    {
        /// <summary>
        /// Вычисление нового хеша.
        /// </summary>
        /// <returns>Успешность нахождения хеша.</returns>
        public bool Hash()
        {
            var guid = Guid.NewGuid();
            Thread.Sleep(5000);
            var hash = guid.GetHashCode();
            if(hash <= 10000)
            {
                return true;
            }
 
            return false;
            
        }
 
        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns>Имя алгоритма.</returns>
        public override string ToString()
        {
            return nameof(SHA256);
        }
    }
}

Теперь нам только и осталось создать экземпляр нашего майнера и запустить процесс майнинга.

using System;
 
namespace DependencyInjection
{
    class Program
    {
        static void Main(string[] args)
        {
            // Создаем экземпляр майнера.
            var miner = new Miner();
 
            // Подписываемся на событие нахождения хеша.
            miner.HashFound += Miner_HashFound;
 
            // Начинаем майнинг.
            Console.WriteLine($"Начало майнинга {DateTime.Now.ToShortTimeString()}");
            miner.Start();
        }
 
        /// <summary>
        /// Обработчик события нахождения хеша.
        /// </summary>
        /// <param name="sender">Майнер.</param>
        /// <param name="e">Корректность хеша.</param>
        private static void Miner_HashFound(object sender, bool e)
        {
            if(e)
            {
                Console.WriteLine("хеш найден");
            }
            else
            {
                Console.WriteLine("Некорректный хеш");
            }
        }
    }
}

В результате мы получим следующее:

Result no DI

Result no DI

Но как и упоминалось ранее, если мы захотим добавить новый алгоритм нам потребуется внести достаточно много изменений в код нашего класса майнера. Давайте внесем изменения в наши классы так, чтобы сделать их более гибкими и универсальными.

Внедрение зависимостей

Для начала добавим интерфейс для алгоритмов поиска хешей.

namespace DependencyInjection
{
    /// <summary>
    /// Базовый интерфейс криптоалгорима хеш-функции.
    /// </summary>
    public interface IAlgorithm 
    {
        /// <summary>
        /// Вычисление нового хеша.
        /// </summary>
        /// <returns>Успешность нахождения хеша.</returns>
        bool Hash();
    }
}

Создадим новый класс алгоритма Ethash и изменим существующий SHA256.

using System;
using System.Threading;
 
namespace DependencyInjection
{
    /// <summary>
    /// Алгоритм вычисления хеш-функции SHA256.
    /// </summary>
    public class SHA256 : IAlgorithm
    {
        /// <inheritdoc />
        public bool Hash()
        {
            var guid = Guid.NewGuid();
            Thread.Sleep(5000);
            var hash = guid.GetHashCode();
            if(hash <= 10000)
            {
                return true;
            }
 
            return false;
            
        }
 
        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns>Имя алгоритма.</returns>
        public override string ToString()
        {
            return nameof(SHA256);
        }
    }
}
using System;
using System.Threading;
 
namespace DependencyInjection
{
    /// <summary>
    /// Алгоритм вычисления хеш-функции Ethash.
    /// </summary>
    public class Ethash : IAlgorithm
    {
        /// <inheritdoc />
        public bool Hash()
        {
            var random = new Random();
            Thread.Sleep(1000);
            var hash = random.Next();
            if (hash <= 10000)
            {
                return true;
            }
 
            return false;
        }
 
        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns>Имя алгоритма.</returns>
        public override string ToString()
        {
            return nameof(Ethash);
        }
    }
}

Внедрение зависимостей через конструктор

Изменим класс майнер следующим образом.

using System;
using System.Threading;
 
namespace DependencyInjection
{
    /// <summary>
    /// Основной класс выполняющих майнинг.
    /// </summary>
    public class Miner
    {
        /// <summary>
        /// Алгоритм поиска хеша.
        /// </summary>
        private IAlgorithm algoritm;
 
        /// <summary>
        /// Поток в котором выполняется поиск.
        /// </summary>
        private Thread thread;
 
        /// <summary>
        /// Событие нахождения хеша.
        /// </summary>
        public event EventHandler<bool> HashFound;
 
        /// <summary>
        /// Создать экземпляр майнера.
        /// </summary>
        /// <param name="algorithm">Алгоритм выполняющий вычисления.</param>
        public Miner(IAlgorithm algorithm)
        {
            algoritm = algorithm;
            thread = new Thread(Mine);
        }
 
        /// <summary>
        /// Начать майнинг.
        /// </summary>
        public void Start()
        {
            thread.Start();
        }
 
        /// <summary>
        /// Остановить майнинг.
        /// </summary>
        public void Stop()
        {
            thread.Abort();
        }
 
        /// <summary>
        /// Метод выполняющий майнинг.
        /// </summary>
        private void Mine()
        {
            while (true)
            {
                var hashResult = algoritm.Hash();
                HashFound?.Invoke(this, hashResult);
            }
        }
    }
}

Как видите, теперь манер не зависит от конкретного алгоритма, а принимает только интерфейс как аргумент конструктора.

Теперь нам немного нужно изменить вызов майнера в консольном приложении.

using System;
 
namespace DependencyInjection
{
    class Program
    {
        static void Main(string[] args)
        {
            Miner miner = null;
            // Выбираем алгоритм.
            Console.WriteLine("Выберите алгоритм: ");
            Console.WriteLine("1 - SHA256");
            Console.WriteLine("2 - Ethash");
            var algorithmImput = Console.ReadLine();
            if(int.TryParse(algorithmImput, out int algorithm))
            {
                switch(algorithm)
                {
                    // Создаем экземпляр майнера.
                    case 1:
                        miner = new Miner(new SHA256());
                        break;
                    case 2:
                        miner = new Miner(new Ethash());
                        break;
                    default:
                        throw new ArgumentException("Неизвестный алгоритм.", nameof(algorithm));
                }
            }
 
            // Подписываемся на событие нахождения хеша.
            miner.HashFound += Miner_HashFound;
 
            // Начинаем майнинг.
            Console.WriteLine($"Начало майнинга {DateTime.Now.ToShortTimeString()}");
            miner.Start();
        }
 
        /// <summary>
        /// Обработчик события нахождения хеша.
        /// </summary>
        /// <param name="sender">Майнер.</param>
        /// <param name="e">Корректность хеша.</param>
        private static void Miner_HashFound(object sender, bool e)
        {
            if(e)
            {
                Console.WriteLine("хеш найден");
            }
            else
            {
                Console.WriteLine("Некорректный хеш");
            }
        }
    }
}

Внедрение зависимостей через свойство

using System;
using System.Threading;
 
namespace DependencyInjection
{
    /// <summary>
    /// Основной класс выполняющих майнинг.
    /// </summary>
    public class Miner
    {
        /// <summary>
        /// Алгоритм поиска хеша.
        /// </summary>
        public IAlgorithm Algoritm { get; set; }
 
        /// <summary>
        /// Поток в котором выполняется поиск.
        /// </summary>
        private Thread thread;
 
        /// <summary>
        /// Событие нахождения хеша.
        /// </summary>
        public event EventHandler<bool> HashFound;
 
        /// <summary>
        /// Создать экземпляр майнера.
        /// </summary>
        public Miner()
        {
            thread = new Thread(Mine);
        }
 
        /// <summary>
        /// Начать майнинг.
        /// </summary>
        public void Start()
        {
            thread.Start();
        }
 
        /// <summary>
        /// Остановить майнинг.
        /// </summary>
        public void Stop()
        {
            thread.Abort();
        }
 
        /// <summary>
        /// Метод выполняющий майнинг.
        /// </summary>
        private void Mine()
        {
            while (true)
            {
                var hashResult = Algoritm.Hash();
                HashFound?.Invoke(this, hashResult);
            }
        }
    }
}
static void Main(string[] args)
{
     // Создаем экземпляр майнера.
     var miner = new Miner();
 
     // Выбираем алгоритм.
     Console.WriteLine("Выберите алгоритм: ");
     Console.WriteLine("1 - SHA256");
     Console.WriteLine("2 - Ethash");
     var algorithmImput = Console.ReadLine();
     if(int.TryParse(algorithmImput, out int algorithm))
     {
         switch(algorithm)
         {
             // Устанавливаем алгоритм.
             case 1:
                 miner.Algoritm = new SHA256();
                 break;
             case 2:
                 miner.Algoritm = new Ethash();
                 break;
             default:
                 throw new ArgumentException("Неизвестный алгоритм.", nameof(algorithm));
         }
     }
 
     // Подписываемся на событие нахождения хеша.
     miner.HashFound += Miner_HashFound;
 
     // Начинаем майнинг.
     Console.WriteLine($"Начало майнинга {DateTime.Now.ToShortTimeString()}");
     miner.Start();
}

Внедрение зависимостей через аргумент метода

using System;
using System.Threading;
 
namespace DependencyInjection
{
    // Алгоритм совсем не хранится в переменной.
    /// <summary>
    /// Основной класс выполняющих майнинг.
    /// </summary>
    public class Miner
    {
        /// <summary>
        /// Поток в котором выполняется поиск.
        /// </summary>
        private Thread thread;
 
        /// <summary>
        /// Событие нахождения хеша.
        /// </summary>
        public event EventHandler<bool> HashFound;
 
        /// <summary>
        /// Создать экземпляр майнера.
        /// </summary>
        public Miner()
        {
           
        }
 
        /// <summary>
        /// Начать майнинг.
        /// </summary>
        public void Start(IAlgorithm algorithm)
        {
            thread = new Thread(() => Mine(algorithm));
            thread.Start();
        }
 
        /// <summary>
        /// Остановить майнинг.
        /// </summary>
        public void Stop()
        {
            thread.Abort();
        }
 
        /// <summary>
        /// Метод выполняющий майнинг.
        /// </summary>
        private void Mine(IAlgorithm algorithm)
        {
            while (true)
            {
                var hashResult = algorithm.Hash();
                HashFound?.Invoke(this, hashResult);
            }
        }
    }
}
static void Main(string[] args)
{
    // Создаем экземпляр майнера.
    var miner = new Miner();
 
    IAlgorithm algorithm = null;
    // Выбираем алгоритм.
    Console.WriteLine("Выберите алгоритм: ");
    Console.WriteLine("1 - SHA256");
    Console.WriteLine("2 - Ethash");
    var algorithmImput = Console.ReadLine();
    if(int.TryParse(algorithmImput, out int algo))
    {
        switch(algo)
        {
            // Устанавливаем алгоритм.
            case 1:
                algorithm = new SHA256();
                break;
            case 2:
                algorithm = new Ethash();
                break;
            default:
                throw new ArgumentException("Неизвестный алгоритм.", nameof(algo));
        }
    }
 
    // Подписываемся на событие нахождения хеша.
    miner.HashFound += Miner_HashFound;
 
    // Начинаем майнинг.
    Console.WriteLine($"Начало майнинга {DateTime.Now.ToShortTimeString()}");
    miner.Start(algorithm);
}

Исходный код также можно посмотреть в репозитории https://github.com/shwanoff/DependencyInjection.

Также хотелось бы упомянуть о IoC-контейнерах. IoC-контейнер (IoC-container) — это своеобразный фреймворк, позволяющий реализовывать Внедрение зависимостей более лаконичным способом, избавляя от рутины. Существует много различных IoC-контейнеров. Вот некоторые наиболее популярные из них:

В заключение хочется сказать, что IoC, DI — очень полезный механизм, но и как любой другой инструмент его нужно использовать только там, где он действительно необходим и принесет больше пользы, чем вреда. Ведь его реальное внедрение в небольшое консольное приложение, в котором вряд ли что-то будет меняться только усложнит архитектуру и понимание кода, а также увеличит вероятность совершения ошибки. В то же время если это серьезный крупный проект, где пожелания заказчика часто изменчивы и противоречивы, данный механизм может серьезно упростить жизнь разработчикам.

 
×
%d такие блоггеры, как: