İçeriğe geç
academia.sh

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.kur yöntemi gerçekleştirim sınıfının adını bir dizge parametresi olarak alıyor ve Class.forName ile 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. BellekDepo bir HashMap üzerinde çalışıyor ve kaç farklı urun adı geldiğine bir sınır koymuyor. DiziDepo sabit uzunlukta iki dizi kullanıyor ve yalnız bir farklı urun adını tutabiliyor.
  • WV19. Depo arayü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, Servis ve Ana sınıfları yalnız Depo arayü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.kur gerçekleştirimi yalnız bir dizge adıyla, yansımayla buluyor.
  • Aynı üç çağrı BellekDepo ile sorunsuz tamamlandı, DiziDepo ile üçüncü çağrıda bir IllegalStateException ile düştü — aynı derlenmiş kod, aynı sıradaki çağrılar, iki ayrı sonuç.
  • Depo arayü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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat