JavaRush/Java блог/Java Developer/Паттерн проектирования Адаптер
Автор
Pavlo Plynko
Java-разработчик в CodeGym

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

Статья из группы Java Developer
участников
Привет! Сегодня мы затронем важную новую тему — паттерны, или по-другому — шаблоны проектирования. Что же такое паттерны? Думаю, тебе известно выражение «не надо изобретать велосипед». В программировании, как и во многих других сферах, есть большое количество типовых ситуаций. Для каждой из них в процессе развития программирования создавались готовые работающие решения. Это и есть шаблоны проектирования. Условно говоря, паттерн — это некий пример, который предлагает решение ситуации вида: «если в вашей программе нужно сделать то-то, как это лучше всего сделать». Паттернов очень много, им посвящена отличная книга «Изучаем шаблоны проектирования», с которой обязательно нужно ознакомиться. Паттерн проектирования «Адаптер» - 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 — 綐, таким образом избавившись от необходимости работать с байтами :) На этом на сегодня все, до встречи на следующих лекциях! :)
Комментарии (70)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Максим Li Java Developer
25 марта, 05:05
Хорошая статья
Islam Yunusov
Уровень 31
28 ноября 2023, 16:01
Мне стало не понятно вот это высказывание: "Есть класс OutputStream, который умеет записывать только байты; есть абстрактный класс Writer, который умеет работать с символами, и есть два несовместимых интерфейса". Что подразумевается под интерфейсами, если мы говорим про классы? Но потом почитав ещё разок обратил внимание на это: "Совокупность методов — это и есть интерфейс". Честно говоря у меня было несколько иное представление об интерфейсах, нежели совокупность методов в том числе самих классов, ну да ладно)
Boldik Sultan
Уровень 34
4 декабря 2023, 22:40
Интерфейсом в программировании называют и абстрактный тип данных, в котором мы описываем поведение, и совокупность методов для взаимодействия с полями класса. Те же самые геттеры и сеттеры в самом примитивном pojo являются его интерфейсом.
Skotique
Уровень 35
28 августа 2023, 15:11
банку тунца этому автору
Ant
Уровень 29
2 августа 2023, 12:47
Может кому пригодится, хорошая книга Швец Александр - Погружение в паттерны проектирования - 2021, можно найти в открытом доступе
Lo
Уровень 35
Expert
23 октября 2023, 15:39
Спасибо! Нашла, скачала.
Dmitry Vidonov
Уровень 29
Expert
25 октября 2023, 13:04
Спасибо за совет!
Ислам
Уровень 33
7 июля 2023, 10:10
Nice
BESTrOFF
Уровень 32
29 мая 2023, 17:33
А в чем отличие между адаптером и оберткой?
15 июня 2023, 19:48
Адаптер меняет интерфейс существующего объекта. Декоратор улучшает другой объект без изменения его интерфейса. Причём Декоратор поддерживает рекурсивную вложенность, чего не скажешь об Адаптере. Адаптер предоставляет классу альтернативный интерфейс. Декоратор предоставляет расширенный интерфейс.
15 июня 2023, 19:50
Адаптер (Adapter) и обертка (Wrapper) - это два различных паттерна проектирования, которые применяются для изменения или расширения функциональности существующих классов или объектов. Вот их отличия: 1. Цель: - Адаптер: Адаптер используется для обеспечения совместимости между двумя непохожими интерфейсами или классами. - Обертка: Обертка используется для добавления дополнительного поведения или функциональности к существующему объекту без изменения его интерфейса. 2. Интерфейс: - Адаптер: Адаптер реализует интерфейс, ожидаемый клиентом, на основе интерфейса или класса, который нужно адаптировать. - Обертка: Обертка оборачивает существующий объект и предоставляет тот же интерфейс, что и обернутый объект. 3. Взаимодействие с клиентом: - Адаптер: Клиент взаимодействует с адаптером так, как если бы это был ожидаемый интерфейс. - Обертка: Клиент взаимодействует с оберткой так, как с обычным объектом, но может получить дополнительную функциональность. 4. Применение: - Адаптер: Адаптер применяется, когда требуется взаимодействие между двумя непохожими интерфейсами или классами, чтобы они могли работать вместе. - Обертка: Обертка применяется, когда нужно добавить или изменить функциональность существующего объекта без его модификации. В целом, оба паттерна позволяют изменять поведение объектов, но адаптер сосредоточен на обеспечении совместимости между различными интерфейсами, в то время как обертка добавляет дополнительное поведение к существующему объекту.
KeLL
Уровень 29
13 июля 2022, 23:34
а почему нельзя memorycard реализовать от usb и сделать тоже самое тоько проще?!
Никита Ильин
Уровень 28
3 августа 2022, 14:34
Потому что пример из реальной жизни, как ты карту памяти собираешься у USB разъем вставлять?)
Никита Ильин
Уровень 28
3 августа 2022, 14:35
класс может быть в библиотеке какой-нибудь быть, и если ты его изменишь, то сломаешь либу, поэтому нужен адаптер
Dim
Уровень 38
15 апреля 2023, 06:44
Потому что классы MemoryCard и USB не являются разработанными нами классами. Мы же не можем менять код классов Reader и InputStream, так же и здесь. Как не можем менять электронную начинку у настоящей карты памяти. Конечно, если мы - её разработчики, то теоретически (технологически - нет), мы можем её вскрыть, добавить микросхему USB-драйвера и проводами соединить цепи передачи данных USB с разъёмом в компьютере, и всё будет работать. Вот только нафига этот колхоз нужен.
Marianna
Уровень 36
13 июня 2022, 14:10
Понравилось про кардридер и особенно про карту,которая лежит внутри кардридера. Просто идеальное сравнение!!!!
Greatsky future developer в future developer
28 декабря 2021, 17:16
Не знаю кому - там что понятно – но 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 вовсе не вызываются методы переданного ему переменной потока (что логично), поэтому это адаптер будет посложнее обычного
Нейросеть
Уровень 41
7 января 2022, 15:47
"к тому же внутри класса InputStreamReader вовсе не вызываются методы переданного ему переменной потока (что логично)" А разве он что то ещё делает?
public int read() throws IOException {
        return sd.read();
    }
