Adapter C# | Паттерн Адаптер C#

Давайте рассмотрим паттерн проектирования Адаптер C# — Adapter C#, для чего он нужен и какие проблемы он решает. Где можно применять данный шаблон, а где это будет излишним.

Идея паттерна

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

Как я уже писал ранее, существует три вида паттернов проектирования:

  • Порождающие паттерны позволяют возможность выполнять инициализацию объектов наиболее удобным и оптимальным способом.
  • Структурные паттерны описывают взаимоотношения между различными классами или объектами, позволяя им совместно реализовывать поставленную задачу.
  • Поведенческие паттерны позволяют грамотно организовать связь между сущностями для оптимизации и упрощения их взаимодействия.

Адаптер (Adapter) – это структурный паттерн, который позволяет адаптировать интерфейс класса в соответствии с требованиями системы. То есть, это своеобразная прослойка между классами, приводящая интерфейс одного класса к используемому в другом.

Архитектура паттерна Adapter

Давайте рассмотрим диаграмму паттерна.

Adapter UML
Adapter UML
  • Target — целевой интерфейс, к которому нужно преобразовать интерфейс существующих классов;
  • Adaptee — существующий класс, чей интерфейс нужно преобразовать;
  • Adapter — класс-адаптер, который преобразует интерфейс адаптируемого класса к целевому;
  • Client — клиенты нового интерфейса, которые работают с адаптированными классами полиморфным образом.

Логика работы паттерна

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

Реализация паттерна проектирования Адаптер на языке C#

Итак, рассмотрим интерфейс, который должен реализовывать кассовый аппарат в нашей информационной системе. У кассового аппарата есть уникальный номер, коллекция товаров, и возможность добавить товар, сохранить и распечатать чек.

using System.Collections.Generic;

namespace Patterns.Adapter.MainCashMachine
{
    /// <summary>
    /// Интерфейс кассового аппарата.
    /// </summary>
    public interface ICashMachine
    {
        /// <summary>
        /// Уникальный номер кассового аппарата.
        /// </summary>
        string Number { get; }

        /// <summary>
        /// Коллекция товаров в текущем чеке.
        /// </summary>
        IReadOnlyList<Product> Products { get; }

        /// <summary>
        /// Собрать чек и вывести его на печать.
        /// </summary>
        /// <remarks>
        /// Печать чека вызывает его сохранение и очистку коллекции товаров текущего чека.
        /// </remarks>
        /// <returns>Текст чека</returns>
        string PrintCheck();

        /// <summary>
        /// Добавить товар в коллекцию товаров текущего чека.
        /// </summary>
        /// <param name="product">Товар, добавляемый в чек.</param>
        void AddProduct(Product product);

        /// <summary>
        /// Сохранить чек.
        /// </summary>
        /// <remarks>
        /// Сохранение чека вызывает очистку коллекции товаров текущего чека.
        /// </remarks>
        /// <param name="checkText"></param>
        void Save(string checkText);
    }
}

