Cześć! Dzisiaj poruszymy ważny nowy temat: wzorce projektowe . Jakie są te wzorce? Myślę, że musisz znać wyrażenie „ nie wymyślaj koła na nowo ”. W programowaniu, podobnie jak w wielu innych dziedzinach, istnieje duża liczba typowych sytuacji. Wraz z rozwojem oprogramowania stworzono gotowe rozwiązania, które działają dla każdego z nich. Rozwiązania te nazywane są wzorcami projektowymi. Zgodnie z konwencją wzorzec to rozwiązanie sformułowane w następujący sposób: „jeśli musisz zrobić X w swoim programie, to jest to najlepszy sposób, aby to zrobić”. Istnieje wiele wzorów. Im dedykowana jest znakomita książka „Head First Design Patterns”, z którą zdecydowanie warto się zapoznać.
Mówiąc zwięźle, wzorzec składa się ze wspólnego problemu i odpowiadającego mu rozwiązania, które można uznać za rodzaj standardu. W dzisiejszej lekcji poznamy jeden z tych wzorców: Adapter. Jego nazwa mówi sama za siebie, a adaptery napotkałeś wiele razy w prawdziwym życiu. Niektóre z najczęstszych adapterów to czytniki kart, które są dostępne w wielu komputerach i laptopach.
Załóżmy, że mamy jakąś kartę pamięci. Więc w czym problem? Nie wie, jak wchodzić w interakcje z komputerem. Nie mają wspólnego interfejsu. Komputer ma port USB, ale nie możemy włożyć do niego karty pamięci. Karty nie można podłączyć do komputera, więc nie możemy zapisać naszych zdjęć, filmów i innych danych. Czytnik kart to adapter, który rozwiązuje ten problem. W końcu ma kabel USB! W przeciwieństwie do samej karty, czytnik kart można podłączyć do komputera. Dzielą wspólny interfejs z komputerem: USB. Zobaczmy, jak to wygląda w praktyce:
Zestaw metod jest dokładnie tym, czym jest interfejs. Jak widać, ta klasa ma
Jednak interfejsy
I choć nasz


