Bir noktanın hikayesi


"Müşteri" istekleri

Koordinatları verilen iki noktayı görüntüle


"Ürün"ü önce C programlama dili ile gerçekleyeceğiz

  • C bir Yapısal Programlama ("Structured Programming") dili

"Müşteri" kodu taslağı

#include <stdio.h>
#include <stdlib.h>

struct point {
        ...
}

...

int main(void)
{
        struct point *p = point_create(3, 5);
        struct point *q = point_create(9, 7);

        point_print(p);
        point_print(q);

        point_destroy(p);
        point_destroy(q);

        return 0;
}

Nokta için bir struct oluşturmaya karar verdik

  • struct → yapı

  • Birbiriyle ilişkili verileri bir isim altında grupluyor

  • C gibi bir Yapısal Programlama dilinde özel veri tipleri kurgulamanın neredeyse tek yolu


"Özel" veri tipi?

Önce "özel" olmayan, ki ona "ilkel" ("primitive") veri tipi diyeceğiz, ne olduğunu anlayalım

int x;
  • Bakın burada int bir "ilkel" veri tipi

  • Söz dizimine dikkat: önce veri tipi (int) sonra o veri tipinde olacak tanımlayıcı (x)

  • Bu bir değişken bildirimi ("variable declaration")


Birlikte tekrar edelim!

x adı int tipinde bir değişkeni tanımlar


C programlama dili kurallarına göre x bir yerel değişkense ilk değeri belirsiz

int x = 3;
  • Bildirim yaparken değişkeni "ilkle"yebiliriz

  • Bu bir tür "inşa" ("construction")

  • x adıyla erişilecek bir veriyi inşa (construct) ediyoruz


Şimdi "nokta"ya ("point") geçebiliriz

  • C'de veri tipleri? int, float, double, char, ...

  • Bunlar ilkel veri tipleri

  • C'de point adında bir veri tipi yok

  • Bu "özel" veri tipini biz tanımlayacağız ("definition")


struct point {
        int x;
        int y;
};
  • C söz dizimi kurallarına göre point veri tipini tanımladık

  • Bu bir veri tipi tanımı, veri bildirimi değil


  • Gelin şimdi veri bildirimini yapalım

  • Aynı söz dizimi kuralını kullanacağız

  • Önce veri tipi sonra o veri tipinde olacak tanımlayıcı


struct point p;
  • Veri tipi? struct point

  • Tanımlayıcı (değişken)? p

  • int'de olduğu gibi sadece struct diyemeyiz değil mi?

  • int tek, struct ise ne tanımladığınıza bağlı olarak çok

  • Tanıma verdiğiniz adla (point) hangi struct tanımı olduğunu belirtiyoruz


Peki bildirim sırasında ilkleme yapabilir miyiz? Tabii, şöyle:

struct point p = { 3, 5 };

veya daha güzeli (C99):

struct point p = {
        .x = 3,
        .y = 5
};

struct point p = { 3, 5 };
  • Bu bir statik yer ayırma ("allocation")

  • p için bellekte gerekli yer derleme zamanında hesaplanıp program yüklendiğinde ayrılıyor

  • Belirsiz sayıda point ile çalışacaksak ne yapmalıyız?

  • Belirsizlik statik yöntem seçeneğini eliyor


Dinamik bellek yönetimi

struct point *p = (struct point *)malloc(sizeof(struct point));

Bir an için C'nin "aignment" ve "padding" kurallarını göz ardı edersek kabaca şöyle

  • Bir point'de iki int var

  • Bir int 64 bitlik bir sistem 8 bayt ise bellekte 2x8 = 16 baytlık yer ayırdık

  • *p işaretçisi artık bize (programa) ait geçerli 16 baytlık bellek alanının başlangıç adresine işaret ediyor


Unutmayın C'de bir bellek alanına yazmak için öncelikle o alanın size ait olması gerekmiyor

  • Önünüze gelen yere ev inşa edemezsiniz

  • Önce arsanın size ait olması lazım

  • Arsanın tapusunu ilgili kişi ve kurumların koordinasyonuyla almalısınız

  • Belleği de işletim sisteminde talep ederek almalısınız, nasıl? malloc


Arsanın bir bedeli var, ama bellek için bir bedel yok sadece dürüst vatandaşlık var

  • Dürüst? İşiniz bittiğinde "arsayı", yani ayrılan belleği iade edin
struct point *p = (struct point *)malloc(sizeof(struct point));

...

free(p);

Gerçekleme

Şimdi bu "özel" veri tipini kullanarak gerçeklemeye geçeceğiz

#include <stdio.h>
#include <stdlib.h>

struct point {
        int x;
        int y;
}

...

int main(void)
{
        struct point *p = point_create(3, 5);
        struct point *q = point_create(9, 7);

        point_print(p);
        point_print(q);

        point_destroy(p);
        point_destroy(q);

        return 0;
}

struct point {
        int x;
        int y;
};

struct point *point_create(int x, int y)
{
        struct point *this = (struct point *)malloc(sizeof(struct point));

        this->x = x;
        this->y = y;

        return this;
}

void point_destroy(struct point *this)
{
        free(this);
}

void point_print(const struct point *this)
{
        printf("(%d,%d)", this->x, this->y);
}

Bakın burada malloc ve free sırasıyla point_create ve point_destroy içinde

  • point_create bir "yapıcı" fonksiyon → constructor (kısaca CTOR)

  • point_destroy bir "yıkıcı" fonksiyon → destructor (kısaca DTOR)

  • CTOR ile bellekten yer alırken ilkleme işlemi de yapıyoruz (başka işlemler de yapılabilir)

  • DTOR ile belleği geri verirken varsa değişkene ait izleri de yok edebiliriz (örnekte buna gerek olmamakla beraber)


Yeni müşteri isteği

Verilen bir noktanın, verilen bir başka noktanın solunda olup olmadığını görüntüle


#include <stdio.h>
#include <stdlib.h>

struct point {
        int x;
        int y;
}

...

int main(void)
{
        struct point *p = point_create(3, 5);
        struct point *q = point_create(9, 7);

        point_print(p), printf(" noktası ");
        point_print(q), printf(" noktasının ");
        printf("%s.\n", point_isleft(p, q) ? "solunda" : "sağında");

        point_destroy(p);
        point_destroy(q);

        return 0;
}


int point_isleft(const struct point *this, const struct point *that)
{
        return this->x < that->x;
}

int point_isright(const struct point *this, const struct point *that)
{
        return this->x > that->x;
}

Gözlemler

Ne yaptık?

  • Yapısal bir Programlama diliyle "veri odaklı" bir programlama yaptık

  • Hikayenin merkezinde struct point veri tipi var

  • Bu veri tipinden türetilmiş veriler var ki bunların her birine "değişken" diyoruz (ör. p, q)

  • Nokta verileri üzerinde çalışan fonksiyonlar var: point_create, point_isleft vb


Fonksiyonları birer emir gibi yorumlayabiliriz: create, destroy, print, isleft?

  • Program belirli değişkenler üzerinde "emirler"le yürüyen bir eylemler dizisi gibi duruyor

  • Buna "Imperative Programming" deniliyor


  • Yapısal Programlama bir metodoloji

  • Problemin çözümünde kullanılan bir yöntem, bir perspektif


  • Bu metodolojide verinin pasif durumda bulunduğuna dikkat edin

  • Akış emirlerle yürüyor, denetim emri veren kişide

  • Ama yine disiplinli bir isimlendirmeyle "veri odaklılığını" koruduk


  • Fonksiyonlar daima point_ ön ekiyle (prefix) isimlendirilmiş

  • CTOR haricinde her fonksiyonda ilk argüman eylemin baş aktörü olen bir nokta işaretçisi


Yapısal Programlama

Yapısal olmayan Programlama? İlk yaygın programlama dili: FORTRAN

  • İlk 5 çift sayıyı görüntüle

    01    DO 20 i = 2, 10, 2
    02    PRINT *, i
    03    20 CONTINUE
    
  • Çıktı

    2
    4
    6
    8
    10
    
  • 03 satırında 20 etiketi kullanılmış

  • 01 satırında kurulan döngüde, döngü sonu bu etikete verilen referansla belirtilmiş


Yapısal olmayan programlama dillerinde böyle bir sorun var(dı)

  • Çeşitli satırlara serpiştirilmiş doğrudan veya dolaylı atlamalar (GOTO)

  • Programın akışını takip etmek çok zor

  • Go To Statement Considered Harmful, Edgar Dijstra


Yapısal Programlama? (Tarihsel olarak) Önce Pascal, sonra C

for (int i = 2; i <= 10; i++)
        printf("%d\n", i);
  • Bakın bu programda GOTO yok

  • Döngü sonu döngü yapısıyla yani "yapısal" olarak belirleniyor


Nesne Yönelimli Programlama

  • Yapısal Programlama, "Yapısal olmayan programlama"ya bir tür cevap

  • 1980'li yılların başına gelindiğinde grafik kullanıcı arayüzleri (GUI) ortaya çıktı

  • GUI'lerde kullanıcı çeşitli "nesneler"le etkileşim halinde

  • Ör. "window", "button", "checkbox" vb

  • İşler "emirler/eylemler" yerine bu nesnelerle yürütülüyor

  • "Yapısal Programlama"dan daha farklı bir perspektif gerekiyor


C++

Şimdi ürün C++ programlama dilinde gerçekleyelim

  • C++ bir Nesne Yönelimli Programlama ("Object Oriented Programming") dili

  • Yine AT&T laboratuvarlarında icat edilmiş bir dil


C++ (en azından başlangıçta) C'ye yapılan NYP eklemeleriyle ortaya çıktı

  • Fakat ortaya çıkan dil C'den farklı (ve C'nin basit ruhuna aykırı)

  • ++ son eki bu ekleme nosyonuna işaret ediyor


C++ > C değildir

C++ ≠ C (bu daha doğru)


#include <cstdio>

class Point {
        ...
};

...

int main(void)
{
        Point p(3, 5), q(9, 7);

        p.print(), printf(" noktası "), q.print(), printf(" noktasının ");
        printf("%s.\n", p.isLeft(q) ? "solunda" : "sağında");

        return 0;
}

  • struct yerine class, işte bu önemli

  • struct point tipi yerine Point

  • Fonksiyonlarda "nesne nokta metot" notasyonu


class Point {
private:
        int m_x;
        int m_y;

public:
        Point(int x = 0, int y = 0)
        {
                m_x = x;
                m_y = y;
        }

        int x(void)
        {
                return m_x;
        }
        int y(void)
        {
                return m_y;
        }

        ...
}

Nitelikler

Nitelikler (veya Özellikler): Attributes (veya Properties)

  • m_x ve m_y

  • Bu niteliklere doğrudan erişim yok

  • Yani nitelikler "sarmalanmış" (enkapsüle edilmiş)


Veri sarmalama

Veri Sarmalama: Data Encapsulation

  • public ve private nitelendiricileri

  • m_x ve m_y niteliklerine erişim private ile korunmuş


Getters

  • x ve y public metotları

  • Korunmuş niteliklere bu metotlarla erişiyoruz


