Hei! I dag skal vi berøre et viktig nytt emne: designmønstre . Hva er disse mønstrene? Jeg tror du må kjenne til uttrykket " ikke oppfinn hjulet på nytt ". I programmering, som på mange andre områder, er det et stort antall vanlige situasjoner. Etter hvert som programvareutviklingen har utviklet seg, er det laget ferdige løsninger som fungerer for hver av dem. Disse løsningene kalles designmønstre. Etter konvensjon er et mønster en løsning formulert slik: "hvis du trenger å gjøre X i programmet ditt, så er dette den beste måten å gjøre det på". Det er mange mønstre. Den utmerkede boken "Head First Design Patterns", som du definitivt bør bli kjent med, er dedikert til dem. Kort sagt består et mønster av et felles problem og en tilsvarende løsning som kan betraktes som en slags standard. I dagens leksjon vil vi møte et av disse mønstrene: Adapter. Navnet sier alt, og du har møtt adaptere mange ganger i det virkelige liv. Noen av de vanligste adapterene er kortleserne som mange datamaskiner og bærbare datamaskiner har. Anta at vi har et slags minnekort. Så hva er problemet? Den vet ikke hvordan den skal samhandle med datamaskinen. De deler ikke et felles grensesnitt. Datamaskinen har en USB-port, men vi kan ikke sette inn minnekortet i den. Kortet kan ikke kobles til datamaskinen, så vi kan ikke lagre bildene, videoene og andre dataene våre. En kortleser er en adapter som løser dette problemet. Tross alt har den en USB-kabel! I motsetning til selve kortet, kan kortleseren kobles til datamaskinen. De deler et felles grensesnitt med datamaskinen: USB. La oss se hvordan dette ser ut i praksis:
public interface USB {
void connectWithUsbCable();
}
Dette er vårt USB-grensesnitt med bare én metode for tilkobling via 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!");
}
}
Dette er vår klasse som representerer minnekortet. Den har allerede de 2 metodene vi trenger, men her er problemet: Den implementerer ikke USB-grensesnittet. Kortet kan ikke settes inn i USB-porten.
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();
}
}
Og her er adapteren vår! Hva gjørCardReader
klasse gjøre og hva gjør det til en adapter? Det hele er enkelt. Klassen som tilpasses (MemoryCard) blir et av adapterens felt. Dette gir mening. Når vi setter et minnekort i en kortleser i det virkelige liv, blir det også en del av det. I motsetning til minnekortet deler adapteren et grensesnitt med datamaskinen. Den har en USB-kabel, dvs. den kan kobles til andre enheter via USB. Det er derfor vår CardReader-klasse implementerer USB-grensesnittet. Men hva skjer egentlig i denne metoden? Akkurat det vi trenger for å skje! Adapteren delegerer arbeidet til minnekortet vårt. Faktisk gjør ikke adapteren noe selv. En kortleser har ingen uavhengig funksjonalitet. Dens jobb er bare å koble sammen datamaskinen og minnekortet for å la kortet gjøre jobben sin - kopiere filer!connectWithUsbCable()
metode) for å dekke minnekortets "behov". La oss lage et klientprogram som vil simulere en person som ønsker å kopiere data fra et minnekort:
public class Main {
public static void main(String[] args) {
USB cardReader = new CardReader(new MemoryCard());
cardReader.connectWithUsbCable();
}
}
Så hva fikk vi? Konsoll utgang:
Memory card successfully inserted!
The data has been copied to the computer!
Utmerket. Vi nådde målet vårt! Her er en lenke til en video med informasjon om adaptermønsteret:
Leser og forfatter abstrakt klasser
Nå skal vi gå tilbake til favorittaktiviteten vår: lære om et par nye klasser for arbeid med input og output :) Jeg lurer på hvor mange vi allerede har lært om. I dag skal vi snakke omReader
og Writer
klasser. Hvorfor akkurat disse klassene? Fordi de er relatert til vår forrige del om adaptere. La oss undersøke dem mer detaljert. Vi begynner med Reader
. Reader
er en abstrakt klasse, så vi vil ikke kunne lage objekter eksplisitt. Men du er faktisk allerede kjent med det! Tross alt er du godt kjent med klassene BufferedReader
og InputStreamReader
, som er dens etterkommere :)
public class BufferedReader extends Reader {
…
}
public class InputStreamReader extends Reader {
…
}
Klassen InputStreamReader
er en klassisk adapter. Som du sikkert husker, kan vi sende et InputStream
objekt til konstruktøren. For å gjøre dette bruker vi vanligvis variabelen System.in
:
public static void main(String[] args) {
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
Men hva InputStreamReader
gjør? Som alle adaptere konverterer den ett grensesnitt til et annet. I dette tilfellet, grensesnittet InputStream
til Reader
grensesnittet. I første omgang har vi InputStream
klassen. Det fungerer bra, men du kan bare bruke det til å lese individuelle byte. I tillegg har vi en Reader
abstrakt klasse. Den har noen veldig nyttige funksjoner - den vet hvordan den skal lese tegn! Vi trenger absolutt denne evnen. Men her står vi overfor det klassiske problemet som vanligvis løses av adaptere – inkompatible grensesnitt. Hva betyr det? La oss ta en titt på Oracle-dokumentasjonen. Her er metodene til klassen InputStream
. Et sett med metoder er nøyaktig hva et grensesnitt er. Som du kan se, har denne klassen enread()
metode (faktisk noen få varianter), men den kan bare lese byte: enten individuelle byte eller flere byte ved å bruke en buffer. Men dette alternativet passer ikke oss - vi vil lese karakterer. Vi trenger funksjonaliteten som allerede er implementert i Reader
abstraktklassen . Dette kan vi også se i dokumentasjonen. Men grensesnittene InputStream
og Reader
er inkompatible! Som du kan se, har hver implementering av read()
metoden forskjellige parametere og returverdier. Og det er her vi trenger InputStreamReader
! Den vil fungere som en adapter mellom klassene våre. Som i eksempelet med kortleseren, som vi vurderte ovenfor, legger vi en forekomst av at klassen er tilpasset "inne" i adapterklassen, dvs. vi sender en til dens konstruktør. I forrige eksempel la vi et MemoryCard
objekt inni CardReader
. Nå sender vi et InputStream
objekt til InputStreamReader
konstruktøren! Vi bruker vår kjente System.in
variabel som InputStream
:
public static void main(String[] args) {
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
Og faktisk, ser vi på dokumentasjonen for InputStreamReader
, kan vi se at tilpasningen lyktes :) Nå har vi metoder for å lese karakterer til vår disposisjon. Og selv om objektet vårt System.in
(strømmen bundet til tastaturet) i utgangspunktet ikke tillot dette, løste språkets skapere dette problemet ved å implementere adaptermønsteret. Den Reader
abstrakte klassen, som de fleste I/O-klasser, har en tvillingbror — Writer
. Den har den samme store fordelen som Reader
— den gir et praktisk grensesnitt for å jobbe med karakterer. Med utgangsstrømmer ser problemet og løsningen det samme ut som med inngangsstrømmer. Det er en OutputStream
klasse som bare kan skrive bytes, det er enWriter
abstrakt klasse som vet hvordan man jobber med tegn, og det er to inkompatible grensesnitt. Dette problemet er igjen løst av adaptermønsteret. Vi bruker OutputStreamWriter
klassen for enkelt å tilpasse de to grensesnittene til Writer
og OutputStream
klassene til hverandre. Etter å ha sendt en OutputStream
bytestrøm til konstruktøren, kan vi bruke en OutputStreamWriter
til å skrive tegn i stedet for byte!
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();
}
}
Vi skrev tegnet med kode 32144 (綐) til filen vår, og eliminerte behovet for å jobbe med bytes :) Det var det for i dag. Vi sees i neste leksjoner! :)
GO TO FULL VERSION