Привет! Сегодня мы затронем важную новую тему — паттерны, или по-другому — шаблоны проектирования. Что же такое паттерны? Думаю, тебе известно выражение «не надо изобретать велосипед». В программировании, как и во многих других сферах, есть большое количество типовых ситуаций. Для каждой из них в процессе развития программирования создавались готовые работающие решения. Это и есть шаблоны проектирования. Условно говоря, паттерн — это некий пример, который предлагает решение ситуации вида: «если в вашей программе нужно сделать то-то, как это лучше всего сделать». Паттернов очень много, им посвящена отличная книга «Изучаем шаблоны проектирования», с которой обязательно нужно ознакомиться. Паттерн проектирования «Адаптер» - 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 — 綐, таким образом избавившись от необходимости работать с байтами :) На этом на сегодня все, до встречи на следующих лекциях! :)