public interface USB {
void connectWithUsbCable();
}
To jest nasz interfejs USB z tylko jedną metodą łączenia przez USB.
public class MemoryCard {
public void insert() {
System.out.println("Memory card successfully inserted!");
}
public void copyData() {
System.out.println("The data has been copied to the computer!");
}
}
To jest nasza klasa reprezentująca kartę pamięci. Ma już 2 metody, których potrzebujemy, ale oto problem: nie implementuje interfejsu USB. Nie można włożyć karty do portu 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();
}
}
A oto nasz adapter! Co znaczyCardReader
class zrobić i co dokładnie czyni go adapterem? To wszystko jest proste. Adaptowana klasa (MemoryCard) staje się jednym z pól adaptera. To ma sens. Kiedy w prawdziwym życiu umieszczamy kartę pamięci w czytniku kart, staje się ona również jego częścią. W przeciwieństwie do karty pamięci adapter współdzieli interfejs z komputerem. Posiada kabel USB, czyli można go podłączyć do innych urządzeń przez USB. Dlatego nasza klasa CardReader implementuje interfejs USB. Ale co dokładnie dzieje się w tej metodzie? Dokładnie to, co musi się wydarzyć! Adapter deleguje pracę na naszą kartę pamięci. Rzeczywiście, adapter sam nic nie robi. Czytnik kart nie posiada żadnej niezależnej funkcjonalności. Jego zadaniem jest jedynie połączenie komputera z kartą pamięci, aby karta mogła wykonać swoją pracę — kopiowanie plików!connectWithUsbCable()
metoda), aby spełnić „potrzeby” karty pamięci. Stwórzmy program kliencki, który będzie symulował osobę, która chce skopiować dane z karty pamięci:
public class Main {
public static void main(String[] args) {
USB cardReader = new CardReader(new MemoryCard());
cardReader.connectWithUsbCable();
}
}
Więc co dostaliśmy? Wyjście konsoli:
Memory card successfully inserted!
The data has been copied to the computer!
Doskonały. Osiągnęliśmy nasz cel! Oto link do filmu z informacjami o wzorze adaptera:
Klasy abstrakcyjne Reader i Writer
Teraz wrócimy do naszej ulubionej czynności: poznawania kilku nowych klas do pracy z danymi wejściowymi i wyjściowymi :) Ciekawe, o ilu już się nauczyliśmy. Dzisiaj porozmawiamy o klasachReader
i Writer
. Dlaczego akurat te zajęcia? Ponieważ są one powiązane z naszą poprzednią sekcją dotyczącą adapterów. Przeanalizujmy je bardziej szczegółowo. Zaczniemy od Reader
. Reader
jest klasą abstrakcyjną, więc nie będziemy mogli jawnie tworzyć obiektów. Ale tak naprawdę już go znasz! W końcu dobrze znasz klasy BufferedReader
i InputStreamReader
, które są jej potomkami :)
public class BufferedReader extends Reader {
…
}
public class InputStreamReader extends Reader {
…
}
Klasa InputStreamReader
jest klasycznym adapterem. Jak zapewne pamiętasz, możemy przekazać InputStream
obiekt do jego konstruktora. W tym celu zwykle używamy System.in
zmiennej:
public static void main(String[] args) {
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
Ale co InputStreamReader
robi? Jak każdy adapter, konwertuje jeden interfejs na inny. W tym przypadku InputStream
interfejs do Reader
interfejsu. Na początek mamy InputStream
klasę. Działa dobrze, ale można go używać tylko do odczytu pojedynczych bajtów. Ponadto mamy Reader
klasę abstrakcyjną. Ma kilka bardzo przydatnych funkcji — wie, jak czytać znaki! Z pewnością potrzebujemy tej umiejętności. Ale tutaj mamy do czynienia z klasycznym problemem zwykle rozwiązywanym przez adaptery — niekompatybilnymi interfejsami. Co to znaczy? Rzućmy okiem na dokumentację Oracle. Oto metody klasy InputStream
. 
read()
metoda (w rzeczywistości kilka wariantów), ale może odczytywać tylko bajty: albo pojedyncze bajty, albo kilka bajtów przy użyciu bufora. Ale ta opcja nam nie odpowiada — chcemy czytać znaki. Potrzebujemy funkcjonalności, która jest już zaimplementowana w Reader
klasie abstrakcyjnej . Widzimy to również w dokumentacji. 
InputStream
i Reader
są niekompatybilne! Jak widać, każda implementacja metody read()
ma inne parametry i zwracane wartości. I tutaj potrzebujemy InputStreamReader
! Będzie pełnić rolę adaptera między naszymi klasami. Podobnie jak w przykładzie z czytnikiem kart, który rozważaliśmy powyżej, instancję adaptowanej klasy umieszczamy „wewnątrz” klasy adaptera, czyli przekazujemy ją jej konstruktorowi. W poprzednim przykładzie umieściliśmy MemoryCard
obiekt wewnątrz CardReader
. Teraz przekazujemy InputStream
obiekt do InputStreamReader
konstruktora! Używamy naszej znanej System.in
zmiennej jako InputStream
:
public static void main(String[] args) {
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
I rzeczywiście, patrząc na dokumentację do InputStreamReader
, widzimy, że adaptacja się powiodła :) Teraz mamy do dyspozycji metody odczytywania znaków. 
System.in
obiekt (strumień powiązany z klawiaturą) początkowo na to nie pozwalał, twórcy języka rozwiązali ten problem, implementując wzorzec adaptera. Klasa Reader
abstrakcyjna, podobnie jak większość klas I/O, ma brata bliźniaka — Writer
. Ma tę samą dużą zaletę, co Reader
— zapewnia wygodny interfejs do pracy z postaciami. W przypadku strumieni wyjściowych problem i jego rozwiązanie wyglądają tak samo jak w przypadku strumieni wejściowych. Jest OutputStream
klasa, która może zapisywać tylko bajty, jest aWriter
Klasa abstrakcyjna, która wie, jak pracować ze znakami, i istnieją dwa niekompatybilne interfejsy. Ten problem ponownie rozwiązuje wzorzec adaptera. Używamy klasy, aby łatwo dostosować do siebie OutputStreamWriter
dwa interfejsy klas Writer
i . OutputStream
Po przekazaniu OutputStream
strumienia bajtów do konstruktora, możemy użyć an OutputStreamWriter
do zapisywania znaków, a nie bajtów!
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();
}
}
Wpisaliśmy znak o kodzie 32144 (綐) do naszego pliku, eliminując konieczność pracy z bajtami :) To tyle na dziś. Do zobaczenia na kolejnych lekcjach! :)
GO TO FULL VERSION