Yapıcı

  • Sınıf ile aynı isme sahip olması yönüyle diğer tüm metotlardan ayrılan bir metot var

  • Bu özel metot bir yapıcı metot (CTOR), point_create fonksiyonuna karşı geliyor

  • Argümanlara öntanımlı değerler verebiliyorsunuz


Tam gerçekleme

class Point {
private:
        int m_x;
        int m_y;

public:
        Point(int x = 0, int y = 0)
        {
                m_x = x;
                m_y = y;
        }

        int x(void)
        {
                return m_x;
        }
        int y(void)
        {
                return m_y;
        }

        bool isLeft(Point other)
        {
                return m_x < other.x();
        }

        void print(void)
        {
                printf("(%d,%d)", m_x, m_y);
        }
}

Ruby

Ve geldik Ruby'ye...


class Point
  def initialize(x, y)
    @x, @y = x, y
  end

  def x
    @x
  end

  def y
    @y
  end

  def isleft(other)
    @x < other.x
  end

  def to_s
    "(#{@x},#{@y})"
  end
end

def main
  p, q = Point.new(3, 5), Point.new(9, 7)
  puts "#{p} noktası #{q} noktasının " + (p.isleft(q) ? 'solunda' : 'sağında')
end

main

Nitelikler

  • @x ve @y

  • Bunlar doğal olrak korunmuş, ör. p.@x ile erişim yapamıyoruz


Getters

  • x ve y metotları (aksi belirtilmediği sürece bunlar daima public)

  • @x ve @y niteliklerine bu metotlarla erişiyoruz


Yapıcı

initialize metodu

  • Bir sınıf içinde varlığı mutlak şart değil

  • Ama nesneyi yaratıldıktan sonra ilklemek istiyorsak bu isimde bir metot yazıyoruz

  • Öyle ki bu metotun argümanları nesne new ile oluşturulurken verilen argümanlar


new metodu

p = Point.new(3, 5)
  • Bakın burada new bir nesne üzerinden değil bir sınıf üzerinden çağrılıyor

  • point_create'in karşılığı

  • Yaptığı kabaca şu

    Point sınıfında bir nesne için bellekte yer ayır (`alloc`)
    Sınıf içinde varsa ilklendirici metotu çağır (`initialize`)
    İlklendirilmiş nesneyi dön
    

  • initialize metodunun dönüş değerleri anlamlı değil, çünkü dikkate alınmıyor

  • new metodu initialize ne dönerse dönsün bir nesne dönüyor

  • initialize'ın biricik görevi nesneyi (ör. nesne niteliklerini) ilklemek


to_s metodu

  • Tüm nesnelerde öntanımlı olarak bulunan bir metot, point_print karşılığı

  • Nesnenin metinsel temsili doğrudan veya dolaylı olarak istendiğinde kullanılıyor

  • Dolaylı?

    p = Point.new(3, 5)
    puts p               # metinsel temsili görüntüle
    puts "#{p} noktası"  # veya böyle
    
  • Sınıf içinde to_s yoksa öntanımlı davranış?


Bakın burayı tekrar edelim

puts "#{nesne} falan"

ile

puts "#{nesne.to_s} falan"

Eşdeğerdir


Örneğimizde isleft adını kullanmıştık, Ruby'de "yakışanı" left?

class Point
  def initialize(x, y)
    @x, @y = x, y
  end

  def x
    @x
  end

  def y
    @y
  end

  def left?(other)
    @x < other.x
  end

  def to_s
    "(#{@x},#{@y})"
  end
end

def main
  p, q = Point.new(3, 5), Point.new(9, 7)
  puts "#{p} noktası #{q} noktasının " + (p.isleft(q) ? 'solunda' : 'sağında')
end

main

Hatta left? yerine <

class Point
  def <(other)
    @x < other.x
  end
end

Bu konuya daha sonra geleceğiz


Unutmayın: nesne.metot veya nesne.metot? veya nesne.metot! çağrılarında geçen tüm metotlar tanımlı olmalı

class Sınıf
  def metot
    ...
  end

  def metot?
    ...
  end

  def metot!
    ...
  end
end

nesne = Sınıf.new
nesne.metot
nesne.metot?
nesne.metot!

Tekrar hatırlayalım:

  • Sonu ? ile biten metotlar, metodun mantıksal bir değer (true, false) döndüğünü anlatır

  • Sonu ! ile bitem metotlar, metodun dikkat edilmesi gereken bir "yan etki" ürettiğini, örneğin bir şeylerin üzerine yazıldığını anlatır

  • Bunlar birer "konvansiyon", sona bu karakterleri ekleyerek metoda sihirli bir ekleme yapılmasını sağlamıyorsunuz

  • "Sihri" yazan sizsiniz, sadece isimlendirme yoluyla niyetinizi daha açık ifade etmiş oluyorsunuz


Setters

x niteliğini oku (Getter)

class Point
  def initialize(x, y)
    @x, @y = x, y
  end

  def x
    @x
  end

  def y
    @y
  end
end
  • Niteliğe yazma nasıl yapılıyor? initialize metodunda görüyoruz

  • Nesne metotlarında niteliğe yazmak için basitçe @x = «value» yeterli

  • Ama bu işlem dışarı kapalı

  • x metodu sadece okuma yapar


Bakın böyle:

class Point
  def initialize(x, y)
    @x, @y = x, y
  end

  def x
    @x
  end

  def x=(value)
    @x = value
  end

  def y
    @y
  end

  def y=(value)
    @y = value
  end
end

Bu örnekte görülen x= ve y= metotları birer Setter

  • Yazma işlemini dışarı açan ("public" yapan) metotlar

  • Kısaca yazıcı, "writer" da denilebilir


p = Point.new(3, 5)
p.x = 7
p.y = 19

Aslında bu atamalar birer metot çağrısı

p = Point.new(3, 5)
p.x=(7)
p.y=(19)

  • Ruby'de metot adlarında sonda = varsa bu bir "mutation" semantiği taşır

  • Ruby'de metot çağrılarında parantezler genel olarak zorunlu değil (bu bir karışıklığa yol açmayacaksa)

  • Bir an için Ruby'nin sözdiziminde = karakterinden önce boşluk kullanmaya da izin verdiğini düşünün

p.x=(7)
p.x=7
p.x = 7

"Getters" tekrar

class Point
  def initialize(x, y)
    @x, @y = x, y
  end

  def x
    @x
  end

  def y
    @y
  end
end

Her nitelik için böyle x, y metodlarına benzer "getters" mı yazacağız?


  • Bazı programlama dillerinde evet, her "public" nitelik için birer Getter yazılıyor

  • Ama Ruby'de gereksiz kod işçiliği sevilmez, bunun bir çözümü var


attr_reader

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end
end

  • attr_reader bir meta metot

  • meta? "kod yazan kod" gibi anlayabilirsiniz

  • attr_reader Kendisine verilen her bir sembolü dikkate alarak aynı isimde birer okuyucu metot yazıyor


attr_reader :falan

ile eşdeğer kod

def falan
  @falan
end

attr_reader Ruby programlama dilinin (if, def gibi) bir anahtar kelimesi değil

  • Bu olağan bir metot

  • attr_reader :x, :y'yi attr_reader(:x, :y) olarak okuyun

  • Okuyucu yazılacak metot adlarını birer dizgi veya (en güzeli ve yaygını) sembol olarak veriyoruz

  • Soru: Doğrudan attr_reader x, y yazsaydık ne olurdu?


  • Ruby'de buna benzer başka "meta" metotlar da var (ör. private)

  • Bu sayede Ruby ile "meta programlama" yapabiliyoruz

  • Ruby'ye gerçek gücünü kazandıran bir özellik


"Setter" tekrar

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def x=(value)
    @x = value
  end

  def y=(value)
    @y = value
  end
end

Her nitelik yazıcısı için böyle x=, y= metodlarına benzer "setters" mı yazacağız?


attr_writer

class Point
  attr_reader :x, :y
  attr_writer :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end
end

p = Point.new(3, 5)
p.x =7
p.y = 19

attr_accessor

class Point
  attr_reader :x, :y
  attr_writer :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end
end

yerine

class Point
  attr_accessor :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end
end

attr_reader, attr_writer, attr_accessor: bunlara genel olarak "nitelik erişimcileri" diyoruz

  • Bunları hep aynı tipte kullanmanız gerekmiyor, farklı kombinasyonlar türebilirsiniz

x'e yazılabilir ama okunamaz, y sadece okunabilir (tuhaf bir kombinasyon)

class Point
  attr_writer :x
  attr_reader :y

  def initialize(x, y)
    @x, @y = x, y
  end
end

x'e hem yazılabilir hem okunabilir, y sadece okunabilir (daha makul bir kombinasyon)

class Point
  attr_accessor :x
  attr_reader :y

  def initialize(x, y)
    @x, @y = x, y
  end
end

veya

class Point
  attr_reader :x, :y
  attr_writer :x

  def initialize(x, y)
    @x, @y = x, y
  end
end

Yazıcıları sadece nesnenin dışından değil nesne metotları içinde de kullanabilirsiniz

class Point
  attr_accessor :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def reset
    self.x = 0
    self.y = 0
  end
end

Dikkat!

Böyle değil

  def reset
    x = 0
    y = 0
  end

Böyle

  def reset
    self.x = 0
    self.y = 0
  end

İlk örnekte sorunun ne olduğunu görebiliyor musunuz? x = 0 satırında:

  • x ismi bir yerel değişkeni mi anlatır

  • Yoksa x= metodunu mu anlatır


  • x = denildiğinde tanımlayıcının çözülmesinde yerel değişken seçeneği ön plandadır

  • self.x denildiğinde tanımlayıcının çözülmesine daima önce metot seçeneğiyle başlanır


Veri sarmalama

Örneği inceleyelim

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def move_to_origin
    reset
  end

  def to_s
    "(#{x}, #{y})"
  end

  private

  attr_writer :x, :y

  def reset
    self.x = 0
    self.y = 0
  end

  ...
end

p = Point.new(3, 5)
puts p # (3, 5) görüntülenir

p.move_to_origin
puts p # (0, 0) görüntülenir

Fakat...

p = Point.new(3, 5)

p.reset
p.x = 7

private

Erişim denetimi yapan bir "meta" metot

  • attr_reader vs gibi

  • Kendisinden sonra tanımlanan tüm metotları "private" yapıyor

  • "private"? "mahrem", yani bu metotlara dışarıdan .metot şeklinde erişilmesine izin verilmiyor


Bunu bir metot adı verirseniz, "kendisinden sonra gelen tüm metotlar" yerine sadece adı verilen metotu private yapar

  • Dikkat! Metot önceden tanımlanmış olmalı
class Point
  attr_accessor :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def move_to_origin
    reset
  end

  def to_s
    "(#{x}, #{y})"
  end

  def reset
    self.x = 0
    self.y = 0
  end

  private :reset
end

public

Buna ilk başta değinmeye bile gerek duymadık

  • Erişim denetimi açıkça tanımlanmadığı sürece her şey "public"

  • Yani private (ve daha sonra değinilecek olan protected) yoksa daima public


Şöyle kombinasyonlar mümkün (ama yaygın değil), deneyin

