Паттерн проектирования «Адаптер»

Статья из группы Java Developer
Привет! Сегодня мы затронем важную новую тему — паттерны, или по-другому — шаблоны проектирования. Что же такое паттерны? Паттерн проектирования «Адаптер» - 1Думаю, тебе известно выражение «не надо изобретать велосипед». В программировании, как и во многих других сферах, есть большое количество типовых ситуаций. Для каждой из них в процессе развития программирования создавались готовые работающие решения. Это и есть шаблоны проектирования. Условно говоря, паттерн — это некий пример, который предлагает решение ситуации вида: «если в вашей программе нужно сделать то-то, как это лучше всего сделать». Паттернов очень много, им посвящена отличная книга «Изучаем шаблоны проектирования», с которой обязательно нужно ознакомиться. Паттерн проектирования «Адаптер» - 2Если говорить максимально кратко, паттерн состоит из распространенной проблемы и ее решения, которое уже можно считать неким стандартом. В сегодняшней лекции мы познакомимся с одним из таких паттернов под названием «Адаптер». Название у него говорящее, и ты не раз встречался с адаптерами в реальной жизни. Один из самых распространенных адаптеров — кардридеры, которыми снабжены множество компьютеров и ноутбуков. Паттерн проектирования «Адаптер» - 3Представь, что у нас есть какая-то карта памяти. В чем состоит проблема? В том, что она не умеет взаимодействовать с компьютером. У них нет общего интерфейса. У компьютера есть разъем USB, но карту памяти в него не вставить. Карту невозможно вставить в компьютер, из-за чего мы не сможем сохранить наши фотографии, видео и другие данные. Кардридер является адаптером, решающим данную проблему. Ведь у него есть USB-кабель! В отличие от самой карты, кардридер можно вставить в компьютер. У них с компьютером есть общий интерфейс — USB. Давай посмотрим, как это будет выглядеть на примере:

public interface USB {

   void connectWithUsbCable();
}
Это наш интерфейс USB с единственным методом — вставить USB-кабель:

public class MemoryCard {

   public void insert() {
       System.out.println("Карта памяти успешно вставлена!");
   }

   public void copyData() {
       System.out.println("Данные скопированы на компьютер!");
   }
}
Это наш класс, реализующий карту памяти. В нем уже есть 2 нужных нам метода, но вот беда: интерфейс USB он не реализует. Карту нельзя вставить в USB-разъем.

public class CardReader implements USB {

   private MemoryCard memoryCard;

   public CardReader(MemoryCard memoryCard) {
       this.memoryCard = memoryCard;
   }

   @Override
   public void connectWithUsbCable() {
       this.memoryCard.insert();
       this.memoryCard.copyData();
   }
}
А вот и наш адаптер! Что же делает класс CardReader и почему, собственно, он является адаптером? Все просто. Адаптируемый класс (карта памяти) становится одним из полей адаптера. Это логично, ведь в реальной жизни мы тоже вставляем карту внутрь кардридера, и она тоже становится его частью. В отличие от карты памяти, у адаптера есть общий интерфейс с компьютером. У него есть USB-кабель, то есть он может соединяться с другими устройствами по USB. Поэтому в программе наш класс CardReader реализует интерфейс USB. Но что же происходит внутри этого метода? А там происходит ровно то, что нам нужно! Адаптер делегирует выполнение работы нашей карте памяти. Ведь сам-то адаптер ничего не делает, какого-то самостоятельного функционала у кардридера нет. Его задача — только связать компьютер и карту памяти, чтобы карта могла сделать свою работу и скопировать файлы! Наш адаптер позволяет ей сделать это, предоставив свой интерфейс (метод connectWithUsbCable()) для «нужд» карты памяти. Давай создадим какую-то программу-клиент, которая будет имитировать человека, желающего скопировать данные с карты памяти:

public class Main {
  
   public static void main(String[] args) {

       USB cardReader = new CardReader(new MemoryCard());
       cardReader.connectWithUsbCable();

   }
}
Что же у нас в результате получилось? Вывод в консоль:

Карта памяти успешно вставлена!
Данные скопированы на компьютер!
Отлично, наша задача успешно выполнена! Вот несколько дополнительных ссылок с информацией о паттерне Адаптер:

Абстрактные классы Reader и Writer

Теперь мы вернемся к нашему любимому занятию: выучим парочку новых классов для работы со вводом и выводом :) Сколько мы их уже выучили, интересно? Сегодня речь пойдет о классах Reader и Writer. Почему именно о них? Потому что это будет в тему нашему предыдущему разделу — адаптерам. Давай рассмотрим их подробнее. Начнем с Reader’a. Reader — это абстрактный класс, поэтому явно создавать его объекты у нас не получится. Но на самом деле ты с ним уже знаком! Ведь хорошо знакомые тебе классы BufferedReader и InputStreamReader являются его наследниками :)

public class BufferedReader extends Reader {
…
}

public class InputStreamReader extends Reader {
…
}
Так вот, класс InputStreamReader — это классический адаптер. Как ты, наверное, помнишь, мы можем передать в его конструктор объект InputStream. Чаще всего мы для этого используем переменную System.in:

