Hej! Idag kommer vi att beröra ett viktigt nytt ämne: designmönster . Vilka är dessa mönster? Jag tror att du måste känna till uttrycket " uppfinn inte hjulet på nytt ". Inom programmering, liksom inom många andra områden, finns det ett stort antal vanliga situationer. I takt med att mjukvaruutvecklingen har utvecklats har färdiga lösningar som fungerar skapats för var och en av dem. Dessa lösningar kallas designmönster. Enligt konventionen är ett mönster någon lösning formulerad så här: "om du behöver göra X i ditt program, så är detta det bästa sättet att göra det". Det finns många mönster. Den utmärkta boken "Head First Design Patterns", som du definitivt borde bekanta dig med, är tillägnad dem. Kortfattat består ett mönster av ett gemensamt problem och en motsvarande lösning som kan betraktas som en sorts standard. I dagens lektion kommer vi att möta ett av dessa mönster: Adapter. Namnet säger allt, och du har stött på adaptrar många gånger i verkligheten. Några av de vanligaste adaptrarna är kortläsarna som många datorer och bärbara datorer har. Anta att vi har något slags minneskort. Så vad är problemet? Den vet inte hur man interagerar med datorn. De delar inte ett gemensamt gränssnitt. Datorn har en USB-port, men vi kan inte sätta in minneskortet i den. Kortet kan inte anslutas till datorn, så vi kan inte spara våra foton, videor och annan data. En kortläsare är en adapter som löser detta problem. Den har trots allt en USB-kabel! Till skillnad från själva kortet kan kortläsaren kopplas in i datorn. De delar ett gemensamt gränssnitt med datorn: USB. Låt oss se hur detta ser ut i praktiken:
public interface USB {
void connectWithUsbCable();
}
Detta är vårt USB-gränssnitt med bara en metod för att ansluta 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!");
}
}
Det här är vår klass som representerar minneskortet. Den har redan de två metoderna vi behöver, men här är problemet: Den implementerar inte USB-gränssnittet. Kortet kan inte sättas in 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();
}
}
Och här är vår adapter! Vad görCardReader
klass gör och vad exakt gör det till en adapter? Det hela är enkelt. Klassen som anpassas (MemoryCard) blir ett av adapterns fält. Detta är vettigt. När vi stoppar in ett minneskort i en kortläsare i verkligheten blir det också en del av det. Till skillnad från minneskortet delar adaptern ett gränssnitt med datorn. Den har en USB-kabel, dvs den kan kopplas till andra enheter via USB. Det är därför vår CardReader-klass implementerar USB-gränssnittet. Men vad exakt händer i denna metod? Exakt vad vi behöver för att hända! Adaptern delegerar arbetet till vårt minneskort. Faktum är att adaptern inte gör någonting själv. En kortläsare har ingen oberoende funktion. Dess uppgift är bara att ansluta datorn och minneskortet för att kortet ska kunna göra sitt jobb — kopiera filer!connectWithUsbCable()
metod) för att tillgodose minneskortets "behov". Låt oss skapa något klientprogram som kommer att simulera en person som vill kopiera data från ett minneskort:
public class Main {
public static void main(String[] args) {
USB cardReader = new CardReader(new MemoryCard());
cardReader.connectWithUsbCable();
}
}
Så vad fick vi? Konsolutgång:
Memory card successfully inserted!
The data has been copied to the computer!
Excellent. Vi uppnådde vårt mål! Här är en länk till en video med information om adaptermönstret:
Läsare och författare abstrakt klasser
Nu ska vi återgå till vår favoritaktivitet: lära oss om ett par nya klasser för att arbeta med input och output :) Jag undrar hur många vi redan har lärt oss om. Idag ska vi prata omReader
och Writer
klasser. Varför just dessa klasser? Eftersom de är relaterade till vårt tidigare avsnitt om adaptrar. Låt oss undersöka dem mer i detalj. Vi börjar med Reader
. Reader
är en abstrakt klass, så vi kommer inte att kunna skapa objekt explicit. Men du är faktiskt redan bekant med det! Du är trots allt väl bekant med klasserna BufferedReader
och InputStreamReader
, som är dess ättlingar :)
public class BufferedReader extends Reader {
…
}
public class InputStreamReader extends Reader {
…
}
Klassen InputStreamReader
är en klassisk adapter. Som du säkert minns kan vi skicka ett InputStream
objekt till dess konstruktör. För att göra detta använder vi vanligtvis System.in
variabeln:
public static void main(String[] args) {
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
Men vad InputStreamReader
gör? Som varje adapter konverterar den ett gränssnitt till ett annat. I det här fallet InputStream
gränssnittet till Reader
gränssnittet. Till en början har vi InputStream
klassen. Det fungerar bra, men du kan bara använda det för att läsa enskilda byte. Dessutom har vi en Reader
abstrakt klass. Den har några mycket användbara funktioner - den vet hur man läser tecken! Vi behöver verkligen denna förmåga. Men här står vi inför det klassiska problemet som vanligtvis löses med adaptrar - inkompatibla gränssnitt. Vad betyder det? Låt oss ta en titt på Oracle-dokumentationen. Här är klassens metoder InputStream
. En uppsättning metoder är precis vad ett gränssnitt är. Som du kan se har denna klass enread()
metod (faktiskt ett fåtal varianter), men den kan bara läsa byte: antingen enskilda byte eller flera byte med en buffert. Men det här alternativet passar oss inte – vi vill läsa karaktärer. Vi behöver den funktionalitet som redan är implementerad i Reader
abstraktklassen . Det kan vi också se i dokumentationen. Men gränssnitten InputStream
och Reader
är inkompatibla! Som du kan se har varje implementering av read()
metoden olika parametrar och returvärden. Och det är här vi behöver InputStreamReader
! Den kommer att fungera som en adapter mellan våra klasser. Som i exemplet med kortläsaren, som vi övervägde ovan, lägger vi en instans av att klassen är anpassad "inuti" adapterklassen, dvs vi skickar en till dess konstruktor. I föregående exempel satte vi ett MemoryCard
objekt inuti CardReader
. Nu skickar vi ett InputStream
objekt till InputStreamReader
konstruktören! Vi använder vår välbekanta System.in
variabel som InputStream
:
public static void main(String[] args) {
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
Och faktiskt, när vi tittar på dokumentationen för InputStreamReader
, kan vi se att anpassningen lyckades :) Nu har vi metoder för att läsa karaktärer till vårt förfogande. Och även om vårt System.in
objekt (strömmen bunden till tangentbordet) från början inte tillät detta, löste språkets skapare detta problem genom att implementera adaptermönstret. Den Reader
abstrakta klassen, som de flesta I/O-klasser, har en tvillingbror — Writer
. Det har samma stora fördel som Reader
— det ger ett bekvämt gränssnitt för att arbeta med karaktärer. Med utströmmar ser problemet och dess lösning ut på samma sätt som med ingångsströmmar. Det finns en OutputStream
klass som bara kan skriva bytes, det finns enWriter
abstrakt klass som vet hur man arbetar med karaktärer, och det finns två inkompatibla gränssnitt. Detta problem löses återigen av adaptermönstret. Vi använder OutputStreamWriter
klassen för att enkelt anpassa de två gränssnitten för klasserna Writer
och OutputStream
till varandra. Efter att ha skickat en OutputStream
byteström till konstruktorn kan vi använda en OutputStreamWriter
för att skriva tecken snarare än 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 tecknet med koden 32144 (綐) till vår fil, vilket eliminerade behovet av att arbeta med bytes :) Det var allt för idag. Vi ses på nästa lektion! :)
GO TO FULL VERSION