class Falan
  private

  def this_is_private
  end

  def not_private_this_is_public
  end

  public :not_private_this_is_public

  def also_private
  end

  public

  def now_public
  end

  def not_public_this_is_private
  end

  private :not_public_this_is_private

  def also_public
  end
end

Veri Sarmalama

  • private ve public nesneye erişimi denetliyor

  • Bu sayede nesnenin "paketlediği" veriyi sarmalamış oluyoruz

  • Sarmalanmış veri paketine erişim tamamen bizim denetimimizde


Ne kadar az şeyi dışarı açarsak o kadar iyi

  • Dışarıya açılan her şey, dış dünyaya verdiğiniz bir söz

  • Ne kadar az söz verirseniz o kadar çok rahat edersiniz

  • Dışarıya sunulması açıkça gerekmeyen her şeyi private yapın

  • Şüphe halinde de ("belki bir gün birisi kullanır") private yapın, bırakın o gün gelsin


Bakın tipik bir örnek

class Foo
  attr_reader :x, :y

  def initialize(a, b)
    @x = calculate_with(a)
    @y = calculate_with(b)
  end

  private

  def calculate_with(param)
    ...
  end
end

  • Bu örnekte dış dünya için önemli olan :x ve :y: dış işleri

  • calculate_with sizin iç işiniz: iç işleri

  • İçeride dilediğiniz zaman dilediğiniz düzenlemeyi yapabilirsiniz


Örneğin gelecekte calculate_with'i kaldırıp bunun yerine ayrı iki fonksiyon kullanmaya karar verdiniz

class Foo
  attr_reader :x, :y

  def initialize(a, b)
    @x = calculate_for_x(a)
    @y = calculate_for_y(b)
  end

  private

  def calculate_for_x(a)
    ...
  end

  def calculate_for_y(b)
    ...
  end
end

İç işlerindeki bu düzenlemenin dış dünya açısından bir önemi yok değil mi?

  • Peki ya calculate_with public olsaydı?

  • calculate_with'i sancısız şekilde kaldıramazdınız

  • Çünkü bu metot dış dünyaya verdiğiniz bir söz

  • Sözleri geri almak her zaman zordur


Nitelik yazıcıları tekrar

class Point
  attr_accessor :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def to_s
    "(#{x}, #{y})"
  end
end

Soru: Nitelik yazıcılarını bu şekilde "public" yapmak doğru mu?

  • Nokta nesnesinin x ve y nitelikleri, nesne inşa edildikten sonra değiştirilebiliyor

  • Bu, nokta nesnesi müşterilerine verilmesi doğal olan bir kabiliyet mi?


  • Yazıcılar açık olmasaydı, koordinatlar hakkında noktanın oluşturulduğu yere bakarak akıl yürütebilirdik

  • Ama artık bu yeterli değil, nokta oluşturulduktan sonra değişmiş olabilir


Nokta tam olarak ne?

  • Noktanın doğal karakteri üzerinde duralım: asli özellikler

  • Asli özellikler: Bir nesneyi karakterize eden temel özellikler

  • Bir noktanın asli özellikleri: x ve y


Yazıcılarla ilgili temel sorun:

Asli özellikleri değiştirilebilir yapmak nesnenin yaşam döngüsü içinde bazı kararların alınmasını güçleştiriyor


  • Bunu yapmayın!

  • Nitelik yazıcıları istisnai durumlarda yararlanabileceğiniz bir olanak olmalı

  • İstisnai durumlar arasında nesneyi karakterize eden temel nitelikler yok

  • Nesneleri inşa edildikten sonra olabildiğince "değiştirilemez" (immutable) yapmak bir norm olmalı


"İç işlerinde yazıcılara" belki ihtiyaç olabilir, nispeten kabul edilebilir bir çözüm:

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def to_s
    "(#{x}, #{y})"
  end

  ...

  private

  attr_writer :x, :y

  ...
end

  • Bu gerçeklemede yazıcılar sadece "iç işlerinde" kullanılabilir durumda

  • Yararı tartışılır: @x = yerine self.x = yazabilme olanağı


Ama bakın şu kod daha anlamlı!

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def to_s
    "(#{x}, #{y})"
  end

  ...

  private

  def x=(value)
    ... # her x değişikliği girişimde gerçekleşecek bir işlem
    @x = value
  end

  def y=(value)
    ... # her y değişikliği girişimde gerçekleşecek bir işlem
    @y = value
  end

  ...
end

Her nitelik değişiminde gerçekleşmesini istediğiniz işlem?

Örnekler:

  • Niteliğe atanacak değerin validasyonu

  • Nitelik değeri değiştiğinde buna bağlı bazı parametrelerin de değiştirilmesi (tekrar hesaplanması)


Kıssadan Hisse

  • Nesneleri kurgularken sorulması gereken ilk soru: "Nesnenin asli özellikleri nedir?"

  • Asli özellikleri nesne inşa edildiğinde ilkleyin ve sonrasında değiştirmeyin

  • Metotları sadece gerekliyse dışarı açın

  • Nitelik yazıcılarından kaçının


Noktanın yer değiştirmesi

Bir noktanın yerinin değiştirilmesi: move metodu

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def move(delta_x, delta_y)
    @x += delta_x
    @y += delta_y
  end
end

Bu metotun sorunları neler?


  • Nesnenin asli özelliklerini değiştiriyor

  • (birincil önemde olmayan sorun) metotun dönüş değeri anlamı değil


class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def move(delta_x, delta_y)
    Point.new(@x + delta_x, @y + delta_y)
  end

  def to_s
    "(#{x}, #{y})"
  end
end

Noktanın yerini değiştirdiğimizde artık yeni bir noktadan söz etmeliyiz

  • Point.new mevcut nokta nesnesi ve değişim miktarlarını dikkate alarak yeni bir nokta nesnesi üretiyor

  • move metodu bir nesne dönüyor


Kompozisyon

Gelin bir "Dikdörtgen" nesnesi tasarlayalım, bir dikdörtgeni nasıl tanımlarsınız?

    y
    ^
    |
    |             width
    |         +----------+
    |         |          |
    |         |          | height
    |         |          |
    |         +----------+
    |
    |
    +----------------------------------------------> x

  • Dikdörtgen eni ve boyu olan bir geometrik nesne

  • En ve boy dışında bir nitelik daha olmalı mı?

  • Eni ve boyu aynı olan iki farklı dikdörtgen?


Dikdörtgenin "pozisyonu"

  • Pozisyon? Dikdörtgenin köşe noktalarından biri yeterli

  • Pozisyon bir nokta, yani nokta nesnesi

  • Standart olarak "sol üst" köşe noktası ("top-left") seçilebilir


Dikdörtgen tekrar

Dikdörtgen en ve boy uzunluklarıyla karakterize edilen, sol üst köşe noktası olan bir geometrik nesnedir


class Rectangular
  attr_reader :position, :width, :height

  def initialize(point, width, height)
    @position = point
    @width    = width
    @height   = height
  end
end

  • Bu gerçeklemede position niteliği bir nokta nesnesi (point)

  • Yani? Bir nesnenin (Rectangular) kompozisyonunda başka nesneler (Point) olabilir

  • NYP metodolojisinde çok güçlü bir tasarım enstrümanı → Kompozisyon


Kompozisyon sayesinde önceden yazılmış kodları tekrar kullanabiliyoruz

  • Buna "Code Reuse" deniliyor

  • NYP'nin önemli vaadlerinden biri

  • Kompozisyon "Code Reuse" için bir enstrüman

  • Başka enstrümanlar da var: ör. "Kalıtım" (buna geleceğiz)


Bu örnekte "Code Reuse" nerede/nasıl gerçekleşiyor?

  • Dikdörtgenin pozisyonu bir nokta nesnesi

  • Dikdörtgenin pozisyonuyla ilişkili işlemleri bu nesneye delege edebiliriz

  • Delege edilen işlemleri Rectangular içinde gerçeklememiz gerekmiyor

  • Örnek? Dikdörtgenin yer değiştirmesi


class Rectangular
  attr_reader :position, :width, :height

  def initialize(point, width, height)
    @position = point
    @width    = width
    @height   = height
  end

  def move(delta_x, delta_y)
    @position = @position.move(delta_x, delta_y)
  end
end

Soru: Bu gerçekleme position niteliğini değiştiriyor?

  • Evet, eldeki probleme göre bu hatalı bir tasarım olabilir

  • Ama mermere kazınmış mutlak bir kanun değil bu

  • Tevil edelim, position dikdörtgenin asli bir özelliği mi?

  • Asli özellikler en ve boy olarak yorumlanabilir

  • Eni ve boyu aynı iki dikdörtgenin pozisyonlar farklı olsa bile bizdeki görsel algısı benzer


class Rectangular
  attr_reader :position, :width, :height

  def initialize(point, width, height)
    @position = point
    @width    = width
    @height   = height
  end

  def move(delta_x, delta_y)
    @position = @position.move(delta_x, delta_y)
  end

  def to_s
    "[#{width}x#{height}]@#{position}"
  end
end

def main
  r = Rectangular.new(Point.new(0, 0), 3, 5)
  puts r
end

to_s metoduna Point nesnesinin katkısını not edin

  • Dikdörtgen pozisyonunun metinsel tarifini nokta nesnesine delege ettik

  • Delegasyona daha ayrıntılı bakacağız


Rectangular nesnesinin nasıl inşa edildiğine bakın

class Rectangular
  attr_reader :position, :width, :height

  def initialize(point, width, height)
    @position = point
    @width    = width
    @height   = height
  end

  ...
end

def main
  r = Rectangular.new(Point.new(0, 0), 3, 5)
  puts r
end

  • İnşa edici 3 "pozisyonel" argüman alıyor: position, width, height

  • Genel ilke: bir metoda "pozisyonel" yolla geçirilen argümanların sayısı çok olmamalı

  • Neden? Pozisyon hatası yapılabilir, okunurluk azalır

  • Örnekte bu sayı 3, ayrıca ilk argümanla diğer ikisi karakter olarak çok farklı

  • Müşteri kodundaki görüntü sıkıntılı: r = Rectangular.new(Point.new(0, 0), 3, 5)


Pozisyonel argüman sayısı fazlaysa "İsimlendirilmiş argüman" kullanın

class Rectangular
  attr_reader :position, :width, :height

  def initialize(position:, width:, height:)
    @position = position
    @width    = width
    @height   = height
  end

  ...
end

def main
  r = Rectangular.new(position: Point.new(0, 0), width: 3, height: 5)
  puts r
end

Hatta bir parça "öntanımlılığa" yaslanabiliriz

  • position verilmezse orijin noktası alınsın

class Rectangular
  attr_reader :position, :width, :height

  def initialize(position: Point.new(0, 0), width:, height:)
    @position = position
    @width    = width
    @height   = height
  end

  ...
end

def main
  r = Rectangular.new(width: 3, height: 5)
  puts r
end

Örnek: Dikdörtgenin SVG çizimi

Verilen bir dikdörtgen nesnesinin SVG formatında vektörel çizimini elde edin


Delegasyon

Kompozisyon ile el ele görünen bir terim

  • Nesne arayüzünde yer alan bazı metotların kompozisyona giren iç nesneye havale edilmesi