public static void main(String[] args) {

   InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
Что же делает InputStreamReader? Как и всякий адаптер, он преобразует один интерфейс к другому. В данном случае — интерфейс InputStream’a к интерфейсу Reader’a. Изначально у нас был класс InputStream. Он неплохо работает, но с его помощью можно читать только отдельные байты. Кроме того, у нас есть абстрактный класс Reader. У него есть отличный и очень нужный нам функционал — он умеет читать символы! Нам такая возможность, конечно, очень нужна. Но здесь мы сталкиваемся с классической проблемой, которую обычно решают адаптеры — несовместимость интерфейсов. В чем же она проявляется? Давай заглянем прямо в документацию Oracle. Вот методы класса InputStream. Паттерн проектирования «Адаптер» - 4Совокупность методов — это и есть интерфейс. Как видишь, метод read() у этого класса есть (даже в нескольких вариантах), но читать он может только байты: или отдельные байты, или несколько байт с использованием буфера. Нам такой вариант не подходит — мы хотим читать символы. Нужный нам функционал уже реализован в абстрактном классе Reader. Это тоже можно увидеть в документации. Паттерн проектирования «Адаптер» - 5Однако интерфейсы InputStream'a и Reader'a несовместимы! Как видишь, во всех реализациях метода read() у них отличаются и передаваемые параметры, и возвращаемые значения. И именно здесь нам понадобится InputStreamReader! Он выступит Адаптером между нашими классами. Как и в примере с кардридером, который мы рассмотрели выше, мы передаем объект «адаптируемого» класса «внутрь», то есть в конструктор класса-адаптера. В прошлом примере мы передавали объект MemoryCard внутрь CardReader. А теперь передаем объект InputStream в конструктор InputStreamReader! В качестве InputStream мы используем уже ставшую привычной переменную System.in:

public static void main(String[] args) {

   InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
И действительно: заглянув в документацию InputStreamReader'a мы увидим, что «адаптация» прошла успешно :) Теперь в нашем распоряжении есть методы, которые позволяют нам читать символы. Паттерн проектирования «Адаптер» - 6И хотя изначально наш объект System.in (поток, привязанный к клавиатуре) не позволял этого делать, создав паттерн Адаптер создатели языка решили эту проблему. У абстрактного класса Reader, как и у большинства I/O-классов, есть брат-близнец — Writer. Он имеет тот же большой плюс, что и Reader — предоставляет удобный интерфейс для работы с символами. С выходными потоками проблема и ее решение выглядят так же, как и в случае со входными. Есть класс OutputStream, который умеет записывать только байты; есть абстрактный класс Writer, который умеет работать с символами, и есть два несовместимых интерфейса. Эту проблему вновь успешно решает паттерн Адаптер. При помощи класса OutputStreamWriter мы легко «адаптируем» два интерфейса классов Writer и OutputStream друг другу. И, получив байтовый поток OutputStream в конструктор, с помощью OutputStreamWriter мы, тем не менее, можем записывать символы, а не байты!

import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException {

       OutputStreamWriter streamWriter = new OutputStreamWriter(new FileOutputStream("C:\\Users\\Username\\Desktop\\test.txt"));
       streamWriter.write(32144);
       streamWriter.close();
   }
}
Мы записали в наш файл символ с кодом 32144 — 綐, таким образом избавившись от необходимости работать с байтами :) На этом на сегодня все, до встречи на следующих лекциях! :)
Комментарии (58)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
KeLL Уровень 29, Москва
13 июля 2022
а почему нельзя memorycard реализовать от usb и сделать тоже самое тоько проще?!
Marianna Уровень 36, Воронеж, Russian Federation
13 июня 2022
Понравилось про кардридер и особенно про карту,которая лежит внутри кардридера. Просто идеальное сравнение!!!!
Greatsky Уровень 37, Беларусь
28 декабря 2021
Не знаю кому - там что понятно – но 1) InputStream extends Object класс является абстрактным – есть методы читающие байты 2) Reader extends Object - является абстрактным – есть методы читающие символы 3) BufferedReader extends Reader класс не абстрактный, есть методы читающие символы 4) InputStreamReader extends Reader класс не абстрактный, есть методы читающие символы 5) класс System extends Object – со статической переменной System.in которая имеет тип InputStream – итого имеем: для чтения байт есть класс InputStream и методы, для чтения символов класс BufferedReader и методы, напрашивается вопрос на кой черт нужен InputStreamReader, может я не прав, но мне кажется не хватает тут везде фразы чтобы читать символы как минимум из консоли, а консоль это InputStream, а как максимум другие потоки которые все имеют почему ( я еще пока не знаю. наверное исторически сложилось ) тип именно InputStream, поэтому и нужен адаптер inputstreamreader ну и наверное что бы на основе потом сделали FileReader extends InputStreamReader, к тому же внутри класса InputStreamReader вовсе не вызываются методы переданного ему переменной потока (что логично), поэтому это адаптер будет посложнее обычного
"Почему бы и да" Уровень 33
22 ноября 2021
Респектую, пример очень удачный
Виталий Уровень 47, Москва, Россия
25 октября 2021
Как же хорошо написано!
Maksim Tatarintsev Уровень 37, Москва
22 сентября 2021
Уровень 24, Россия
22 мая 2021
В качестве InputStream мы используем уже ставшую привычной переменную System.in InputStreamReader inputStreamReader = new InputStreamReader(System.in); Поскажите каким образом System.in запихнули в конструктор InputStreamReader если там принимаются обьекты типа inputStream? Класс System ни от чего не наследуется и ничего не имплементит
Иван Борзов Уровень 22, Тула, Россия
8 мая 2021
Мне одному показалось, что пример с InputSream и Reader не добавляет пониммания? Ну есть три метода у одного класса, три метода у другого класса... А потом раз - магия, и мы "адаптируем" и у нас уже три совместных метода))) А как же разобрать вместо "магии" конкретную реализацию?? Я полез в код смотреть... и непросто там все. Без стакана не разобраться. Зачем этот пример нужен, если он необъясним?
Akmaljon Jamoliddinov Уровень 20
14 марта 2021
Anonymous #2297535 Уровень 22, Северодвинск, Россия
13 марта 2021
"Чтобы сделать матрешку, надо сделать матрешку.." (с) ТХ