Anasayfa » 7. TypeScript’de Sınıflar

7. TypeScript’de Sınıflar

by Levent KARAGÖL
12 dakikalık okuma süresi

Bu makalemizde, TypeScript’de sınıf tanımlarını, erişim belirleyicilerini, kurucu fonksiyonları ve salt-okunur değerleri, statik değerler ve fonksiyonları işleyecek, ardından sınıflarda kalıtım ve kalıtımda fonksiyonların geçersiz kılınması konularına göz atacak, son olarak da soyut sınıflar ile makalemizi tamamlayacağız. Her ne kadar buradaki bazı başlıklar tasarım kalıplarının konusu olsa da, en azından bu konu hakkındaki özel serimiz için ön gereksinim ihtiyaçlarımızı karşılamış olacağız.

Aslında erişim belirleyiciler ve salt-okunur değerler haricinde sınıflar TypeScript’in değil, JavaScript’in konusu. Burada anlatılan pek çok işlemi ECMAScript 6 sonrası TypeScript olmadan da gerçekleştirebiliyorduk. Ancak bundan sonraki makalelerimizin konusu olan Interface’ler, Generic yapılar ve dekoratörler için sınıflar konusunda eksiğimizin kalmaması gerekiyor.

Sınıflar ne yazık ki JavaScript dünyasında gerekliliği tartışılmaya devam edilen bir konu. Diğer endüstriyel programlama dillerinde böyle bir tartışma söz konusu olmazken, JavaScript’in vahşi doğa yaşamında herkes kendi yolunu farklı bir şekilde bulduğu ve hayatta kaldığı için, ortama alışkın olanlar buna çok da şaşırmayacaktır. Ancak kabul etmemiz gereken bazı gerçekler var. JavaScript neredeyse nefes alan her canlı tarafından az ya da çok biliniyor. Bu dille geliştirilen yazılımların çok büyük çoğunluğu da oldukça küçük boyutlarda, nasıl yazarsanız yazın çalışabilecek seviyede bulunuyor. JavaScript kullanan ve yüzlerce Mikroservice’den oluşan büyük projelerde çalışan yazılımcı sayısı da oldukça az. Dolayısıyla diğer endüstriyel programlama dillerinde standart hale gelen bazı ihtiyaçlar (SOLID prensipleri, Loose Coupling tasarım kalıpları, DDD vs) bu ortamdaki çoğu yazılımcı tarafından talep görmüyor. Hatta JavaScript geliştiricilerin büyük çoğunluğu için tüm bunlar şehir efsanelerinden ibaret. Ama biz biliyoruz ki, su-i misal emsal olmaz!


Sınıf Tanımları

JavaScript’de ve dolayısıyla TypeScript’de sınıf tanımlamak için “class” ifadesini kullanırız. Sınıf içerisindeki fonksiyonlarda “function” ifadesini kullanmayız. Son olarak “new” ifadesi ile sınıftan yeni bir örnek türetip kullanırız. Buraya kadar bahsettiklerimizi bir örnek üzerinde görelim.

class MyClass {

    myFunction(value1: number, value2: number): number {

        return value1 + value2;
    }
}

const myClass: MyClass = new MyClass();

const result: number = myClass.myFunction(1, 2); // 3


Erişim Belirleyiciler

JavaScript’de sınıf içerisinde tanımladığımız tüm değişken ve fonksiyonlar, ECMAScript 2020’ye kadar sınıftan türetilen nesneler üzerinden erişilebilir durumdaydı çünkü JavaScript’de erişim belirleyiciler yoktu. Sadece değişken ve fonksiyon isimlerinin başına getirilen “_” ifadesi ile JavaScript geliştiricileri kendi “ahlaki private” notasyonunu kurmuştu (Closure’lardan bahsetmiyorum). Biz biliyorduk ki, bir değişken veya fonksiyonun ismi “_” işareti ile başlıyorsa, bu fonksiyon dışarıdan çağırmak amacıyla yazılmamıştır, yapısı veya ismi her an fütursuzca değiştirilebilir ve bu eylem kütüphanenin geriye dönük uyumluluğunu bozmaz.

ECMAScript 2020 ile birlikte değişken ve fonksiyon isimlerinin önüne “#” işaretini koyarak gerçek “private” üyeler eklemenin önü açıldı. Gel gelelim bunlar da kalıtım yoluyla alt sınıflara aktarılamıyordu.

Neyse ki TypeScript’de dışarıdan erişilebilir (public), dışarıdan erişilemez sadece sınıf içerisinden erişilebilir (private) ve dışarıdan erişilemez sadece sınıf içerisinden ve kalıtım yoluyla aktarılan sınıflardan erişilebilir (protected) şeklinde kısıtlamalar koyabiliyoruz. “public” ve “private” erişim belirleyicileri hemen bir örnek üzerinde görelim, “protected” kısmına makalenin sonuna doğru sınıflarda kalıtım konusunu işledikten sonra geri döneceğiz.

class MyClass {

    public myPublicValue: number = 3;
    private myPrivateValue: number = 7;