Dikdörtgenin (pozisyonun) merkez noktasına olan uzaklığı

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def distance
    Math.sqrt((@x * @x) + (@y * @y))
  end
end

class Rectangular
  attr_reader :position, :width, :height

  def initialize(position: Point.new(0, 0), width:, height:)
    @position = position
    @width    = width
    @height   = height
  end

  def distance
    @position.distance
  end
end

def main
  r = Rectangular.new(position: Point.new(1, 0), width: 3, height: 5)
  puts r.distance
end

Uzaklık hesabı içteki pozisyon nesnesine delege edilmiş

  ...

  def distance
    @position.distance
  end

  ...

Kompozisyonda sıklıkla karşılaşılan bir durum

  • Bu örnekte arayüzdeki sadece bir metot delege ediliyor

  • Olağan senaryolarda bu şekilde delege edilen pek çok metot olabilir


  • Kompozisyon gerçek gücünü bu tür delegasyonlarda buluyor

  • Ana nesne problemin tüm unsurlarıyla tek başına mücadele etmiyor

  • Yeri geldiğinde sorumluluğu kompoze edilen iç nesnelere havale ediyor


Delegasyon karmaşıklıkla mücadele etmenin bir yolu

  • Gerçek hayatta da böyle değil mi?

  • Yoğun iş yüküyle mücadele etmenin en etkili yollarından biri: gelen bazı işleri yardımcılara havale etmek

  • Dışarıdan bakıldığında sorumlu biziz ama gerçekte işi yapan bir başkası


Forwardable modülü

Madem ki sık karşılaşılıyor, Ruby'de "delegasyon"u kolaylaştırıcı özellikler var mı?

  • Evet var

  • En bilineni Forwardable modülü (ama bununla sınırlı değil)


require 'forwardable' # Standart kitaplıktan yükle

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def distance
    Math.sqrt((@x * @x) + (@y * @y))
  end
end

class Rectangular
  extend Forwardable # Delegasyon için gerekli DSL'i (ör. def_delegator) etkinleştir

  def_delegator :@position, :distance # Delegasyonu tanımla

  attr_reader :position, :width, :height

  def initialize(position: Point.new(0, 0), width:, height:)
    @position = position
    @width    = width
    @height   = height
  end
end

def main
  r = Rectangular.new(position: Point.new(1, 0), width: 3, height: 5)
  puts r.distance
end

Katıştırma: extend

  • Forwardable modülünü Rectangular nesnesine "katıştırıyoruz"

  • Katıştırma → "mixin"

  • Katıştırma include veya extend metotlarıyla gerçekleşiyor

  • Bu örnekte extend (neden olduğuna daha sonra değineceğiz)


class Rectangular
  extend Forwardable

  def_delegator :@position, :distance

  ...
end

extend Forwardable?

  • Forwardable modülünde delegasyon tanımlayan bir DSL var

  • Bu satır sayesinde tüm Rectangular nesnelerinde sınıf düzeyinde def_delegator DSL metotu kullanılabiliyor

def def_delegator :@position, :distance?

  • Rectangular nesnesinin distance metotu uyarıldığında @position nesnesinin aynı isimli metotu uyarılıyor

  • def_delegator aslında basitçe distance adında bir metot yazıyor, tanıdık geldi mi?


Katıştırma: include

  • Ruby'de "katıştırma"nın daha yaygın bir şekli include

  • Şimdi bunu örnekleyelim


Dikdörtgen nesnelerini sırala

  • Elimizde rastgele sırada dikdörtgen nesnelerinden oluşan bir dizi var

  • Diziyi nasıl sıralarız?

...

def main
  rectangulars = [
    Rectangular.new(position: Point.new(0, 0), width: 3,  height: 5),
    Rectangular.new(position: Point.new(1, 0), width: 2,  height: 4),
    Rectangular.new(position: Point.new(0, 0), width: 10, height: 6),
    Rectangular.new(position: Point.new(2, 4), width: 1,  height: 2)
  ]

  rectangulars.sort.each do |r|
    puts r
  end
end

Sıralamanın ölçüsü ne olmalı? Bunu biz tanımlayacağız

  • İki dikdörtgenden alanı küçük/eşit/büyük olan diğerinden küçük/eşit/büyüktür, pozisyondan bağımsız olarak

  • Tanım neyse sıralama da ona göre yapılacak

  • Şu anki tanımımız bu, unutmayın bu bir tanım, farklı da olabilirdi


class Rectangular
  include Comparable

  attr_reader :position, :width, :height

  def initialize(position: Point.new(0, 0), width:, height:)
    @position = position
    @width    = width
    @height   = height
  end

  def area
    width * height
  end

  def <=>(other)
    area <=> other.area
  end
end

Comparable modülü

  • "Sıralanabilir" nitelikteki türdeş bir nesne topluluğunda nihai "sıralama" işlemini yapıyor

  • Nasıl?

  • "Sıralama"da ne yapıldığını daha iyi anlamamız lazım


"Sıralama" bir kolleksiyonda dolaşarak yapılan bir işlem

  • Her seferinde kolleksiyonda iki öge seçiliyor

  • Seçilen iki öge karşılaştırılıyor

  • Karşılaştırma üç sonuçlu bir işlem: küçük, eşit, büyük


Sıralama algoritması

  • Kolleksiyonda rastgele bir sırayla dolaşmamalıyız, bunun "optimum" bir yolu olmalı

  • Kolleksiyonda dolaşma ve öge çiftinin seçimi "sıralama algoritması"nın konusu

  • Sıralama algoritması? Quick sort, Bubble Sort, Merge Sort vb


  • Sıralama algoritması kolleksiyonu oluşturan türdeş nesnelerin ne olduğundan bağımsız

  • Sıralamanın probleme yani nesne türüne bağlı olan tek tarafı "karşılaştırma"


  • Comparable modülü "bana karşılaştırma lojiğini ver, sıralamayı ben hallederim" diyen bir modül

  • Karşılaştırma lojiği → <=> metodu


Karşılaştırma: <=>

  • Üç sonuçlu bir işlem: küçük, eşit, büyük

  • Bu nedenle <=> gibi bir gösterimi var

  • Sayısal olarak ifade edecek olursak: negatif, sıfır, pozitif

  • Buna "spaceship operator" da deniliyor (cepheden bakıldığında bir uzay gemisi)


Örneğimizde karşılaştırma lojiği

class Rectangular
  ...

  def <=>(other)
    if area < other.area
      -1 # negatif herhangi bir sayı da olur
    elsif area > other.area
      1  # pozitif herhangi bir sayı da olur
    else
      0  # eşit ise daima 0
    end
  end
end

Veya...

class Rectangular
  ...

  def <=>(other)
    area <=> other.area
  end
end

Comparable: "Bana yeter ki karşılaştırma lojiğini ver, ben de sana en optimum sıralama metotlarını sağlayacağım!"

  • include Comparable ne yapıyor? İlgili nesnelerden oluşan dizide sort ailesindeki metotları sağlıyor

  • Dikkat buyurun, bu metotlar Rectangular nesnesinde tanımlı değil


...

def main
  rectangulars = [
    Rectangular.new(position: Point(0, 0), width: 3,  height: 5),
    Rectangular.new(position: Point(1, 0), width: 2,  height: 4),
    Rectangular.new(position: Point(0, 0), width: 10, height: 6),
    Rectangular.new(position: Point(2, 4), width: 1,  height: 2)
  ]

  rectangulars.sort.each do |r|
    puts r
  end
end

"Katıştırma" konusuna daha sonra ayrıntılı değineceğiz


Veri sarmalama: protected

Gelin karşılaştırma lojiğini değiştirelim

  • Karşılaştırma bağlamında dikdörtgen "büyüklüğünün" ölçüsü olarak alan dışında orijine olan uzaklığı da hesaba katalım

  • Hayali bir nitelik: bigness

  • bignessdistance x area


class Rectangular
  include Comparable

  extend Forwardable

  def_delegator :@position, :distance

  ...

  def area
    weight * height
  end

  def bigness
    distance * area
  end

  def <=>(other)
    bigness <=> other.bigness
  end
end

Fakat bigness dışarı açık tutmak istemediğimiz bir nitelik

  • Sadece karşılaştırmada anlamlı bir nitelik

  • Dış dünya (müşteriler) bunu bilmese de olur, bu bizim iç işimiz

  • Veri sarmalama → Dışarı olabildiğince az bilgi sızdır


O halde bigness private olmalı?

class Rectangular
  include Comparable

  extend Forwardable

  def_delegator :@position, :distance

  ...

  def area
    weight * height
  end

  def <=>(other)
    bigness <=> other.bigness
  end

  private

  def bigness
    distance * area
  end
end

Fakat bir problemimiz var

  • Karşılaştırma sırasında other.bigness çağrısı hata üretir

  • Çünkü other nesnesinin private bir metodunu uyarıyoruz


Şunu diyebiliyor muyuz?

  • "Evet gizli, fakat aile arasında sır olmasın"

  • Aile? Aynı türde, yani aynı sınıf veya mirasçılarından inşa edilmiş nesneler


Evet bunu yapabiliyoruz; private yerine protected kullanarak

class Rectangular
  include Comparable

  extend Forwardable

  def_delegator :@position, :distance

  ...

  def area
    weight * height
  end

  def <=>(other)
    bigness <=> other.bigness
  end

  protected

  def bigness
    distance * area
  end
end

protected niteleyicisi

  • private'ın gevşetilmiş hali

  • Aynı türde, yani aynı sınıf veya mirasçılarından inşa edilmiş nesneler üzerinden uyarılabilen metotlar

  • Dışarı sızdırmak istemediğimiz iç işleri kontratları için kullanılıyor

  • Gerçek hayat problemlerinde (sayıları çok yüksek olmasa da) böyle durumlar var

  • Örneğimizdeki "iç işleri kontratı" → bigness


Meta metotlar

  • to_s

  • <=>

Böyle "işe yarar" başka metotlar var mı?


eql?: Eşdeğer mi?

  • to_s ve <=> gibi bir meta metot

  • Her nesnede (en üst ata sınıftan miras alınarak gelen) bir varsayılan gerçeklemesi bulunuyor

  • Varsayılan? (Basitleştirecek olursak) nesnelerin object_idleri

  • Diğer meta metotlarda olduğu gibi varsayılanı değiştirmek ne işe yarıyor? Örnekleyeceğiz


Problem: Rectangular nesnelerinden oluşan bir diziyi tekilleştir

  • Dizide tekrar eden nesneleri çıkar

  • Tekrar eden nesneler? Aynı nitelikteki yani "eşdeğer" dikdörtgenler

  • Yapılan işlemin adı: uniq, tekilleştirme


Eşdeğerliğin ölçüsü ne olmalı? Bunu biz tanımlayacağız

  • İki dikdörtgenin pozisyonu, eni ve boyu aynı ise bu dikdörtgenler eşdeğerdir

  • Tanım neyse eşdeğerlik denetimi de ona göre yapılacak

  • Şu anki tanımımız bu, unutmayın bu bir tanım, farklı da olabilirdi


