Вітання! Сьогодні ми торкнемося важливої нової теми — патерни, або по-іншому — шаблонів проектування . Що ж таке патерни? Думаю, тобі відомий вислів «не треба винаходити велосипед». У програмуванні, як і багатьох інших сферах, є велика кількість типових ситуацій. Для кожної їх у процесі розвитку програмування створювалися готові діючі рішення. Це і є шаблони проектування. Умовно кажучи, патерн — це приклад, який пропонує вирішення ситуації виду: «якщо у вашій програмі потрібно зробити те, як це найкраще зробити». Паттернов дуже багато, їм присвячено чудову книгу «Вивчаємо шаблони проектування», з якою обов'язково потрібно ознайомитися. Якщо говорити максимально коротко, патерн складається з поширеної проблеми та її вирішення, яке вже можна вважати стандартом. У сьогоднішній лекції ми познайомимося з одним із таких патернів під назвою «Адаптер». Назва в нього говорить, і ти неодноразово зустрічався з адаптерами в реальному житті. Один із найпоширеніших адаптерів - кардрідери, якими забезпечені безліч комп'ютерів та ноутбуків. Уяви, що ми маємо якусь карту пам'яті. У чому проблема? У цьому, що вона вміє взаємодіяти з комп'ютером. Вони не мають спільного інтерфейсу. Комп'ютер має роз'єм 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("Данные скопированы на компьютер!");
}
}
Це наш клас, який реалізує картку пам'яті. У ньому вже є два необхідні нам способу, але ось біда: інтерфейс 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();
}
}
Що ж у нас у результаті вийшло? Виведення в консоль:
Карта памяти успешно вставлена!
Данные скопированы на компьютер!
Відмінно, наше завдання успішно виконане! Ось кілька додаткових посилань з інформацією про патерн Адаптер:
- Відео Adapter Pattern - Design Patterns ;
- Паттерн проектування "Адаптер" / "Adapter" ;
- Шаблони проектування простою мовою .
Абстрактні класи 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
. Сукупність методів - це і є інтерфейс. Як бачиш, методread()
у цього класу є (навіть у кількох варіантах), але читати може лише байти: або окремі байти, або кілька байт з використанням буфера. Нам такий варіант не підходить – ми хочемо читати символи. Потрібний нам функціонал уже реалізовано в абстрактному класіReader
. Це також можна побачити в документації. Однак інтерфейси 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
ми побачимо, що «адаптація» пройшла успішно :) Тепер у нашому розпорядженні є методи, які дозволяють нам читати символи. І хоча споконвічно наш об'єкт 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 — 綐, таким чином позбавившись необхідності працювати з байтами :) На цьому на сьогодні все, до зустрічі на наступних лекціях! :)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