Давайте рассмотрим паттерн проектирования Адаптер C# — Adapter C#, для чего он нужен и какие проблемы он решает. Где можно применять данный шаблон, а где это будет излишним.
Идея паттерна
Паттерн (шаблон) проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте рассмотрим один из наиболее часто используемых структурных паттернов — Адаптер.
Как я уже писал ранее, существует три вида паттернов проектирования:
- Порождающие паттерны позволяют возможность выполнять инициализацию объектов наиболее удобным и оптимальным способом.
- Структурные паттерны описывают взаимоотношения между различными классами или объектами, позволяя им совместно реализовывать поставленную задачу.
- Поведенческие паттерны позволяют грамотно организовать связь между сущностями для оптимизации и упрощения их взаимодействия.
Адаптер (Adapter) – это структурный паттерн, который позволяет адаптировать интерфейс класса в соответствии с требованиями системы. То есть, это своеобразная прослойка между классами, приводящая интерфейс одного класса к используемому в другом.
Архитектура паттерна Adapter
Давайте рассмотрим диаграмму паттерна.

- 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(); } } }
Как вы видите, мы обращаемся к обоим кассовым аппаратам абсолютно идентично. Проверим полученный результат.

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

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