Dizideki tekrarı görebiliyor musunuz? Rectangular.new(Point(0, 0), 3, 5) nesneleri

...

def main
  rectangulars = [
    Rectangular.new(position: Point.new(0, 0), width: 3, height: 5),
    Rectangular.new(position: Point.new(1, 0), width: 2, height: 4),
    Rectangular.new(position: Point.new(0, 0), width: 3, height: 5),
    Rectangular.new(position: Point.new(2, 4), width: 4, height: 2)
  ]

  puts rectangulars.uniq.size # 3
end

class Point
  ...

  def eql?(other)
    [x, y] == [other.x, other.y]
  end

  ...
end

class Rectangular
  ...

  def eql?(other)
    [position, width, height] == [other.position, other.width, other.height]
  end

  ...
end

  • Bakın bu örnekte "pozisyonların eşdeğerliği"ni Point nesnesine havale ettik

  • Eşdeğerlik tanımını değiştirebiliriz

  • Örneğin sadece alanları aynı olan dikdörtgenleri eşdeğer kabul etmek

  • Bu yapıldığında sonuç ne olur? Bu tanıma uygun eql? metodunu yazabilir misiniz?


Sınıf metotları

İnşa edici metota bakalım

class Rectangular
  attr_reader :position, :width, :height

  def initialize(position: Point.new(0, 0), width:, height:)
    @position = position
    @width    = width
    @height   = height
  end

  ...
end

def main
  r = Rectangular.new(width: 3, height: 5)
  puts r
end

Öntanımlı orijin noktası için daha okunur bir düzenleme yapabilir miyiz?

class Rectangular
  attr_reader :position, :width, :height

  def initialize(position: Point.origin, width:, height:)
    @position = position
    @width    = width
    @height   = height
  end

  ...
end

def main
  r = Rectangular.new(width: 3, height: 5)
  puts r
end

Point.origin

  • origin nasıl bir metot?

  • Metodu ne üzerinden uyarıyoruz? .'nın solunda ne var?


Point.origin

  • Bu bir sınıf metodu

  • Çünkü metot bir nesne üzerinden değil bir sınıf üzerinden uyarılıyor

  • Böyle başka metotlarla karşılaştınız mı? Evet, new

  • new özel bir sınıf metotu (ama üzerine yazılabilir)


Bir sınıf tanımında

  1. Nesne üzerinden uyarılacak şekilde tanımlanan bir metota nesne metodu ("instance method") diyoruz

  2. Sınıf üzerinden uyarılacak şekilde tanımlanan bir metota sınıf metodu ("class method") diyoruz

Yaygın olan ilki: hemen hemen daima nesne metotlarıyla çalışıyoruz


class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y= y
  end

  def Point.origin
    Point.new(0, 0)
  end
end

Ama bunun yakışanı:

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y= y
  end

  def self.origin
    new(0, 0)
  end
end
  • Point yerine self yeterli

  • Sınıf metotunun gövdesinde self zaten Point'i gösterdiğinde self.new yerine new yeterli


Miras alma

Bir Square (kare) nesnesi gerçekleyelim

class Square
  attr_reader :position, :side

  def initialize(side, position: Point.origin)
    @position = position
    @side     = side
  end

  def area
    side * side
  end

  def periphery
    4 * side
  end

  def to_s
    "#{side}@#{position}"
  end

  ...
end

def main
  squares = [Square.new(3), Square.new(1, position: Point.new(3, 5))]
  squares.each do |square|
    puts square
  end
end

Rectangular nesnesi için yapılan gerçeklemeyi neredeyse tekrar ediyoruz

  • "Code reuse" yapıyoruz

  • Rectangular gerçeklemesindeki kodları kullanmanın bir yolu yok mu?


Rectangular gerçeklemesini hatırlayalım

class Rectangular
  attr_reader :position, :width, :height

  def initialize(position: Point.origin, width:, height:)
    @position = position
    @width    = width
    @height   = height
  end

  def area
    width * height
  end

  def periphery
    2 * (width + height)
  end

  def move(delta_x, delta_y)
    @position = @position.move(delta_x, delta_y)
  end

  def to_s
    "[#{width}x#{height}]@#{position}"
  end
end

Miras alma (inheritance) kullanacağız

class Square < Rectangular
  ...
end
  • Sözdizimi Çocuk sınıf < Ata sınıf

  • "Square bir tür Rectangular'dır" demiş oluyoruz

  • Ya da "Square özelliklerini Rectangular'dam miras alır"


Fakat farklılıklar var, bunları nasıl yöneteceğiz?

  • CTOR'lar farklı

    square = Square.new 3, position: Point.new(3, 5)
    rectangular = Rectangular.new width: 3, height: 3, position: Point.new(3, 5)
    
  • Metinsel temsiller farklı (yukarıdaki örnekler için)

    puts square # 3@(3, 5)
    puts rectangular # [3, 3]@(3, 5)
    
  • Öte yandan bunların dışındaki neredeyse tüm metotlar aynı


Miras alma mekaniği

Denemeler yapalım

class A
  def initialize
  end

  def meth_inside_a
    puts 'meth_inside_a'
  end

  def some_other_meth
    puts 'defined in a'
  end
end

class B < A
  def meth_inside_b
    puts 'meth_inside_b'
  end

  def some_other_meth
    puts 'defined in b'
  end
end

a = A.new
b = B.new

a.meth_inside_a
a.some_other_meth

b.meth_inside_a
b.meth_inside_b
b.some_other_meth

a.meth_inside_b

  • Ata sınıf gereçeklemesi çocuk sınıfa geçiyor

  • Çocuk sınıf Ata sınıftan miras aldığı bazı metotların üzerine yazmış

  • "Üzerine yazma" sadece çocuk sınıfı etkiliyor, Ata sınıf etkilenmiyor


O halde şöyle olabilir mi?

class Square < Rectangular
  attr_reader :position, :side

  def initialize(side, position: Point.origin)
    @position = position
    @side     = side
  end

  def area
    side * side
  end

  def periphery
    4 * side
  end

  def to_s
    "#{side}@#{position}"
  end
  ...
end

İyi bir başlangıç , fakat çok verimsiz:

  • area, periphery gerçeklemeleri tekrar ediyor

  • Temel sorun Rectangular'daki tüm metotlarda width, height ayrımı olması

  • Bu ayrımı koruyup sadece width = height = side özelleştirmesi yapan bir çözüm lazım


class Square < Rectangular
  attr_reader :side

  def initialize(side, position: Point.origin)
    super(position: position, width: side, height: side)

    @side = side
  end

  def to_s
    "#{side}@#{position}"
  end
end

super

Çağrıldığı noktada içinde bulunduğu metodun Ata sınıftaki karşılığını çalıştırır

class A
  def meth
    puts 'meth in A'
  end
end

class B < A
  def meth
    super

    puts 'meth in B'
  end
end

a = A.new
b = B.new

b.meth

Dikkat! Argüman vermeden salt super olarak çağrılırsa içinde bulunduğu metoda geçirilen tüm argümanları Ata sınıftaki metoda geçirir

class A
  def meth(x)
    puts "meth in A: #{x}"
  end
end

class B < A
  def meth(x)
    super

    puts 'meth in B'
  end
end

a = A.new
b = B.new

b.meth 42

super'in kullanıldığı metot argüman alıyor fakat Ata sınıftaki metod argüman almıyorsa super() şeklinde kullanın


Şecere (Soy ağacı)

Nesnenin soy ağacını inceleyelim

class A; end

class B < A; end

A.ancestors #=> [A, Object, Kernel, BasicObject]
B.ancestors #=> [B, A, Object, Kernel, BasicObject]

Object.ancestors #=> [Object, Kernel, BasicObject]
Kernel.ancestors #=> [Kernel]
BasicObject.ancestors #=> [BasicObject]
  • Ruby'de tüm nesnelerin görünmeyen ataları var: en tepede BasicObject, altında Object

Fakat Kernel böyle değil (bu bir nesne mi)? Ruby'de bir şeyin ne olduğunu sorgulamak için class'ına bakın

class A; end
class B < A; end

A.class #=> Class
B.class #=> Class
Object.class #=> Class
Kernel.class #=> Module
BasicObject.class #=> Class
  • class metoduyla Class tipini karıştırmayın

  • A.class #=> Class şunu diyor: A sabitiyle gösterilen şey bir sınıf


Kernel.class #=> Module: Kernel sabitiyle gösterilen şey bir modül

  • İşte bu ilginç, modüller bir nesnenin nasıl atası olabiliyor?

  • Bir başka soru? Bir nesne aynı anda (seviyede) birden fazla atadan miras alabilir mi?


Katıştırma (mixin) tekrar

Katıştırmanın sırrı

module M; end

class A; end
class B < A; include M; end

B.ancestors #=> [B, M, A, Object, Kernel, BasicObject]

include M ile bir modülün bir sınıfa katıştırılması da bir "miras alma etkisi" yaratıyor

  • M'nin A'dan önce geldiğini not edin, neden? Önce B < A etkin sonra include M

Karşılaştıralım

module M
  def some_meth
    puts 'some_meth'
  end
end

class A
  include M
end

ile

class M
  def some_meth
    puts 'some_meth'
  end
end

class A < M
end

Görüldüğü gibi some_meth metodunun varlığı açısından ikisi de aynı nihai sonucu veriyor, fakat ciddi bazı farklar var

  • class A < M statik bir miras almadır, miras alma ilişkisi çok erken kuruluyor

  • include M dinamik bir miras alma, miras alma ilişkisi çalışma zamanında daha geç fazda kuruluyor


Çoklu miras alma

Multiple inheritance

  • Ruby'de "statik" anlamda çoklu miras alma yoktur, aynı anda sadece bir Ata'dan miras alabilirsiniz

  • Fakat katıştırma yoluyla çoklu miras alma mümkün


module M; end

class A; end
class B < A; include M; end

B.ancestors #=> [B, M, A, Object, Kernel, BasicObject]

Bakın B'nin iki atası var M ve A

  • Bir tür çoklu miras alma

  • Ama M'in yine de bir modül olduğunu unutmayın


Dinamik miras alma

  • Miras alma ilişkisi < operatörüyle kurulduğunda bu bir statik miras alma

  • Bu operatörü kullanmadan da miras alma ilişkisi kurulabiliyor

  • Buna dinamik miras alma diyoruz

  • Örneğin katıştırmanın bir tür dinamik miras alma olduğunu artık biliyoruz


module M; end
class A; end

class B < A; end

class C; include M; end

D = Class.new A
  • Bu örnekte D nesnesi A nesnesinden dinamik olarak miras alıyor

  • "D adında bir sınıf oluştur, öyle ki atası A olsun"


  • Bu tür dinamik miras alma ata sınıfın çalışma zamanında belirlendiği senaryolarda yararlı

  • Bu çok olağan bir senaryo değil

  • Bir dinamik programlama dili olan Ruby'nin gücünü gösteren etkili bir örnek


Dinamik miras almada sınıf gövdesi nerede? Basitçe blok kullanıyoruz

D = Class.new A do
  ...
