Data Abstraction Nedir, Nerede Bulunur?
Yazılım geliştirme prensiplerinin en önemlilerinden birisi olan abstraction, yani soyutlama, kendi içinde iki çeşide ayrılıyor. Bunlar procedural abstraction ve data abstraction. Bu yazıda kısaca data abstraction‘ın ne olduğundan bahsetmeye çalışacağım.
Öncelikle abstraction ve encapsulation terimleri ne anlama geliyor ondan bahsetmek istiyorum çünkü bu terimler tanımları çok yakın olduğundan genellikle birbiri ile karıştırılıyorlar.
Abstraction: Gereksiz karmaşıklığın (complexity) gizlenerek oluşturulan bileşenlerin sadece ilgili kısımlarının yazılımın diğer kısımlarına sunulması işlemidir. Bu sayede bu bileşenleri kullanan diğer bileşenler alt seviyelerdeki kompleks işlemlerin nasıl yapıldığı ile ilgilenmek zorunda kalmaz. Java’da çoğunlukla interface, abstract class gibi konseptleri kullandığımızda bir takım detayları soyutlamış oluruz.
Encapsulation: Bir yazılım bileşeninin iç yapısının dış dünyadan gizlenmesidir. Böylece bu bileşenin işleyişinde yapılabilecek herhangi bir değişikliğin bileşeni kullanan diğer yazılım bileşenlerini etkilememesi sağlanır. Java’da bir sınıfta private method, private field gibi dışarıya kapalı tanımlar yaptığımızda sınıfımızın işleyişini ve iç yapısındaki verileri encapsulate etmiş oluruz. Böylece bu sınıfın işleyişine ileride müdahale ettiğimizde bu sınıfı kullanan diğer sınıflara dokunmak zorunda kalmayız.
Abstraction, yazılımımızı farklı seviyelere (layers of abstraction) ayırmamızı sağlar. Örneğin, kullandığımız işletim sistemi diskten bir dosyanın nasıl okunması gerektiğini bilir ve bizden birçok donanım spesifik detayı gizleyerek sadece ilgili fonksiyonaliteyi soyut olarak bize sunar. Java, platform bağımsız bir dil olarak, işletim sistemleri ile iletişime nasıl geçilmesi gerektiği ile ilgili detayları bizden gizleyerek, dosya okuma/yazma gibi işlemleri daha da soyut olarak bize çeşitli sınıflar ile sağlar. Apache’nin açık kaynak kodlu POI kütüphanesi, Java’nın temel dosya/okuma yazma bileşenlerini kullanarak excel dosya formatının nasıl okunup yazılması gerektiği ile ilgili birçok gereksiz detayı bizden saklar. Bu sayede bizim yazılımımız sadece excel’in satırlarına ilgili bilgileri girip dosyayı kaydet komutunu vermekten ibaret olur.
Aşağıda Apache POI kütüphanesinin dökümantasyonundan örnek bir excel yazma kodu göstermek istiyorum.
Workbook wb = new HSSFWorkbook();
CreationHelper createHelper = wb.getCreationHelper();
Sheet sheet = wb.createSheet("new sheet");
// Create a row and put some cells in it. Rows are 0 based.
Row row = sheet.createRow((short) 0);
// Create a cell and put a value in it.
Cell cell = row.createCell(0);
cell.setCellValue(1);
// Or do it on one line.
row.createCell(1).setCellValue(1.2);
row.createCell(2).setCellValue(createHelper.createRichTextString("This is a string"));
row.createCell(3).setCellValue(true);
// Write the output to a file
FileOutputStream fileOut = new FileOutputStream("workbook.xls");
wb.write(fileOut);
fileOut.close();
Bu örnekten görüleceği gibi, bir excel dosyasının formatının nasıl olması gerektiği ile ilgili detayları kullandığımız kütüphane bizden soyutladı. Diske dosya nasıl yazılır detayını ise Java içerisinde bulunan sınıflar bu kütüphaneden soyutladı.
Bu sayede yazılımda kompleks işleri bir üst seviye için kolaylaştıran farklı soyutlama seviyeleri ortaya çıktı. Yazılım mimarisine eklenen her bir soyutlama seviyesi, yazılımı makine anlayacak seviyeden insan anlayacak seviyeye bir adım daha yaklaştırır.
Bir excel dosyası tonla bilgi taşıyan zengin bir içeriğe sahiptir. HSSFWorkbook
sınıfı bu bilgilerin çoğunu
ve nasıl manipüle edileceğine ait detayları bizden gizler, bu sayede bu kütüphanede ileriki zamanlarda
yapılabilecek performans iyileştirmeleri yada refactoring gibi işlemlerde bizim kodumuzu güncellememiz gerekmez.
Encapsulation da burada bu şekilde kullanıldı.
Abstraction yazılımın tasarımı aşamasında yapılan bir çalışmadır. Encapsulation ise implementasyon aşamasında yapılır.
Peki Data Abstraction Ne?
Yukarıda verdiğim örnekler genellikle procedural abstraction’a örnekti. Yani bir işin nasıl yapılacağına dair detayların soyutlanması idi. Data Abstraction ise, komplex bir datanın nasıl implement edildiğine ait detayın bu datayı kullananlardan saklanmasıdır. Data ne kadar abstract ise, o kadar günlük hayatta kullandığımız datalara yakın, makine dili olan 1 ve 0’lara uzaktır.
Ben bu konuya Structure and Interpretation of Computer Programs isimli kitapta verilen karmaşık sayılar örneği ile devam etmek istiyorum çünkü ben de bu kitap sayesinde bu konsept ile tanıştım.
Matematik derslerinden hatırlarsınız karmaşık sayılar diye bir konu vardır, hiç sevememiştim. Bir karmaşık sayı kutupsal ve düzlemsel(bu kelime doğru mu emin değilim) olarak iki farklı şekilde gösterilebilir.
Örneğin yukarıdaki resimdeki z karmaşık sayısı aşağıdaki iki farklı biçimde gösterilebilir ve ikisi de sayısal olarak aynıdır:
z = r ∠ θ
z = a + bi
Buradan anlaşılıyor ki ComplexNumber
diye bir sınıf geliştirmek istesek,
bu sınıf içerisinde veriyi iki farklı şekilde tutabiliriz. Birinci yöntem bir açı ve bir büyüklük içeren yani iki adet
double
alandan oluşan bir sınıf tasarlamak. İkinci yöntem ise karmaşık sayının reel eksendeki izdüşümünü
ve sanal eksendeki izdüşümünü tutan yine iki adet double
alandan oluşan bir sınıf tasarlamak.
Şimdi tek tek iki yöntemin de nasıl tasarlanabileceğine bakalım.
Öncelikle ComplexNumber
isimli bir interface tanımlayıp, karmaşık sayılar üzerinde yapılabilecek işlemleri
tanımlıyoruz. Böylece karmaşık sayıyı kullanacak olan başka yazılım bileşenlerine ihtiyaçları olan methodları
baştan belirlemiş oluyoruz.
public interface ComplexNumber {
ComplexNumber sum(ComplexNumber other);
ComplexNumber subtract(ComplexNumber other);
ComplexNumber multiply(ComplexNumber other);
ComplexNumber divide(ComplexNumber other);
double getRealPart();
double getImaginaryPart();
double getAngle();
double getMagnitude();
}
Daha sonra bu interface’den türeyen abstract bir sınıf tanımlayıp, sum
, subtract
, multiply
ve divide
methodlarını buraya yazıyorum çünkü bu methodlar datanın içeride nasıl tutulduğundan bağımsız methodlar.
Yani karmaşık sayılar üzerinde yapılan dört işlemde bile karmaşık sayı datasının içeride nasıl tutulduğu
ile ilgilenmemiş olduk.
public abstract class BaseComplexNumber implements ComplexNumber {
private final static double EPSILON = 0.00001;
@Override
public ComplexNumber sum(ComplexNumber other) {
double realPart = getRealPart() + other.getRealPart();
double imaginaryPart = getImaginaryPart() + other.getImaginaryPart();
return ComplexNumbers.fromRectangularForm(realPart, imaginaryPart);
}
@Override
public ComplexNumber subtract(ComplexNumber other) {
double realPart = getRealPart() - other.getRealPart();
double imaginaryPart = getImaginaryPart() - other.getImaginaryPart();
return ComplexNumbers.fromRectangularForm(realPart, imaginaryPart);
}
@Override
public ComplexNumber divide(ComplexNumber other) {
double magnitude = getMagnitude() / other.getMagnitude();
double angle = getAngle() - other.getAngle();
return ComplexNumbers.fromPolarForm(magnitude, angle);
}
@Override
public ComplexNumber multiply(ComplexNumber other) {
double magnitude = getMagnitude() * other.getMagnitude();
double angle = getAngle() + other.getAngle();
return ComplexNumbers.fromPolarForm(magnitude, angle);
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof ComplexNumber)) {
return false;
}
ComplexNumber other = (ComplexNumber) obj;
return isZero(getRealPart() - other.getRealPart()) && isZero(getImaginaryPart() - other.getImaginaryPart());
}
private boolean isZero(double x) {
return x + EPSILON > 0 && x - EPSILON < 0;
}
}
Kendisine ait realPart
, imaginaryPart
, angle
ve magnitude
değerlerini dönme
veya gerekirse hesaplama işlerini ise datayı asıl tutan sınıfın omuzlarına yükledik,
çünkü bu işlemler içerideki verinin nasıl saklandığına bağlı değişiklik gösteriyorlar.
Kutupsal gösterimdeki (polar form) şekliyle veriyi tuttuğumuz bir sınıf geliştirecek olursak, aşağıdaki gibi bir implementasyon yapabiliriz:
public class PolarComplexNumber extends BaseComplexNumber implements ComplexNumber {
private final double magnitude;
private final double angle;
public PolarComplexNumber(double magnitude, double angle) {
this.magnitude = magnitude;
this.angle = angle;
}
@Override
public double getRealPart() {
return magnitude * Math.cos(angle);
}
@Override
public double getImaginaryPart() {
return magnitude * Math.sin(angle);
}
@Override
public double getAngle() {
return angle;
}
@Override
public double getMagnitude() {
return magnitude;
}
@Override
public String toString() {
double degrees = Math.toDegrees(angle);
return String.format("%.2f x (cos(%.0fu00b0) + i x sin(%.0fu00b0))", magnitude, degrees, degrees);
}
}
Bu sınıf görüldüğü gibi açı ve büyüklük değerlerini tutup, realPart ve imaginaryPart değerlerini hesaplayarak hizmet veriyor.
Düzlemsel gösterimdeki (rectangular form) halde veriyi tutmak istersek eğer, aşağıdaki gibi bir sınıf geliştirebiliriz:
public class RectangularComplexNumber extends BaseComplexNumber implements ComplexNumber {
private final double realPart;
private final double imaginaryPart;
public RectangularComplexNumber(double realPart, double imaginaryPart) {
this.realPart = realPart;
this.imaginaryPart = imaginaryPart;
}
@Override
public double getRealPart() {
return realPart;
}
@Override
public double getImaginaryPart() {
return imaginaryPart;
}
@Override
public double getAngle() {
return Math.atan2(imaginaryPart, realPart);
}
@Override
public double getMagnitude() {
return Math.sqrt(realPart * realPart + imaginaryPart * imaginaryPart);
}
@Override
public String toString() {
return String.format("%.2f + i x %.2f", realPart, imaginaryPart);
}
}
Görüldüğü üzere bu sınıf da realPart ve imaginaryPart değerlerini tutup, angle ve magnitude değerlerini hesaplayarak hizmet veriyor.
BaseComplexNumber
sınıfında factory method olarak kullandığımız ComplexNumbers.fromPolarForm
ve ComplexNumbers.fromRectangularForm
methodları bu iki gösterimden herhangi birisini seçip
onu dönecek şekilde geliştirilebilir. ComplexNumbers
aracılığı ile başka yazılım bileşenlerinin
elde edebileceği tek nesne tipi ComplexNumber
olacaktır.
Yani datanın nasıl represent edildiğini datayı kullananlardan soyutlamış olduk.
Ben hazır yazmışken iki sınıfı da kullanayım dedim ve ortaya şöyle bir sınıf çıktı:
public class ComplexNumbers {
public static ComplexNumber fromRectangularForm(double realPart, double imaginaryPart) {
return new RectangularComplexNumber(realPart, imaginaryPart);
}
public static ComplexNumber fromPolarForm(double magnitude, double angle) {
return new PolarComplexNumber(magnitude, angle);
}
}
Buradan çıkarmamız gereken sonuç tabi ki de bir sistemde aynı veriyi birçok farklı şekilde tasarlamak gerektiği değil. Bir yazılımda kompleks bir veri tipi (karmaşık sayı gibi) yaratıyorsanız, bu veri tipini kullanan yazılımın diğer bileşenleri için önemli olan iki soruyu cevaplamanız gerekmektedir.
Bunlardan birincisi, bu veriyi nasıl yaratacakları.
Bu örneklerde, ComplexNumbers
sınıfı içerisindeki iki factory method ile verinin kolayca yaratılmasını sağladık.
İkinci önemli soru da bu veri üzerindeki gerekli bilgilere nasıl erişecekleri.
Açtığımız ComplexNumber
arayüzü ile karmaşık sayı ile ilgili bilgilere
nasıl erişeceklerini de diğer yazılım bileşenlerine anlatmış olduk.
Bundan gerisi bizim içeride veriyi nasıl ve ne formatta sakladığımız ve bu da bizden başkasını ilgilendirmez. Biz istersek içeride byte dizisi olarak tutalım, istersek String olarak tutalım, ya da çok daha kötü bir yapı kuralım. Dış dünyaya aynı interface’i çalışır durumda verebiliyorsak verimizin iç dünyasını istediğimiz biçimde şekillendirebiliriz.
Finito
Bu yazıda birçok soyut kavrama değindiğim için tonla hata yapmış olabilirim. Böyle durumlarda yorumlarınız ile beni düzeltmekten geri kalmayın.
Yazıda bir ara ismini zikrettiğim muhteşem kitabın görselini de ekleyeyim. Çok uzun uğraşlar sonucu keşfettiğim bir kitap değil zaten, şu tarz listelerden buluyorum böyle kitapları: What is the single most influential book every programmer should read?