Ну да он не совсем напрямую использует поток,а через переменную класса StreamDecoder, который собсна как я понял и расшифровывает байты как символы. Но это лишь и доказывает что InputStreamReader не что иное как адаптер, ибо никакую собственную логику он не реализует, а лишь является переходником. "для чтения символов класс BufferedReader и методы, напрашивается вопрос на кой черт нужен InputStreamReader" BufferReader это класс оболочка и данные он читает исключительно из других потоков реализующих интерфейс Reader. Тебя наверное смутил метод read() в этом классе, когда ты не встретил там ни одного упоминания переменной in, в которой и хранится входящий поток. Но это всё потому, что read() в BufferReader читает не из потока ,а из своего буфера и когда буфер полностью прочитан, тогда вызывается метод fill(), который создаёт новый буфер и заполняет его из потока в переменной in(например из того же InputStreamReader`а).
if (nextChar >= nChars) {
                    fill();
comrade_b
Уровень 39
3 июня 2022, 14:14
Древние комменты, но все же. Наверное самое большое отличие, которое я вижу для себя такое:
InputStream - читает байты по одному (byte).
InputStreamReader - читает символы по одному (char).
BufferedReader - использует буффер, поэтому способен читать все построчно (String).
Отдельно Ридер вызвать не получится, класс обстрактный.
System.in просто статическая перменная.
Вообще я прилетел сюда с вводной лекции по адаптерам. И тут, в отличие от основной лекции, хотя бы пример понятный дали.
Владоs
Уровень 30
5 марта 2023, 20:13
InputStream читает байтики по одному, InputStreamReader считает чарочки под одному, а вот BufferedReader читает целые строки от начала строки до ее переноса, то есть метод readLine(), на то он и буфер, чтобы обращаться к буферу, это + к производительности.
"Почему бы и да"
Уровень 33
22 ноября 2021, 09:45
Респектую, пример очень удачный