end
  • Bazı senaryolarda bu çok önemli bir avantaj sunuyor: "Closures"

  • Daha sonra değineceğimiz bir avantaj

  • Ama genel olarak dinamik miras almadan kaçınıyoruz, basit olanı (statik miras alma) varken


  • Daha olağan bir senaryo: özel "İstisna" ("Exception") nesneleri oluşturmak
ParseError = Class.new StandardError

İstisnalar

Programda ortaya çıkan beklenmedik, yani "istisnai" durumları yönetmekte kullanılıyor

>> 3/0
(irb):1:in `/': divided by 0 (ZeroDivisionError)
        from (irb):1:in `<main>'
    ...

ZeroDivisionError: Sıfıra bölme hatası


  • İstisnanın hatayı temsil eden bir nesne olduğuna dikkat edin

  • Nesnenin türü → İstisna sınıfı

  • Örnekte ZeroDivisionError


Bu güne kadar istisnalarla çok karşılaştık

  • Çünkü Ruby (ve benzeri pek çok programlama dilinde) hataları hemen hemen daima istisnalar ile yönetiyoruz

SyntaxError: Söz dizimi hatası

>> 0rder = 5
...in `eval': (irb):5: syntax error, unexpected local variable or method, expecting end-of-input (SyntaxError)
0rder = 5
 ^~~~
  • Tanımlayıcılar rakamla başlayamaz

NameError: İsim hatası

>> puts x
(irb):7:in `<main>': undefined local variable or method `x' for main:Object (NameError)
        ...
  • x ismi herhangi bir değişken veya metot adı değil

ArgumentError: Argüman hatası


>> "foo bar".delete_prefix
(irb):2:in `delete_prefix': wrong number of arguments (given 0, expected 1) (ArgumentError)
        from (irb):2:in `<main>'
        ...

NoMethodError: Olmayan metot hatası

>> 3.foo
(irb):3:in `<main>': undefined method `foo' for 3:Integer (NoMethodError)
Did you mean?  floor
        ...
  • Tamsayılarda foo adında bir metot yok

Çok sık karşılaşılan bir NoMethodError hatası

>> i = nil
=> nil
>> i.foo
(irb):10:in `<main>': undefined method `foo' for nil:NilClass (NoMethodError)
        ...
  • nil nesnesinde foo adında bir metot yok

  • Dinamik dillerin laneti!

  • Bu hatayla "çalışma zamanında" sık karşılaşabiliriz


  • Bir nesneye eriştik, ama bu örnekte olduğu gibi nil olduğu açık şekilde görülen bir i erişimi değil

  • Belki bir işlemin sonucuyla veya metota geçirilen bir argümanla

  • Kodu okuduğumuzda nesnenin nil olacağına ilişkin bir belirti yok, hava aydınlık

  • Bağlama göre nesnenin tipini ve dolayısıyla hangi metotlara cevap verdiğini biliyoruz

  • Fakat işe bakın ki bir programlama hatası olarak nesne boş yani nil çıkıyor


def uppercase_first_on_match(strings, character)
  strings.find { |string| string.include? character }.upcase
end

uppercase_first_on_match %w[apple orange banana melon], 'a' #=> APPLE

Peki ya böyle?

uppercase_first_on_match %w[apple orange banana melon], 'i'  # undefined method `upcase' for nil:NilClass (NoMethodError)

Bu tür hataların doğru yönetilmesi lazım

  • Doğru yönetmek → Doğru yerde, doğru şekilde yönetmek

Nesnenin nil olma ihtimali varsa (bu durum olağan yani istisnai değilse)

def uppercase_first_on_match(strings, character)
  string = strings.find { |string| string.include? character }
  string.upcase if string
end

veya

def uppercase_first_on_match(strings, character)
  string = strings.find { |string| string.include? character }
  string&.upcase
end

Nesnenin nil olması olağan değilse, yani bu beklenmeyen bir durumsa

  • Hatayı hangi katmanda yöneteceğimiz bakmamız lazım

Sorular

  • Hatayı bununla karşılaştığımız metot içinde mi yönetelim?

  • Yoksa bu metotu çağıran tarafta mı yönetelim?


Hatayı yaşadığımız nesne bizim denetimimizde değilse metot içinde yönetmeyi tercih edebiliriz

def uppercase_first_on_match(strings)
  string = strings.find { |string| string.include? 'a' }
  raise 'At least one string must have an "a"' unless string

  string.upcase
end

Hata üret: raise

İstisna üretiyor

  • Genel formu: raise [«İSTİSNA SINIFI», ] «HATA İLETİSİ»

  • Örnekte bir istisna sınıfı yani hata tipi tanımlamadık

  • raise hata tipi tanımlanmadığında RuntimeError tipinde bir hata üretir

  • Bu doğru bir pratik değil, hata tipini tanımlamamız lazım

  • Neden? Bunun üzerinde daha sonra duracağız


def uppercase_first_on_match(strings)
  string = strings.find { |string| string.include? 'a' }
  raise ArgumentError, 'At least one string must have an "a"' unless string

  string.upcase
end
  • Hata tipini ArgumentError olarak verdik, "geçirilen argümanda bir hata var" niyetiyle

  • ArgumentError Ruby'nin yerleşik hata (istisna) tiplerinden biri

  • Önceki örneğe göre daha iyi ama en uygunu özel bir hata tipi oluşturmak (buna geleceğiz)


Nesnenin kaynağı denetimimizde ise çağıran tarafta yönetmeyi de tercih edebiliriz

def sanitize_strings(strings)
  return strings if strings.any? { |string| string.include? 'a' }

  raise 'At least one string must have an "a"'
end

def uppercase_first_on_match(strings)
  strings.find { |string| string.include? 'a' }.upcase
end

uppercase_first_on_match sanitize_strings(%w[apple orange banana melon]) #=> APPLE
uppercase_first_on_match sanitize_strings(%w[lemon melon]) # undefined method `upcase' for nil:NilClass (NoMethodError)

Bazen (kritik kod yollarında) defansif programlama yapmak adına hatayı her iki tarafta yönetebiliriz

def uppercase_first_on_match(strings)
  string = strings.find { |string| string.include? 'a' }
  return string.upcase if string

  raise 'Unexpected strings: expected to have a letter "a" in at least one string'
end

uppercase_first_on_match sanitize_strings(%w[apple orange banana melon])
uppercase_first_on_match (%w[lemon melon])

Hata yakala: rescue

def uppercase_first_on_match(strings)
  string = strings.find { |string| string.include? 'a' }
  raise ArgumentError, 'At least one string must have an "a"' unless string

  string.upcase
end

def main
  puts uppercase_first_on_match ARGV
rescue ArgumentError
  abort 'Error occured'
end

main

  • rescue'nun hangi girinti seviyesinde yazıldığına dikkat edin!

  • Bu aslında bir kısa yol


begin / end

def main
  begin
    puts uppercase_first_on_match ARGV
  rescue ArgumentError
    abort 'Error occured'
  end
end
  • begin / end bir ifade grubu oluşturuyor

  • Burada özel bir durum var, grup zaten metot gövdesine oturmuş durumda

  • Ruby bu özel durumda bir kısa yol sunuyor, özel bir begin/end bloğu yazmadan şöyle:

def main
  puts uppercase_first_on_match ARGV
rescue ArgumentError
  abort 'Error occured'
end

Fakat bazı senaryolarda kısa yolu kullanmadan açık halde yazmalısınız

def main
  begin
    puts uppercase_first_on_match ARGV
  rescue ArgumentError
    err = 'Error occured'
  end

  puts 'Operation done.'

  abort if err
end

  • begin / end verilen örneklerdeki gibi hata yönetiminde sık kullanılıyor

  • Pratikte karşılaşılan diğer bir senaryo grubun dönüş değerini kullanmak

  • Bu senaryoda yeni bir yardımcı metot yazmak yerine lojiği begin / end ile gruplamasına alıyoruz

  • Bu teknik çok kısa lojikler için yararlı; lojiği küçük bir metota almadan yerinde gerçekliyoruz

  • Yardımcı metoda alsaydık bir de o metoda bakmamız gerekeceğinden okunurluk zorlaşacaktı

  • Ama bu tekniği kötüye kullanmayın, lojik basit değilse mutlaka metoda bölün

def language_data_for(language)
  self.data[language] ||= begin
    (file = language_file_for(language)) ? YAML.load_file(file) : {}
  end
end

Hata yönetimine geri dönelim

def uppercase_first_on_match(strings)
  string = strings.find { |string| string.include? 'a' }
  raise ArgumentError, 'At least one string must have an "a"' unless string

  string.upcase
end

def main
  puts uppercase_first_on_match ARGV
rescue ArgumentError
  abort 'Error occured'
end

main ARGV
  • Bir sorun var, hata iletisi bu haliyle yararsız, hata hakkında ilave bilgi verilmemiş

  • Hata detayına nasıl erişeceğiz? Üretilen hata nesnesiyle

  • Hata iletisi hata nesnesi içinde bulunuyor

  • Hata nesnesine nasıl erişeceğiz?

  • Şöyle: rescue «HATA TİPİ» => «HATA NESNESİNİ TAŞIYACAK DEĞİŞKEN»


def uppercase_first_on_match(strings)
  string = strings.find { |string| string.include? 'a' }
  raise ArgumentError, 'At least one string must have an "a"' unless string

  string.upcase
end

def main
  puts uppercase_first_on_match ARGV
rescue ArgumentError => e
  abort "Error occured: #{e.message}"
end

main
  • Örnekte hata nesnesi için e gibi kısa bir ismi tercih ettik

  • Başka bir isim de olabilir ama bu isim çok yaygın ve deyimsel

  • Hata iletisi nerede? e nesnesinin message niteliğinde


Hata yönetiminde hata yapılabilir mi? Örnekteki hatayı görebiliyor musunuz?

def uppercase_first_on_match(strings)
  string = strings.find { |string| string.include? 'a' }
  raise ArgumentError, 'At least one string must have an "a"' unless string

  string.upcase
end

def main
  puts uppercase_first_on_match
rescue ArgumentError => e
  abort "Error occured: #{e.message}"
end

main

  • uppercase_first_on_match metoduna ARGV argümanını geçirmeyi unuttuk

  • Ne oldu? uppercase_first_on_match beklediği argümanı alamadığından kendisi de bir ArgumentError üretti

  • Hatalar birbirine karışıyor, bu ArgumentError farklı türde bir hata

  • Özel bir hata tipi oluşturma yerine standart hataları kullanmamızın bedeli

  • Özel bir hata tipi üretmemiz lazım, nasıl?


StringsError = Class.new StandardError

def uppercase_first_on_match(strings)
  string = strings.find { |string| string.include? 'a' }
  raise StringsError, 'At least one string must have an "a"' unless string

  string.upcase
end

def main
  puts uppercase_first_on_match ARGV
rescue StringsError => e
  abort "Error occured: #{e.message}"
rescue ArgumentError => e
  abort "Programming error: #{e.message}"
end

main

  • Aynı metot içinde birden fazla hata tipinin yönetilebileceğine dikkat edin!

  • Kendi hata tipimizi nasıl oluşturduk? Dinamik miras almayla

  • Ama statik miras almayla da olur

class StringsError < StandardError; end

