Ders 10 / 19
Veri Erişim Soyutlaması
Aynı derlenmiş çağıran kümesi — nesne kabı, servis ve giriş noktası — bir depo arayüzünün iki ayrı gerçekleştirimiyle, hiç yeniden derlenmeden koşturuluyor. Çağıranın sınıf dosyası yalnız arayüzü anıyor; aynı çağrı bir gerçekleştirimde sorunsuz çalışırken ötekinde bir istisnayla düşüyor, çünkü arayüz sözü ile gerçekleştirim davranışı ayrı ve kap bu ayrımı kapatmıyor, yalnız gizliyor.
İçindekiler
Önceki ders bir isteğin verisinin doğru olup olmadığını ölçtü, ama o veri işlendikten sonra nereye yazıldığı hiç sorulmadı. Bu ders bunu ele alıyor: bir depo arayüzünün gerçekleştirimini kabın nasıl sağladığını, ve aynı çağıranın yeniden derlenmeden iki ayrı gerçekleştirimle nasıl koşabildiğini ölçüyor. Depo kavramı, bir iş mantığının kalıcılık ayrıntılarından bağımsız kalması M16/K05 Veri Erişim Katmanı kursunda kuruldu ve burada tekrarlanmıyor; bu dersin sorusu ondan ayrı — kabın bu arayüzü hangi düzenekle doldurduğu.
Veritabanı burada da modellenir: gerçek bir bağlantı açılmaz, gerçek bir SQL ifadesi
çalıştırılmaz. Depo yalnız iki yöntem taşıyan bir arayüz — bir miktarı okumak, bir miktarı
yazmak — ve bu iki yöntemin arkasında ne durduğu çağıran için görünmez. Görünmezliğin ne
kadar gerçek olduğunu göstermenin tek yolu, aynı çağıranı gerçekten iki ayrı gerçekleştirimle
koşturmaktır; bu derste kullanılan yöntem tam olarak bu.
Önceki derslerde “aynı davranışı iki yolla koşturmak” kabın kurduğu nesneyle elle kurulan nesneyi karşılaştırmak anlamına geliyordu. Bu ders aynı ilkeyi bir adım öteye taşıyor: burada karşılaştırılan iki nesne değil, aynı çağıran kodun iki ayrı gerçekleştirime karşı koşturulmasıdır. Bir arayüzün asıl sınandığı yer de burası — bir sınıfın davranışını taklit etmek yetmiyor, aynı sözleşmeyi tutan tamamen farklı bir iç yapının da aynı çağıran kodu memnun edip etmediği görülmesi gerekiyor.
Aynı Çağıran, İki Ayrı Gerçekleştirim
- WV16. Bu ölçüm gerçek bir sınıf yolu ayrımını sahneye koyar: arayüz, çağıran kod ve her gerçekleştirim ayrı dizinlere derlenir, sonra çalışma zamanında farklı kombinasyonlarla bir araya getirilir. Bir sınıfın hangi sınıf yolunda gerekmek zorunda olduğu M08/K05 Derleme, Test ve Ekosistem kursunda ölçüldü; burada aynı düzenek arayüz–gerçekleştirim ayrımı için kullanılıyor, sayılar oradan tekrarlanmıyor.
- WV17.
Kap.kuryöntemi gerçekleştirim sınıfının adını bir dizge parametresi olarak alıyor veClass.forNameile buluyor; bu, çağıranın hiçbir gerçekleştirim sınıfını derleme zamanında anmadan bir örnek kurabilmesinin yolu.
Kap, Servis ve Ana sınıfları yalnız Depo arayüzüne karşı derleniyor — derleme sınıf
yoluna BellekDepo da DiziDepo da hiç girmiyor. Bu üç sınıf tek bir kez derleniyor, sonra
iki ayrı çalışma zamanı sınıf yoluyla, hiç yeniden derlenmeden koşturuluyor: birinde
gerçekleştirim BellekDepo, ötekinde DiziDepo. Aradaki fark yalnız kos çağrısına verilen
üçüncü sınıf yolu bileşeni ve Ana.main’e geçirilen gerçekleştirim adı.
// IkiGercek.java — ayni derlenmis cagiran, arayuzu degistirmeden iki ayri gerceklestirimle kosuyor
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
public class IkiGercek {
static Path yaz(Path dizin, String ad, String kaynak) throws IOException {
Files.createDirectories(dizin);
Path p = dizin.resolve(ad + ".java");
Files.writeString(p, kaynak);
return p;
}
static void derle(Path cikti, List<Path> sinifYolu, List<Path> kaynaklar) throws IOException {
Files.createDirectories(cikti);
List<String> arg = new ArrayList<>(List.of("-d", cikti.toString()));
if (!sinifYolu.isEmpty()) {
StringJoiner sj = new StringJoiner(File.pathSeparator);
for (Path p : sinifYolu) sj.add(p.toString());
arg.add("-classpath");
arg.add(sj.toString());
}
for (Path k : kaynaklar) arg.add(k.toString());
ToolProvider derleyici = ToolProvider.findFirst("javac").orElseThrow();
StringWriter cikisYazi = new StringWriter();
int sonuc = derleyici.run(new PrintWriter(cikisYazi), new PrintWriter(cikisYazi), arg.toArray(new String[0]));
if (sonuc != 0) throw new RuntimeException("derleme basarisiz: " + cikisYazi);
}
static String kos(List<Path> sinifYolu, String gerceklestirimAdi) {
URL[] url = new URL[sinifYolu.size()];
try {
for (int i = 0; i < url.length; i++) url[i] = sinifYolu.get(i).toUri().toURL();
try (URLClassLoader yukleyici = new URLClassLoader(url, IkiGercek.class.getClassLoader().getPlatformClassLoader())) {
Class<?> ana = Class.forName("Ana", true, yukleyici);
ByteArrayOutputStream tampon = new ByteArrayOutputStream();
PrintStream eski = System.out;
System.setOut(new PrintStream(tampon));
try {
ana.getMethod("main", String[].class).invoke(null, (Object) new String[]{gerceklestirimAdi});
} finally {
System.setOut(eski);
}
return tampon.toString().strip();
}
} catch (InvocationTargetException e) {
return "istisna: " + e.getCause().getClass().getSimpleName() + ": " + e.getCause().getMessage();
} catch (Throwable t) {
return "istisna: " + t.getClass().getSimpleName();
}
}
public static void main(String[] args) throws Exception {
Path kok = Files.createTempDirectory("ikigercek");
Path kaynak = kok.resolve("kaynak");
Path sArayuz = yaz(kaynak.resolve("arayuz"), "Depo",
"public interface Depo {\n"
+ " int miktarBul(String urun);\n"
+ " void miktarGuncelle(String urun, int miktar);\n"
+ "}\n");
Path sKap = yaz(kaynak.resolve("cagiran"), "Kap",
"public class Kap {\n"
+ " static Servis kur(String gerceklestirimAdi) throws ReflectiveOperationException {\n"
+ " Class<?> c = Class.forName(gerceklestirimAdi);\n"
+ " Depo depo = (Depo) c.getDeclaredConstructor().newInstance();\n"
+ " return new Servis(depo);\n"
+ " }\n"
+ "}\n");
Path sServis = yaz(kaynak.resolve("cagiran"), "Servis",
"public class Servis {\n"
+ " private final Depo depo;\n"
+ " public Servis(Depo depo) { this.depo = depo; }\n"
+ " public String uygula(String urun, int degisim) {\n"
+ " int mevcut = depo.miktarBul(urun);\n"
+ " int yeni = mevcut + degisim;\n"
+ " depo.miktarGuncelle(urun, yeni);\n"
+ " return urun + \": \" + mevcut + \" -> \" + yeni;\n"
+ " }\n"
+ "}\n");
Path sAna = yaz(kaynak.resolve("cagiran"), "Ana",
"public class Ana {\n"
+ " public static void main(String[] args) throws Exception {\n"
+ " Servis servis = Kap.kur(args[0]);\n"
+ " System.out.println(servis.uygula(\"vida\", 5));\n"
+ " System.out.println(servis.uygula(\"vida\", -2));\n"
+ " System.out.println(servis.uygula(\"civi\", 3));\n"
+ " }\n"
+ "}\n");
Path sBellek = yaz(kaynak.resolve("bellek"), "BellekDepo",
"import java.util.*;\n"
+ "public class BellekDepo implements Depo {\n"
+ " private final Map<String, Integer> veri = new HashMap<>();\n"
+ " public int miktarBul(String urun) { return veri.getOrDefault(urun, 0); }\n"
+ " public void miktarGuncelle(String urun, int miktar) { veri.put(urun, miktar); }\n"
+ "}\n");
Path sDizi = yaz(kaynak.resolve("dizi"), "DiziDepo",
"public class DiziDepo implements Depo {\n"
+ " private final String[] adlar = new String[1];\n"
+ " private final int[] miktarlar = new int[1];\n"
+ " private int dolu = 0;\n"
+ " public int miktarBul(String urun) {\n"
+ " for (int i = 0; i < dolu; i++) if (adlar[i].equals(urun)) return miktarlar[i];\n"
+ " return 0;\n"
+ " }\n"
+ " public void miktarGuncelle(String urun, int miktar) {\n"
+ " for (int i = 0; i < dolu; i++) if (adlar[i].equals(urun)) { miktarlar[i] = miktar; return; }\n"
+ " if (dolu == adlar.length) throw new IllegalStateException(\"depo kapasitesi dolu: \" + urun + \" eklenemedi\");\n"
+ " adlar[dolu] = urun; miktarlar[dolu] = miktar; dolu++;\n"
+ " }\n"
+ "}\n");
Path cArayuz = kok.resolve("cikti/arayuz");
Path cCagiran = kok.resolve("cikti/cagiran");
Path cBellek = kok.resolve("cikti/bellek");
Path cDizi = kok.resolve("cikti/dizi");
derle(cArayuz, List.of(), List.of(sArayuz));
derle(cCagiran, List.of(cArayuz), List.of(sKap, sServis, sAna));
derle(cBellek, List.of(cArayuz), List.of(sBellek));
derle(cDizi, List.of(cArayuz), List.of(sDizi));
System.out.println("-- ayni cagiran (Kap+Servis+Ana), BellekDepo gerceklestirimiyle --");
System.out.println(kos(List.of(cCagiran, cArayuz, cBellek), "BellekDepo"));
System.out.println("-- ayni cagiran, hic yeniden derlenmeden, DiziDepo gerceklestirimiyle --");
System.out.println(kos(List.of(cCagiran, cArayuz, cDizi), "DiziDepo"));
System.out.println("kusur turu: hata (istisna adiyla dustu, DiziDepo kapasitesini asan cagriyi reddetti)");
}
}
-- ayni cagiran (Kap+Servis+Ana), BellekDepo gerceklestirimiyle -- vida: 0 -> 5 vida: 5 -> 3 civi: 0 -> 3 -- ayni cagiran, hic yeniden derlenmeden, DiziDepo gerceklestirimiyle -- istisna: IllegalStateException: depo kapasitesi dolu: civi eklenemedi kusur turu: hata (istisna adiyla dustu, DiziDepo kapasitesini asan cagriyi reddetti)
Servis sınıfının kaynağında BellekDepo ya da DiziDepo adı hiçbir yerde geçmiyor —
yalnız Depo depo alanı var, ve o alanın türü arayüz. Kap.kur da aynı şekilde: aldığı tek
bilgi bir dizge, Class.forName ile bulduğu sınıfı Depo’ya döküyor. Bu üç sınıf ilk
derle çağrısında, hiçbir gerçekleştirimin sınıf yolunda bulunmadığı bir ortamda derleniyor,
ve derlenmiş hâlleri iki ayrı kos çağrısında tekrar kullanılıyor. Aradaki fark yalnız
kos’a verilen sınıf yolu listesinin üçüncü bileşeni.
Bu derlemenin gerçekten “yeniden derlenmeden” olduğunu doğrulayan şey, derle(cCagiran, ...)
çağrısının yalnız bir kez yapılması. cCagiran dizini bir kez üretiliyor ve iki kos
çağrısına da aynen veriliyor; ikinci kos çağrısı öncesinde Kap, Servis ya da Ana
kaynağına dönüp tekrar derle çağıran hiçbir satır yok. Gerçek bir kurulumda bu, bir
uygulamanın dağıtım paketinin (çağıran sınıfları içeren kısmı) bir kez üretilip, hangi
gerçekleştirimin bağlanacağının yalnız çalışma zamanı sınıf yoluyla belirlendiği anlamına
gelir — kaynak koddaki hiçbir değişiklik gerekmeden, hiçbir yeniden derleme adımı
çalıştırmadan, aynı .class dosyaları farklı bir depoya bağlanabiliyor.
Arayüz Sözü ile Gerçekleştirim Davranışı Ayrıdır
- WV18.
BellekDepobirHashMapüzerinde çalışıyor ve kaç farklıurunadı geldiğine bir sınır koymuyor.DiziDeposabit uzunlukta iki dizi kullanıyor ve yalnız bir farklıurunadını tutabiliyor. - WV19.
Depoarayüzü bu kapasite farkını hiçbir yerde belirtmiyor; iki yöntemin imzası da bir sınır sözü taşımıyor.
İlk koşumda vida iki kez güncelleniyor (0 -> 5, sonra 5 -> 3), ardından farklı bir
üründe (civi) bir kayıt daha açılıyor (0 -> 3). BellekDepo ile bu üç çağrı da sorunsuz
tamamlanıyor. DiziDepo ile aynı üç çağrı, aynı sırayla, aynı derlenmiş Servis sınıfı
üzerinden yapılıyor — ama üçüncüsü bir istisnayla düşüyor: DiziDepo yalnız bir farklı ürün
adını tutabiliyor, ve civi ikinci bir ürün adı olarak geldiğinde kapasite doluyor.
Bu, dersin sınırlayıcı ölçümü: arayüz sözü ile gerçekleştirim davranışı ayrıdır, ve kap bu
ayrımı kapatmaz, yalnız gizler. Depo arayüzü miktarGuncelle’nin her zaman başarılı
olacağını hiçbir yerde vaat etmiyor, ama pratikte BellekDepo ile çalışırken bu vaat hiç
test edilmeden doğru görünüyor — sınır hiç aşılmadığı için sorun hiç ortaya çıkmıyor. Aynı
kod DiziDepo’ya bağlandığında, arayüzün hiç söz vermediği bir kapasite sınırı aniden
görünür oluyor. Kap, Servis’in hangi gerçekleştirimle çalıştığını çağırandan gizliyor, ama
gerçekleştirimlerin kendi aralarındaki davranış farkını ortadan kaldırmıyor — yalnız
o farkın nerede ortaya çıkacağını, arayüzün arkasına, çalışma zamanına erteliyor.
Bu ayrımın bir de tersi var: iki gerçekleştirim de vida ürünü üzerindeki iki çağrıda
birebir aynı sonucu veriyor. Arayüzün gerçekten sağladığı şey bu — tek bir anahtarla yapılan
oku-yaz döngüsü, gerçekleştirimden bağımsız olarak aynı davranıyor. Sorun yalnız arayüzün
söylemediği bir varsayımın (sınırsız sayıda farklı anahtar) bir gerçekleştirimde geçerli,
ötekinde geçersiz olmasından doğuyor. Bir arayüz, üzerinde imzası bulunan yöntemlerin sözünü
verir; imzada yer almayan her şey — kapasite, eşzamanlılık, gecikme — gerçekleştirimin kendi
kararıdır ve arayüz onu hiç taşımaz.
Bu, kabın veri erişimi katmanındaki asıl katkısını da açıklığa kavuşturuyor. Kap, Servis
sınıfını hangi gerçekleştirimin dolduracağına karar veriyor, ama bu kararın doğru
gerçekleştirimi seçmiş olduğunu hiçbir yerde garanti etmiyor — Kap.kur, kendisine verilen
dizge adının gerçekten Depo’yu uygulayan bir sınıfı gösterdiğini bile yalnız çalışma
zamanında, (Depo) c.getDeclaredConstructor().newInstance() satırındaki döküm başarısız
olmadığı sürece anlıyor. Bir gerçekleştirimin arayüzün taşımadığı bir varsayımı ihlal etmesi
de aynı şekilde yalnız o varsayım gerçekten test edildiğinde ortaya çıkıyor — DiziDepo ile
tek bir ürün adı kullanan bir çağıran hiçbir zaman kapasite sınırını görmez. Bu derste
üçüncü çağrının farklı bir ürün adı taşıması bilinçli bir seçim: sınır yalnız iki farklı
anahtar aynı depoya ulaştığında görünür oluyor, ve bu da arayüz testinin neden yalnız
“yöntem çağrılıyor mu” değil, “gerçekleştirimin gerçek sınırları nerede” sorusunu da
sorması gerektiğini gösteriyor.
Özet
Kap,ServisveAnasınıfları yalnızDepoarayüzüne karşı bir kez derlendi ve hiç yeniden derlenmeden iki ayrı gerçekleştirimle (BellekDepo,DiziDepo) koşturuldu.- Çağıranın sınıf dosyasında hiçbir gerçekleştirim sınıfının adı geçmiyor;
Kap.kurgerçekleştirimi yalnız bir dizge adıyla, yansımayla buluyor. - Aynı üç çağrı
BellekDepoile sorunsuz tamamlandı,DiziDepoile üçüncü çağrıda birIllegalStateExceptionile düştü — aynı derlenmiş kod, aynı sıradaki çağrılar, iki ayrı sonuç. Depoarayüzü kapasite gibi imzada yer almayan hiçbir garantiyi taşımıyor; bu tür farklar yalnız gerçekleştirim değiştiğinde ortaya çıkıyor.- Kap, çağırandan gerçekleştirim seçimini gizliyor ama gerçekleştirimler arasındaki davranış farkını ortadan kaldırmıyor; farkı yalnız çalışma zamanına erteliyor.
Sonraki Adım
Bu ders bir arayüzün arkasında hangi gerçekleştirimin durduğunu ölçtü, ama gerçekleştirimin kendi alanlarının nesne ile depo arasında nasıl eşleştiği hiç sorulmadı. Sıradaki ders bunu ele alıyor: bir nesnenin alanları ile bir tablonun sütunları arasındaki bağlamanın kaynakta bir açıklamadan geldiğini, ve erişilmeyen bir alanın hiç yüklenmediğini ölçüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.