Реализуем данный интерфейс в обычном кассовом аппарате.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Patterns.Adapter.MainCashMachine
{
    /// <summary>
    /// Кассовый аппарат собственного производства.
    /// </summary>
    public class CashMachine : ICashMachine
    {
        /// <summary>
        /// Список товаров в чеке.
        /// </summary>
        private List<Product> _products;

        /// <summary>
        /// Уникальный номер кассового аппарата.
        /// </summary>
        private Guid _number;

        /// <inheritdoc />
        public IReadOnlyList<Product> Products => _products;

        /// <inheritdoc />
        public string Number => _number.ToString();

        /// <summary>
        /// Создать новый экземпляр кассового аппарата.
        /// </summary>
        public CashMachine()
        {
            _number = Guid.NewGuid();
            _products = new List<Product>();
        }

        /// <inheritdoc />
        public void AddProduct(Product product)
        {
            if (product == null)
            {
                throw new ArgumentNullException(nameof(product));
            }

            _products.Add(product);
        }

        /// <inheritdoc />
        public string PrintCheck()
        {
            var checkText = GetCheckText();
            Save(checkText);
            return checkText;
        }

        /// <inheritdoc />
        public void Save(string checkText)
        {
            if(string.IsNullOrEmpty(checkText))
            {
                throw new ArgumentNullException(nameof(checkText));
            }

            using (var sr = new StreamWriter("checks.txt", true, Encoding.Default))
            {
                sr.WriteLine(checkText);
            }
            _products.Clear();
        }

        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns>Номер кассового аппарата.</returns>
        public override string ToString()
        {
            return $"Касса №{Number}";
        }

        /// <summary>
        /// Сформировать текст чека для вывода на печать и сохранения в файл.
        /// </summary>
        /// <returns>Форматированный текст чека.</returns>
        private string GetCheckText()
        {
            var date = DateTime.Now.ToString("dd MMMM yyyy HH:mm");
            var checkText = $"Кассовый чек от {date}\r\n";
            checkText += $"Идентификатор чека: {Guid.NewGuid()}\r\n";
            checkText += "Список товаров:\r\n";
            foreach (var product in _products)
            {
                checkText += $"{product.Name}\t\t\t{product.Price}\r\n";
            }
            var sum = _products.Sum(p => p.Price);
            checkText += $"ИТОГО\t\t\t{sum}\r\n";
            return checkText;
        }
    }
}
using System;

namespace Patterns.Adapter.MainCashMachine
{
    /// <summary>
    /// Товар.
    /// </summary>
    public class Product
    {
        /// <summary>
        /// Наименование товара.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Стоимость.
        /// </summary>
        public decimal Price { get; set; }

        /// <summary>
        /// Создать новый экземпляр товара.
        /// </summary>
        /// <param name="name">Название.</param>
        /// <param name="price">Цена.</param>
        public Product(string name, decimal price)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException(nameof(name));
            }

            if(price < 0)
            {
                throw new ArgumentException("Цена не может быть меньше нуля.", nameof(price));
            }

            Name = name;
            Price = price;
        }

        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns>Название товара.</returns>
        public override string ToString()
        {
            return Name;
        }
    }
}

Теперь создадим в проекте новую библиотеку классов (.dll), которая будет символизировать иностранный кассовый аппарат, предоставляющий совершенно другой интерфейс взаимодействия. Добавим в созданную библиотеку два класса иностранного кассового аппарата и чека, используемого в нем.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Patterns.Adapter.ForeignCashMachine
{
    /// <summary>
    /// Иностранный кассовый аппарат.
    /// </summary>
    public class ForeignCashMachine
    {
        /// <summary>
        /// Список чеков, хранящихся в кассовом аппарате.
        /// </summary>
        private List<Check> _checks = new List<Check>();

        /// <summary>
        /// Название кассового аппарата.
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// Массив чеков, хранящихся в кассовом аппарате.
        /// </summary>
        public Check[] Checks => _checks.ToArray();

        /// <summary>
        /// Текущий заполняемый чек.
        /// </summary>
        public Check CurrentCheck => _checks.LastOrDefault();

        /// <summary>
        /// Создать экземпляр кассового аппарата.
        /// </summary>
        public ForeignCashMachine()
        {
            var rnd = new Random();
            Name = $"KKA{rnd.Next(10000, 99999)}";
            _checks.Add(new Check());
        }

        /// <summary>
        /// Добавить товар в текущий чек.
        /// </summary>
        /// <param name="name">Наименование товара.</param>
        /// <param name="price">Стоимость товара.</param>
        public void Add(string name, double price)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException(nameof(name));
            }

            if (price < 0)
            {
                throw new ArgumentException("Цена не может быть меньше нуля.", nameof(price));
            }

            CurrentCheck.Add(name, price);
        }

        /// <summary>
        /// Сохранить чек.
        /// </summary>
        /// <remarks>
        /// Возвращается копия последнего заполненного кассового чека,
        /// а в кассовом аппарате заводится новый пустой чек.
        /// </remarks>
        /// <returns>Чек.</returns>
        public Check Save()
        {
            var check = (Check)CurrentCheck.Clone();
            _checks.Add(new Check());
            return check;
        }

        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns>Название кассового аппарата.</returns>
        public override string ToString()
        {
            return Name;
        }
    }
}
using System;