Kalıtım yoluyla sadece yeni bir hata tipi ürettik, gövdesi olmayan bir sınıf?

  • Evet, bu çok olağan bir senaryo

  • Çoğu durumda fazlasına ihtiyacımız yok

  • Bize tek gereken hatanın tipi, diğer hatalardan ayrılması için

  • Deyimsel olarak bunu hemen hemen daima StandardError yerleşik hatasından miras alarak yapıyoruz


Hata tipi yeterli değilse, özel şeyler yapan bir hata tipi oluşturmak istiyorsanız? Boşlukları doldurun

class MyError < StandardError
  ...
end

veya

MyError = Class.new StandardError do
  ...
end
  • İlk form daha doğru, ikinci formu sadece basit senaryo için öneriyoruz

En temelde şunu yapmaya çalıştık

  • Hata yönetimini en tepe seviyede (main fonksiyonu) yönettik

  • Bunu yapmasaydık son kullanıcı muhtemelen ona anlamsız gelecek istisna iletileriyle karşılacaktı

  • Bu bir UX duyarlılığı, son kullanıcıyı istisna iletileriyle karşı karşıya bırakmayın


Ruby hata hiyerarşisi

StandardError yerleşik hatasından miras alıyoruz

  • Özel hata tiplerinde hemen hemen daima StandardError'den miras alıyoruz

  • Bu Ruby'nin yerleşik hata tiplerinden biri

  • Bunların bir kısmını zaten biliyorsunuz, ör. NameError, ArgumentError

  • Ruby'nin diğer hata tiplerini ve soy ağacını inceleyin: https://ruby-doc.org/core-3.0.1/Exception.html


İstisnaları sadece istisnai durumlarda kullanın

Deneyimsiz programcılarda görülen yanlış bir pratik: program lojiğini istisnalarla kurmak

def main
  file = ARGV.first
  content = File.read file
  ...
rescue Errno::ENOENT
  abort "File not found: #{file}"
end

main

Böyle yapmayın!


def main
  file = ARGV.first
  abort "File not found: #{file}" unless File.exist? file

  content = File.read file
  ...
end

main

Çok Biçimlilik

Çok Biçimlilik

[Circle.new(3, 5, 2), Square.new(9, 4, 1)].each do |shape|
  puts "#{shape} şeklinin alanı: #{shape.area}"
end

  • Bir isim → Çok biçim

  • Bir isim: area

  • Çok biçim: shape nesneleri area mesajına farklı şekilde cevap veriyor


Diğer bir çok biçimlilik örneği

var these = [19, "19"];
var those = [];
for (var i = 0; i < these.length; i++)
       those.push(these[i] + 1);
// those = [20, "191"]
  • JavaScript'te + operatörü çokbiçimli davranıyor

  • Dizideki güncel nesne eğer:

    • Tamsayı ise toplama yapıyor

    • Dizgi ise tamsayıyı dizgiye çevirip ekleme yapıyor


Çok Biçimlilik'te

  • Sadece davranış ismiyle ilgileniyoruz → area

  • Davranışı sergileyen nesnelere ortak bir isim veriyoruz → shape (şekil)

  • Nesnenin kimliğini ayrıntılı bilmemiz gerekmiyor → Circle, Square


shapes = [ Square.new(3), Rectangular.new(width: 1, height: 3, position: Point.new(3, 5)), Circle.new(2) ]

shapes.each do |shape|
  case shape
  when Square      then area = shape.side * shape.side
  when Rectangular then area = shape.width * shape.height
  when Circle      then area = shape.radius * shape.radius * Math::PI
  else                  raise("Tanınmayan şekil: #{shape}")
  end

  puts area
end

 shapes = [ Square.new(3), Rectangular.new(width: 1, height: 3, position: Point.new(3, 5)), Circle.new(2) ]

 shapes.each do |shape|
   puts shape.area
 end

Dinamik Dağıtım

Bir kargo merkezi ve kargo paketleri

  • Kargo paketleri nasıl dağıtılır?

  • Paketin hangi yolla (hava, kara, deniz) gönderileceğine karar vermek lazım

  • Alıcı adrese göre yöntem değişir

Cargo Dispatch

  • Alıcı adrese bak

  • Ulaşım yöntemini tayin et

  • Tek isim → send, çok biçimli davranış → farklı ulaşım yöntemleri


  • Şekil nesneleri arasında dolaşıyor ve area metodunu çalıştırıyoruz

  • area'nın hangi gerçeklemesi çalıştırılacak?

  • İlgili nesnenin sınıfı neyse o sınıfta tanımlı metod çalıştırılacak

  • Ama ilgili nesnenin sınıfını bilmemiz gerekmiyor

  • Uygun area gerçeklemesine çalışma zamanında karar veriliyor


  • Kargo paketi nasıl gönderilecek? send metodu

  • Şekil nesnesinin alanı nasıl hesaplanacak? area metodu

  • Karar anında bir tür dağıtım (dispatch) yapılıyor

  • Bu karar çalışma zamanında veriliyor

  • Dinamik dağıtım → dynamic dispatch


Arayüz

  • Vana/vida → açma/kapama

  • Arabalar → direksiyon, vites sistemi

  • Telefon → kulaklık/mikrofon, tuş takımı

  • USB standartı → Aygıtlar, USB soketleri


  • İç yapısı (gerçeklemesi) farklı olabilen nesnelerle haberleşmemizi sağlıyor

  • Bir tür sözleşme

    • Vanayı saat yönünün tersine çevirerek akışı kes

    • Aracın kalkışında debriyaj, 1'nci vites, gaz

    • Telefonda arama yapmak için 4 + 7 haneli numarayı tuşla/çevir

    • USB aygıtı kullanmak için aygıtı USB soketine tak

  • Nesneleri arayüzler üzerinden kullanıyoruz

  • Arayüzler aynı, gerçeklemedeki farklı olabilir


Arayüz ve Çok Biçimlilik

Arayüz ve Çok biçimlilik birbiriyle yakından ilintili

  • Çok biçimlilik → bir isim, çok biçim

  • İsim → arayüzde tanımlanan bir sözleşme maddesi

  • Biçim → ilgili arayüzün (o sınıf için) gerçeklemesi


Nesne İlişkileri


Miras Alma

Miras Alma


UML Diyagramları

Sınıf ilişkilerini UML diyagramlarıyla gösteriyoruz

  • Bir modelleme aracı

  • Unified Modelling Language

  • Tasarım sırasında yararlı


  • "Miras Alma" ilişkisi ebeveyn sınıfa doğru içi boş bir ok

Miras Alma


is-a

"Miras Alma" bir is-a ilişkisi kurar

  • Car is a Vehicle: Araba bir Araçtır

Bu ilişkinin doğal olması lazım

  • Aksi halde hatalı bir soyutlama yapmış olursunuz

  • Hatalı soyutlamanın bedeli?

  • Karmaşık ve kırılgan kod

  • Yapboz'da yerine oturmayan parçalar


Miras Alma (Hatalı Soyutlama)

Hatalı Miras Alma


class Vehicle
  # Araç'la ilgili tüm ortak akıl burada...

  def start_engine
    puts "başla"
  end
  def stop_engine
    puts "dur"
  end
end

class Car < Vehicle
  # Hafta sonu turu
  def sunday_drive
    start_engine
    # Gez dolaş ve dön...
    stop_engine
  end
end

def main
  car = Car.new
  car.sunday_drive
end

main

Car is a Vehicle: Araba bir Araçtır

  • Yukarıdaki cümle kulağa doğru geliyor; fakat...

  • İsimlendirmelere aldanmayın, modele bakın

  • "Araç" nasıl modellenmiş? Vehicle sınıfıyla

  • Vehicle? "Motoru olan bir nesne" üretir


Her aracın (vasıtanın) bir motoru var mıdır?

  • Bisiklet?

  • Planör?

  • Kayık?


Bize başka bir soyutlama lazım

  • Her aracın motoru olmayabilir

  • "Araba motoru olan bir araçtır"


Kompozisyon

  • "Kompozisyon" ilişkisi kompoze edilen sınıfa doğru içi dolu bir ok

Doğru Kompozisyon


has-a

"Kompozisyon" bir has-a ilişkisi kurar

  • Car is a Vehicle, that has an Engine

  • "Araba motora sahip bir Araçtır"

  • Bakın bu farklı bir model


class Engine
  # Motor'la ilgili tüm ortak akıl burada...
  def start
    puts "başla"
  end
  def stop
    puts "dur"
  end
end

class Car
  def initialize
    @engine = Engine.new
  end

  # Hafta sonu turu
  def sunday_drive
    @engine.start
    # Gez dolaş ve dön...
    @engine.stop
  end
end

def main
  car = Car.new
  car.sunday_drive
end

main

Kompozisyon veya Miras Alma?

Mümkün olan her yerde "Kompozisyon"u, "Miras Alma"ya tercih edin.

  • Bir modellemenin eşiğindesiniz

  • Hemen is-a ve has-a ilişkilerini tayin edin

  • Model'e alternatif açılardan bakın

  • is-a ilişkisi çok doğal olmalı

  • Doğal değilse has-a ilişkisini arayın

  • Doğallık şüphesi varsa has-a


Bazen "Kompozisyon" ve "Miras Alma" birlikte kullanılabilir

Kompozisyon ve Miras Alma


class Engine
  # Motor'la ilgili tüm ortak akıl burada...
  def start
    puts "başla"
  end
  def stop
    puts "dur"
  end
end

class GasolineEngine < Engine
  # Benzinli Motor
end

class DieselEngine < Engine
  # Dizel Motor
end

class Car
  def initialize
    @engine = GasolineEngine.new
  end

  # Hafta sonu turu
  def sunday_drive
    puts "Motor tipi: #{@engine.class}"
    @engine.start
    # Gez dolaş ve dön...
    @engine.stop
  end

  # Dizel'e çevir
  def switch_to_diesel
    @engine = DieselEngine.new
  end
end

def main
  car = Car.new
  car.sunday_drive
  car.switch_to_diesel
  car.sunday_drive
end

main

Delegasyon

"Kompozisyon"lu modelde motorun çalışma aklı nerede?

  • Motorda ama bir kısmı Araba'ya sızmış gibi: car.start

  • Araba motorun çalışmasına bu kadar yakın olmamalı

  • Araba modelinin müşterisi motorun çalışmasıyla ilgilenmez

Ne yapmalı?


Araba içinde motorun çalışmasıyla ilgili tüm işler motora devredilmeli

  • Car sınıfında motorla ilgili tüm işler Engine sınıfına bırakılmalı

  • Engine sınıfı "motor işleri"ni temsil ediyor

  • "Motor"un delegasyonu


class Engine
  # Motor'la ilgili tüm ortak akıl burada...
  def start
    puts "başla"
  end
  def stop
    puts "dur"
  end
end

class GasolineEngine < Engine
  # Benzinli Motor
end

class DieselEngine < Engine
  # Dizel Motor
end

class Car
  def initialize
    @engine = GasolineEngine.new
  end
  def sunday_drive
    puts "Motor tipi: #{@engine.class}"
    start
    # Cruise out into the country and return...
    stop
  end
  def switch_to_diesel
    @engine = DieselEngine.new
  end
  def start
    @engine.start
  end
  def stop
    @engine.stop
  end