    public myPublicFunction(value1: number, value2: number): number {

        return value1 + value2;
    }

    private myPrivateFunction(value1: number, value2: number): number {

        return value1 + value2;
    }
}

const myClass: MyClass = new MyClass();

// Aşağıdaki fonksiyon ve değişkene public olması sebebiyle erişim sağlanabilir.
const result1: number = myClass.myPublicFunction(1, 2); // 3
const result2: number = myClass.myPublicValue; // 3

// Aşağıdaki fonksiyon ve değişken private olarak tanımlandı
// Erişmeye çalışmak derleme sırasında hataya sebep olur
const result3: number = myClass.myPrivateFunction(1, 2);
const result4: number = myClass.myPrivateValue;

.Net veya Java gibi dilleri kullananlar için erişim belirleyicilerini kullanmak çok doğal bir davranıştır. Ancak bu dillerde deneyiminiz varsa, normalde erişim belirleyici kullanılmayan sınıf elemanları varsayılan olarak “private” konumda iken, TypeScript’de erişim belirleyici kullanılmayan sınıf elemanları varsayılan olarak “public” konumda olduğunu ve dışarıdan erişilebilir halde bulunduğunu unutmamak gerekir.


Kurucu Fonksiyonlar

Gelelim JavaScript’te de kullandığımız kurucu fonksiyonlara. Bilindiği üzere kurucu fonksiyonlar (veya metotlar), sınıfın yeni bir örneği çıkartılırken geçilen parametrelerin alındığı sınıfın özel bir fonksiyonudur. Tip tanımlamalarını ve erişim belirleyicilerini saymazsak, aşağıdaki sınıf tanımının JavaScript’teki kullanımdan pek bir farkı yoktur.

class MyClass {

    private value1: number;
    private value2: number;

    constructor(value1: number, value2: number) {

        this.value1 = value1;
        this.value2 = value2;
    }

    public sum(): number {

        return this.value1 + this.value2;
    }
}

const myClass: MyClass = new MyClass(1, 2);

const result: number = myClass.sum(); // 3


Yukarıdaki kullanım şekli çoğu sınıf için tam bir klişedir. Kurucu fonksiyondan geçilen parametreler, sınıf içerisindeki private değişkenlere geçilir. Bu kullanım için TypeScript tarafında aşağıdaki gibi kolaylık sağlanmıştır.

class MyClass {

    constructor(private value1: number, private value2: number) {}

    public sum(): number {

        return this.value1 + this.value2;
    }
}

const myClass: MyClass = new MyClass(1, 2);

const result: number = myClass.sum(); // 3

Yukarıdaki yazım şekli, bir önceki örnek ile aynı işlevi görür. Kurucu fonksiyonun aldığı değişkenler, sınıf seviyesindeki değişkenlere otomatik olarak atanır. Elbette ihtiyaca yönelik olarak erişim belirleyiciler değiştirilebilir.


Salt-Okunur Değerler

TypeScript’de Özel Tip Tanımı isimli makalemizin “Salt-Okunur Tipler” başlıklı kısmında “readonly” ifadesi ile değerleri nasıl salt-okunur hale getireceğimizi görmüştük. Benzer şekilde sınıflarda salt-okunur olarak tanımlanan değerler, kurucu fonksiyon içerisinde ilk değerlerini alırlar ve değerleri bir daha değiştirilemez. Az önceki kısa yazım şekline salt-okunur ifadesini ekleyerek aşağıdaki kodu elde edebiliriz.

class MyClass {

    constructor(private readonly value1: number, private readonly value2: number) {}

    public sum(): number {

        return this.value1 + this.value2;
    }
}

const myClass: MyClass = new MyClass(1, 2);

const result: number = myClass.sum(); // 3


Statik Değerler ve Fonksiyonlar

Sınıftan türetilen bir nesne üzerinden değil de, doğrudan sınıf üzerinden değişken ve fonksiyonlara erişmek istediğimizde static tanımlamaları kullanırız. Aşağıda örnek bir static değişken ve fonksiyon örneği bulunmaktadır.

class MyClass {

    public static value1: number = 1;

    public static sum(value2: number): number {

        return this.value1 + value2;
    }
}

const result: number = MyClass.sum(2); // 3


Sınıflarda Kalıtım

Sınıflarda kalıtım konusu ECMAScript 6’dan bu yana JavaScript’de de kullandığımız yapılardandır ancak TypeScript’de fark olarak artık “protected” erişim belirleyicilerimiz var. Böylece bazı değer ve fonksiyonlarımızı dışarıdan erişime kapayıp, sadece kalıtım yoluyla devralan sınıflarımıza açık hale getirebiliriz. Konuyu hemen bir örnek üzerinde özetleyelim.

class MyBaseClass {

    constructor(protected readonly value1: number, protected value2: number) {}

    protected sum(): number {

        return this.value1 + this.value2;
    }
}

class MyClass extends MyBaseClass {

    constructor(value1: number, value2: number) {

        super(value1, value2);
    }

    public writeSum(): string {

        return "Toplam: " + super.sum();
    }
}