namespace Patterns.Adapter.ForeignCashMachine
{
    /// <summary>
    /// Иностранный товар.
    /// </summary>
    public class ForeignProduct
    {
        /// <summary>
        /// Название.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Стоимость.
        /// </summary>
        public double Price { get; set; }

        /// <summary>
        /// Создание нового экземпляра класса ForeignProduct.
        /// </summary>
        /// <param name="name"> Название. </param>
        /// <param name="price"> Стоимость. </param>
        public ForeignProduct(string name, double price)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException(nameof(name));
            }

            if (price < 0)
            {
                throw new ArgumentException("Цена не может быть меньше нуля.", nameof(price));
            }

            Name = name;
            Price = price;
        }

        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns> Название. </returns>
        public override string ToString()
        {
            return Name;
        }
    }
}
using System;
using System.Collections.Generic;

namespace Patterns.Adapter.ForeignCashMachine
{
    /// <summary>
    /// Кассовый чек.
    /// </summary>
    public class Check : ICloneable
    {
        /// <summary>
        /// Список товаров в кассовом чеке.
        /// </summary>
        private List<ForeignProduct> _products;

        /// <summary>
        /// Номер чека.
        /// </summary>
        public int Number { get; private set; }

        /// <summary>
        /// Дата создания чека.
        /// </summary>
        public DateTime DateTime { get; private set; }

        /// <summary>
        /// Товары в чеке.
        /// </summary>
        public IReadOnlyList<ForeignProduct> Products => _products;

        /// <summary>
        /// Создать экземпляр чека.
        /// </summary>
        public Check()
        {
            var rnd = new Random();

            Number = rnd.Next(10000, 99999);
            DateTime = DateTime.Now;
            _products = new List<ForeignProduct>();
        }

        /// <summary>
        /// Добавить товар в чек.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="price"></param>
        public void Add(string name, double price)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException(nameof(name));
            }

            if (price < 0)
            {
                throw new ArgumentException("Цена не может быть меньше нуля.", nameof(price));
            }