end

def main
  car = Car.new
  car.sunday_drive
  car.switch_to_diesel
  car.sunday_drive
end

main

Bu gerçeklemede bir sorun var

  • Kod DRY değil

  • start ve stop metodları bu eylemleri motor nesnesine gönderiyor

  • Her delegasyonda böyle sarmalayıcı metodlar yazmak zorunda mıyız?

  • "Şu isimdeki metodları şu nesneye gönder" gibi bir kolaylık?


Delegasyon - Forwardable modülü

require 'forwardable'

# Motor
class Engine
  # Motor'la ilgili tüm ortak akıl burada...
  def start
    puts 'başla'
  end

  def stop
    puts 'dur'
  end
end

class GasolineEngine < Engine
  # Benzinli Motor
end

class DieselEngine < Engine
  # Dizel Motor
end

# Araba
class Car
  extend Forwardable

  def_delegators :@engine, :start, :stop

  def initialize
    @engine = GasolineEngine.new
  end

  def sunday_drive
    puts "Motor tipi: #{@engine.class}"
    start
    # Cruise out into the country and return...
    stop
  end

  def switch_to_diesel
    @engine = DieselEngine.new
  end
end

def main
  car = Car.new
  car.sunday_drive
  car.switch_to_diesel
  car.sunday_drive
end

main

Örnek: Üretilen nesneleri sayı

class Rectangular
  attr_reader :width, :height

  def initialize(width, height)
    @width  = width
    @height = height
  end
end

def main
  rectangulars = []

  rectangulars << Rectangular.new 1, 2
  rectangulars << Rectangular.new 3, 5
  rectangulars << Rectangular.new 4, 9

  puts rectangulars.size
end

main

Nasıl ve nerede üretildiğine bakılmaksızın inşa edilen dikdörtgenlerin sayısını nasıl takip ederiz?

  • Nasıl ve nerede üretildiğine bakılmaksızın...

  • İlk örnekte nesne sayını main metodunda bir dizi üzerinden öğreniyoruz

  • Çeşmenin başı? initialize metodu?


class Rectangular
  attr_reader :width, :height

  def initialize(width, height)
    @width  = width
    @height = height

    @count += 1 # ??? 
  end
end

Bu yaklaşımın sorunları?

  • @count bir nesne niteliği, nesneye özgü

  • Halbuki bize tüm dikdörtgen nesneleri için kullanılacak bir sayaç lazım, bir nesneye özgü olmamalı

Ayrıca

  • @count niteliği nasıl ilklenecek?

  • Her "construction"da eski değerini nasıl koruyacak?

  • Nesne düzeyinde değil sınıf düzeyinde anlamlı bir sayaç kullansak?


Sınıf değişkenleri

class Rectangular
  @@count = 0

  attr_reader :width, :height

  def initialize(width, height)
    @width  = width
    @height = height

    @@count += 1
  end
end

def main
  rectangulars = []

  rectangulars << Rectangular.new 1, 2
  rectangulars << Rectangular.new 3, 5
  rectangulars << Rectangular.new 4, 9

  puts Rectangular.@@count # ???
end

main

Sınıf değişkeni

  • @@ ile başlıyor

  • Sınıf içinde (nesne metotları dahil) her yerden erişilebiliyor

  • Ama dışarıdan erişilemiyor (nesne nitelikleri gibi)

  • Özel bir erişici yazmamız lazım


class Rectangular
  @@count = 0

  attr_reader :width, :height

  def initialize(width, height)
    @width  = width
    @height = height

    @@count += 1
  end

  def self.count
    @@count
  end
end

def main
  rectangulars = []

  rectangulars << Rectangular.new 1, 2
  rectangulars << Rectangular.new 3, 5
  rectangulars << Rectangular.new 4, 9

  puts Rectangular.count # ???
end

main

  def self.count
    @@count
  end
  • @@count için okuyucu

  • Benzer şekilde yazıcı da yazılabilir

  • Sınıf değişkenleri için bunları yazmanın daha kısa bir yolu yok

  • Neden? Çok sık kullanılmıyor mu?


class Square < Rectangular
  attr_reader :length

  def initialize(length)
    @length = length

    super(length, length)
  end
end

def main
  rectangulars = []

  rectangulars << Rectangular.new 1, 2
  rectangulars << Rectangular.new 3, 5
  rectangulars << Rectangular.new 4, 9

  puts Rectangular.count

  squares = []
  squares << Square.new 1
  squares << Square.new 2

  puts Square.count
  puts Rectangular.count
end

main

  • Miras almada sorun yok, sınıf değişkeni çocuk sınıflara başarıyla iletiliyor

  • Fakat önemli bir sorun var

  • @@count tüm bir soyağacındaki doğumları takip ediyor; ata ve çocuk sınıflar


"Class variables considered harmful"

  • Sınıf değişkenleri kapsamı tanımlandığı sınıf ve mirasçıları olan global bir değişken gibi davranıyor

  • Global değişkenleri sevmiyoruz, neden?

  • Sadece tanımlandığı sınıfa özgü bir nitelik lazım

  • Bu nedenle sınıf değişkenleri sık kullanılan bir şey değil ve olmamalı


"Nesneye özgü"lük

  • Nesne nitelikleri sadece bir nesneye özgüdür; nesneler arasında paylaşılmaz

  • Sınıfa özgü bir nitelik nasıl tanımlanır?

  • Sınıf da eğer bir nesne ise bu yapılabilir?


Ruby'de sınıflar da birer nesnedir

  • Kulağa tuhaf geliyor

  • "Nesneye özgü"lükten yararlanmamız için anlamlı olacak bir cümle


Ruby'de "nesneye özgü" bir nitelik daima @ ön ekiyle belirtilir

  • Sınıf nesnesine özgü bir nitelik de @ ile başlamalı

  • Fakat nesne metotları içinde kullanılan @ nitelikleri nesneye özgü

Bu durumda

  • Sınıf nesnesi niteliği nerede ilklenebilir

  • İpucu: @ niteliklerini ilk seviye sınıf gövdesinde kullanmayı denediniz mi?

  • Sınıf nesnesi niteliklerine nasıl erişilir?


self'in öyküsü

class SomeClass
  p self

  def instance_method
    p self
  end
end

def main
  some = SomeClass.new
  some.instance_method
end

main

class BirSınıf
  BU BÖLGE SINIF NESNESİNİN

  def herhangi_bir_nesne_metodu
    BU BÖLGE SINIFTAN İNŞA EDİLEN NESNENİN
  end
end

Sınıf nesnesi nitelikleri

class Rectangular
  @count = 0

  attr_reader :width, :height

  def initialize(width, height)
    @width  = width
    @height = height

    Rectangular.count += 1 # @count += 1 olmaz değil mi?
  end

  def self.count
    @count
  end

  def self.count=(value)
    @count = value
  end
end

def main
  rectangulars = []

  rectangulars << Rectangular.new 1, 2
  rectangulars << Rectangular.new 3, 5
  rectangulars << Rectangular.new 4, 9

  puts Rectangular.count # ???
end

main

Deneme yapalım

class Square < Rectangular
  attr_reader :length

  def initialize(length)
    @length = length

    super(length, length)
  end
end

def main
  rectangulars = []

  rectangulars << Rectangular.new 1, 2
  rectangulars << Rectangular.new 3, 5
  rectangulars << Rectangular.new 4, 9

  puts Rectangular.count

  squares = []
  squares << Square.new 1
  squares << Square.new 2

  puts Square.count
  puts Rectangular.count
end

main

Sınıf nesnesi niteliği için erişiciler (getter/setter)

  @count = 0

  def self.count
    @count
  end

  def self.count=(value)
    @count = value
  end

Bunun bir kısayolu yok mu?

  • Nesne niteliklerinde attr_* metotlarını kullanıyorduk, bunun gibi bir şey

  • attr_* metotlarını sınıf nesnesi nitelikleri için kullanamayız değil mi?

  • Bu metotlar nesneye özgü nitelik erişicileri tanımlıyor; self'i takip edin


  • Sınıf nesnesi nerede? @ ile başlayan sınıf nesnesi nitelikleri için sınıfın birinci seviye gövdesinde

  • Fakat bu durum sınıf düzeyinde çalışan attr_* metotları için geçerli değil

  • Sınıf düzeyinde çalışan bu metotlar nesne metotu yazıyor, sınıf nesnesi metotu değil

  • Sınıf nesnesi üzerinde etki gösterecek bir yer olmalı; içte bir yer


Hatırladınız mı?

class Rectangular
  @count = 0

  class << self
    def count
      @count
    end

    def count=(value)
      @count = value
    end
  end
 end

class Rectangular
  @count = 0

  class << self
    attr_accessor :count
  end
 end

  • Sınıfı "adeta" yarıp en iç kısmına erişiyoruz

  • Artık sınıf nesnesi içindeyiz

  • Buna "eigenclass" diyoruz, öz sınıf

  • attr_* metotlarının yazdığı nesne metotları artık sınıf nesnesine ait, nesneye değil


Sınıf düzeyinde anlamlı nitelikleri sınıf değişkenleri yerine sınıf nesnesi nitelikleriyle temsil edin


  • Sınıf sabitleri

  • Sınıf değişkenleri

  • Sınıf nesnesi nitelikleri

  • Nesne nitelikleri


Şimdi toparlayalım, bir deneme yapacağız

  • Tüm bu ögelerin kalıtımdan nasıl etkilendiğine bakacağız

  • Yardımcı bir metot tanımlayarak başlayalım


# Yardımcı
def dump(p)
  puts "#{p}: #{eval(p, TOPLEVEL_BINDING) || 'boş'}"
end

puts "A sınıfı tanımlanıyor"
class A
  attr :instance_variable

  CONSTANT = "#{self}_CONSTANT"
  @@class_variable = "#{self}_class_variable"
  @class_instance_variable = "#{self}_class_instance_variable"

  def initialize
    @instance_variable = "#{self}_instance_variable"
  end

  class << self
    attr :class_instance_variable
    def class_variable
      @@class_variable
    end
  end
end

puts "B < A sınıfı tanımlanıyor (değişiklik yapmadan)"
class B < A
end

puts "C < A sınıfı tanımlanıyor (değişiklik yaparak)"
class C < A
  CONSTANT = "#{self}_CONSTANT"
  @@class_variable = "#{self}_class_variable"
  @class_instance_variable = "#{self}_class_instance_variable"
end

puts "A sınıfından a nesnesi oluşturuluyor"
a = A.new
puts "B sınıfından b nesnesi oluşturuluyor"
b = B.new
puts "C sınıfından c nesnesi oluşturuluyor"
c = C.new

puts
dump 'A'
dump 'A::CONSTANT'
dump 'A.class_variable'
dump 'A.class_instance_variable'
dump 'a.instance_variable'
puts
dump 'B'
dump 'B::CONSTANT'
dump 'B.class_variable'
dump 'B.class_instance_variable'
dump 'b.instance_variable'
puts
dump 'C'
dump 'C::CONSTANT'
dump 'C.class_variable'
dump 'C.class_instance_variable'
dump 'c.instance_variable'