const myClass: MyClass = new MyClass(1, 2);

const result: string = myClass.writeSum(); // Toplam: 3

Yukarıdaki örneğimizde “MyClass” sınıfı “MyBaseClass” sınıfının özelliklerini kalıtım yoluyla devralmıştır. Bu sınıftan türetilen örnek nesnemiz üzerinden “writeSum” fonksiyonunu çağırdığımızda, “protected” olarak işaretlendiği için dışarıdan erişilemez olan “sum” fonksiyonunu çağırabilir hale gelir. Yine dikkat edilecek bir husus, “MyClass” sınıfının kurucu fonksiyonuna geçilen parametreler “super” ifadesi ile kalıtım yoluyla devraldığı sınıfın kurucu fonksiyonuna geçilebilir. “super” ifadesi bir üst sınıfı temsil eder. Bir sonraki başlığımızda bu ifadeye tekrar değineceğiz.


Fonksiyonların Geçersiz Kılınması (Overriding)

Sınıflarda kalıtım konusunun vazgeçilmez parçası, alt sınıftaki fonksiyonların, aynı isimdeki üst sınıf fonksiyonlarını ezmesi/geçersiz kılmasıdır (function/method overriding). Eğer alt sınıftaki bir fonksiyon ismi ile kalıtım yoluyla devralınan üst sınıftaki fonksiyon isimleri aynı ise, alt sınıf fonksiyonu diğerini ezerek geçersiz hale getirir. Böylece dışarıdan yapılan çağrımlarda üst sınıftaki değil, alt sınıfta bulunan fonksiyon çalıştırılır. Ancak ihtiyaç durumunda bu fonksiyon içerisinden üst sınıf fonksiyonuna “super” ifadesi ile ulaşabiliriz. Hızlı bir örnek ile konuyu birlikte inceleyelim.

class MyBaseClass {

    constructor(protected readonly value1: number, protected value2: number) {}

    protected sum(): number {

        return this.value1 + this.value2;
    }
}

class MyClass extends MyBaseClass {

    constructor(value1: number, value2: number) {

        super(value1, value2);
    }

    public override sum(): number {

        return 2 * super.sum();
    }
}


const myClass: MyClass = new MyClass(1, 2);

const result: number = myClass.sum(); // 6

Yukarıdaki örnekte hem kalıtım yoluyla devraldığımız “MyBaseClass” sınıfı içerisinde, hem de “MyClass” sınıfı içerisinde “sum” isimli birer fonksiyon bulunur. Dikkat ederseniz sonradan gelen “sum” fonksiyonunun isminin başında “override” ifadesi yer alır. Bu ifade, fonksiyonun üst sınıftaki aynı isimli fonksiyonun yerine geçeceği anlamına gelir. Dışarıdan çağrım sırasında üst sınıftaki fonksiyon değil, bu fonksiyon çağrılır. Ancak dilersek, bu fonksiyon içerisinden “super” ifadesi ile üst sınıfa ulaşabilir ve örnekte olduğu gibi buradaki fonksiyonu da çağırabiliriz.

Not: TypeScript’de üst fonksiyonu ezmek için “override” ifadesinin kullanımı zorunlu değildir. Ancak kodun daha kolay okunabilmesi için kullanılması tavsiye edilir. Ayrıca daha sonradan göreceğimiz “tsconfig.json” konfigürasyon dosyası üzerinden bu kullanımı zorunlu hale de getirebiliriz.


Soyut Sınıflar

Soyut sınıflar, temelde tasarım kalıplarının konusu olmakla birlikte, başka sınıflar tarafından kalıtım yoluyla devralınabilen ancak doğrudan yeni bir örneği çıkartılıp kullanılamayan sınıflardır. Böyle bir sınıf tanımlamak için sınıf tanımının başında “abstract” ifadesi kullanılır. Böyle bir sınıfın “new” ifadesi ile örneğini türetmek istersek derleme sırasında hata alırız.

Bir önceki örneğimizde bulunan üst sınıfımızı soyut sınıf olarak tanımlamak istersek, aşağıdaki gibi bir kod yazmamız gerekir.

abstract class MyBaseClass {

    constructor(protected readonly value1: number, protected value2: number) {}

    protected sum(): number {

        return this.value1 + this.value2;
    }
}

class MyClass extends MyBaseClass {

    constructor(value1: number, value2: number) {

        super(value1, value2);
    }

    public override sum(): number {

        return 2 * super.sum();
    }
}


const myClass: MyClass = new MyClass(1, 2);

const result: number = myClass.sum(); // 6

// Aşağıdaki tanımlama, soyut bir sınıftan örnek türetmeye çalıştığımız için derleme sırasında hataya sebep olur
const myBaseClass: MyBaseClass = new MyBaseClass(1, 2);


Böylece TypeScript’de sınıflar konusunun sonuna geldik. Bir sonraki makalemizde TypeScript’de Interface kullanımı konusunu işleyeceğiz.

Herkese iyi çalışmalar…

İLGİNİZİ ÇEKEBİLİR

Bir Yorum Yaz