            _products.Add(new ForeignProduct(name, price));
        }

        /// <summary>
        /// Создать копию чека.
        /// </summary>
        /// <returns>Копия чека.</returns>
        public object Clone()
        {
            return new Check()
            {
                _products = _products,
                DateTime = DateTime,
                Number = Number
            };
        }

        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns>Текст чека.</returns>
        public override string ToString()
        {
            var checkText = $"Кассовый чек от {DateTime.ToString("HH:mm dd.MMMM.yyyy")}\r\n";
            checkText += $"Номер чека: {Number}\r\n";
            return checkText;
        }
    }
}

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

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Patterns.Adapter.MainCashMachine
{
    /// <summary>
    /// Адаптер для работы с иностранными кассовыми аппаратами как с обычными.
    /// </summary>
    public class ForeignCashMachineAdapter : ICashMachine
    {
        /// <summary>
        /// Иностранный кассовый аппарат.
        /// </summary>
        private ForeignCashMachine.ForeignCashMachine _foreignCashMachine;

        /// <inheritdoc />
        public string Number => _foreignCashMachine.Name;

        /// <inheritdoc />
        public IReadOnlyList<Product> Products
        {
            get
            {
                var productsTuple = _foreignCashMachine.CurrentCheck.Products;
                var products = productsTuple.Select(s => new Product(s.Name, Convert.ToDecimal(s.Price)));
                return (IReadOnlyList<Product>)products;
            }
        }

        /// <summary>
        /// Создать экземпляр иностранного кассового аппарата под обычный.
        /// </summary>
        /// <param name="foreignCashMachine">Иностранный кассовый аппарат.</param>
        public ForeignCashMachineAdapter(ForeignCashMachine.ForeignCashMachine foreignCashMachine)
        {
            _foreignCashMachine = foreignCashMachine 
                ?? throw new ArgumentNullException(nameof(foreignCashMachine));
        }

        /// <inheritdoc />
        public void AddProduct(Product product)
        {
            if (product == null)
            {
                throw new ArgumentNullException(nameof(product));
            }

            _foreignCashMachine.Add(product.Name, Convert.ToDouble(product.Price));
        }

        /// <inheritdoc />
        public string PrintCheck()
        {
            var check = _foreignCashMachine.Save();
            var checkText = GetCheckText(check);
            Save(checkText);
            return checkText;
        }

        /// <inheritdoc />
        public void Save(string checkText)
        {
            if (string.IsNullOrEmpty(checkText))
            {
                throw new ArgumentNullException(nameof(checkText));
            }

            using (var sr = new StreamWriter("checks2.txt", true, Encoding.Default))
            {
                sr.WriteLine(checkText);
            }
        }

        /// <summary>
        /// Сформировать текст чека для вывода на печать и сохранения в файл.
        /// </summary>
        /// <param name="check">Чек иностранного кассового аппарата.</param>
        /// <returns>Форматированный текст чека.</returns>
        private string GetCheckText(ForeignCashMachine.Check check)
        {
            var date = check.DateTime.ToString("dd MMMM yyyy HH:mm");
            var checkText = $"Кассовый чек от {date}\r\n";
            checkText += $"Идентификатор чека: {check.Number}\r\n";
            checkText += "Список товаров:\r\n";
            foreach (var product in check.Products)
            {
                checkText += $"{product.Name}\t\t\t{product.Price}\r\n";
            }
            var sum = check.Products.Sum(p => p.Price);
            checkText += $"ИТОГО\t\t\t{sum}\r\n";
            return checkText;
        }
    }
}

Теперь нам остается только проверить работу кассовых аппаратов на практике. Для этого реализуем следующий вызов.

using System;
using System.Collections.Generic;

namespace Adapter
{
    class Program
    {
        static void Main(string[] args)
        {
            // Создадим новый обычный кассовый аппарат.
            var cashMachineStandart = new CashMachine();
            var cashMacineForeign = new ForeignCashMachine.ForeignCashMachine();
 
            // Создадим список товаров, которые будут добавлены в чек.
            var products = new List<Product>
            {
                new Product("Samsung SSD 256Gb", 9600),
                new Product("Crucial RAM DDR3 4Gb", 2500),
                new Product("Intel CPU Core-i7 6400", 8000)
            };
 
            // Проверяем обычный кассовый аппарат.
            TestCashMachine(cashMachineStandart, products);
 
            // Создаем и проверяем иностранный кассовый аппарат.
            var cashMacineForeignAdapter = new ForeignCashMachineAdapter(cashMacineForeign);
            TestCashMachine(cashMacineForeignAdapter, products);
        }
 
        private static void TestCashMachine(ICashMachine cashMachine, List<Product> products)
        {
            // Добавляем товары в чек.
            products.ForEach(p => cashMachine.AddProduct(p));
 
            // Печатаем чек и сохраняем его в переменную.
            var check = cashMachine.PrintCheck();
 
            // Выводим результат работы с обычным кассовым аппаратом на экран.
            Console.WriteLine(check);
            Console.ReadLine();
        }
    }
}

Как вы видите, мы обращаемся к обоим кассовым аппаратам абсолютно идентично. Проверим полученный результат.

Adapter result
Adapter result

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

Adapter struct
Adapter struct

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

Рекомендую также изучить статью Паттерн Декоратор (Decorator). А еще подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.