5 Kasım 2018 Pazartesi

Generics (soysallar) ve TCustomAttribute kullanımı ve RTTI

Genericleri anlatmaya tersten başladım aslında, yani listelerden. Oysa öncelikle soysal tip bildirimi kullanılan sınıflardan bahsetmem gerekiyordu. Belki de bu sistemi yeni kullanmaya başladığım içindir.

Ancak şu unutulmamalıdır ki generic kullanımı normalde fazla gerekmez, ta ki RTTI kullanıncaya kadar. Ve RTTI kullanımı da sizi modern tasarımlara Design Pattern'lere götürecektir, Design Pattern kullanmaya başladıktan sonra ise tüm tasarımlarınız generic içermeye başlayacaktır. Yani aslında şunu söyleyebilirim, eğer Design Pattern kullanmaya niyetiniz yoksa bu sayfayı okumanıza da hiç gerek yok bence. Direkt olarak atlamanızı tavsiye ederim.

Benim Delphi yazılımcılarında tanık olduğum en ciddi sorun, moderniteye sırtlarını dönmüş olmaları. Çoğunlukla Delphi 7 onlara yetiyor, web yazılımı yapacakları zaman DotNet ya da PHP ile işlerini halletmeye çalışıyorlar, bitiyor gidiyor. İşin enteresanı, bu dillere geçtikleri zaman modern yazılımı, Design Pattern'leri öğrenmeye çalışıyorlar. Bunları Delphi'de uygulamak akıllarına gelmiyor. Varsa yoksa komponentler, hem de VCL. Neyse bu ayrı bir yazı konusu.

Bu yazıda bir procedure'de generic nasıl kullanılır, onu inceleyeceğiz.
Generic'i tanımlarsak : Bir Class'a, bir procedure ya da function'a <T : Base type> şeklinde verdiğimiz bir tip bildirimidir. Bu tip bildirimini daha sonra Class'ın (ya da neye verdiysek onun) içinde kullanabiliriz. Bu run time esnasında sanki aralara a.inc dosyasını include etmek gibidir. Yani buradaki T tipinin geçtiği her yere bu tip kopyalanır. Burada istersek Base type da kullanabiliriz. Bunun anlamı, verdiğimiz tip bildirimi bu base type ya da bundan türemiş bir type olması gerektiğidir. Bu sistemin en güzel yanı, aralarında bir uyumsuzluk olduğunda hemen derleme hatası vermesidir. Görüldüğü gibi bize ciddi bir type safety (tip güvenliği) sağlamaktadır.

Bunun için öncelikle bir DB tablosunun entity'sini oluşturacağız,
Bunun için MySQL üzerinde gelen örnek veritabanı olan SAKILA'yı kullanacağım.

  TActor = class(TBaseModel)
  private
    Factor_id: Integer;
    Ffirst_name: string;
    Flast_name: string;
    Flast_update: TDateTime;
    Fis_active: Boolean;
  published
    property ActorId: Integer read Factor_id write Factor_id;
    property FirstName: string read Ffirst_name write Ffirst_name;
    property LastName: string read Flast_name write Flast_name;
    property IsActive: Boolean read Fis_active write Fis_active;
    property LastUpdate: TDateTime read Flast_update write Flast_update;
  end;


Bu tabloya fazladan IsActive kolonunu, tablo yaratmak için Sql komutunu oluştururken  bazı  kısımlara da girmesi için ekledim.

Sql komutunu yazdıracağımız entity bu ancak bu sınıfı sql komutuna çevirebilmek için bu bilgilerden fazlasına ihtiyacımız var. Tablonun adı, tablo kolonlarının adı, uzunluğu, ondalık kısım gibi bilgileri de bilmemiz gerekiyor. İşte bu noktada imdadımıza TCustomAttribute yetişiyor.
Custom attribute'ler bir class, class'ın bir değişkeni (field), bir property'si veya metodu için tanımlanabilirler. Custom attribute, bu class'ın şu özelliği de var demek anlamına gelir. Daha sonra bu özellikleri alarak onlar üzerinde işlemler yapabiliriz.  Kullanımı şu şekildedir;

  [Entity('actor')]                // EntityAttribute
  TActor = class(TBaseModel)
    ...
    [Column('first_name', [], 45)]

    property FirstName: string read Ffirst_name write Ffirst_name;
    ...
  end;

Custom Attribute için ayrı bir unit oluşturup içine Attribute tanımlarımızı yapıyoruz. EntityAttribute tablonun ismini Entity class'ına iliştirmek ve daha sonra almak için kullanılır. ColumnAttribute ise her bir kolonun özelliklerini o kolonlara iliştirmek için kullanılır.

  EntityAttribute = class(TCustomAttribute)
  private
    FTableName : string;
    FSchemaName : string;
  public
    constructor Create(const ATableName: string; const ASchemaName: string = '');

    property TableName: string read FTableName;
    property Schema: string read FSchemaName;
  end;

  TColProp = (
    cpPrimaryKey,
    cpRequired,
    cpUnique,
    cpNotNull,
    cpAutoGenerated
  );
  TColProps = set of TColProp;
  ColumnAttribute = class(TCustomAttribute)
  private
    FColumnName : string;
    FLength: Integer;
    FPrecision: Integer;
    FScale: Integer;
    FColProps : TColProps;
    function GetIsPrimaryKey : Boolean;
    function GetIsAutoGenerated : Boolean;
  public
    constructor Create(const AColumnName : string) overload;
    constructor Create(const AColumnName : string; AColProps : TColProps;
                       ALength : Integer=0;
                       APrecision : Integer=0;
                       AScale : Integer=0); overload;
    property ColumnName: string read FColumnName write FColumnName;
    property Length: Integer read FLength write FLength;
    property Precision: Integer read FPrecision write FPrecision;
    property Scale: Integer read FScale write FScale;
    property ColProps: TColProps read FColProps write FColProps;
    property IsPrimaryKey: Boolean read GetIsPrimaryKey;
    property IsAutoGenerated: Boolean read GetIsAutoGenerated;
  end;

Ve bunların metodlarını yazıyoruz (sadece atamalar var).

Bunun arkasından Entity'lerimizi Attribute'lerle donatıyoruz.

  [Entity('actor')]                //] EntityAttribute
  TActor = class(TBaseModel)
  private
    Factor_id: Integer;
    Ffirst_name: string;
    Flast_name: string;
    Flast_update: TDateTime;
    Fis_active: Boolean;
  published
    [Column('actor_id', [cpPrimaryKey, cpRequired, cpUnique, cpNotNull])]
    property ActorId: Integer read Factor_id write Factor_id;
    [Column('first_name', [], 45)]
    property FirstName: string read Ffirst_name write Ffirst_name;
    [Column('last_name', [], 45)]
    property LastName: string read Flast_name write Flast_name;
    [Column('is_active')]
    property IsActive: Boolean read Fis_active write Fis_active;
    [Column('last_update')]
    property LastUpdate: TDateTime read Flast_update write Flast_update;
  end;

Bu biraz ORM tanımlamalarına benziyor değil mi? Zaten amacımız bu Entity'yi kullanarak Table Create SQL cümleciği oluşturmak.

Şimdi sıra bu Sql'i oluşturacak sınıfı yazmaya geldi.
  TSqlCreateBuilder<T : TBaseModel> = class
  private
    function GetColumnDefinition(const AColName: string; AColProps: TColProps;
      AColType: TTypeKind; ALen, APrec, AScl: Integer; ATypeInfo : Pointer;
      AAutoGen : boolean): string;
    function BuildSQLDataType(AColType : TTypeKind; ALen, APrec, AScl : integer; ATypeInfo : Pointer) : string;
  public
    function Execute : string;
  end;

Burada gördüğümüz gibi T yi tip belirteci olarak gösterdik. Daha sonra yazdığımız TBaseModel sözcüğü T için göndereceğimiz tipin en azından TBaseModel olması ya da ondan türetilmiş bir sınıf olması gerektiğini belirtir. Bu yanlış class geçmeyi engellemek için iyi bir yöntemdir. Programdan daha sonra geçtiğimiz entity tipi bu class içinde doğrudan o tanımlıymış gibi işlem görecektir.

Yazacağımız yordamda ilk önce EntityAttribute'den tablo adını elde ediyoruz. 
1-RttiContext yarat,
  FRttiContext := TRttiContext.Create;
2-Bundan GetType ile RttiType bilgisini al,
    LRttiType := FRttiContext.GetType(TypeInfo(T));
3-Bu entity'ye iliştirilmiş tüm Attribute'leri tarayıp EntityAttribute'yi bulmak için bir for döngüsü oluştur ve EntityAttribute'yi bulunda tableName'i al, döngüden çık.

    tblName := '';
    for attr in LRttiType.GetAttributes do
      if attr is EntityAttribute then
      begin
        tblName := (attr as EntityAttribute).FullName;
        Break;
      end;

4-Tablonun kolon bilgilerini almak için Entity'nin property'leri üzerinde bir döngü oluşturuyoruz.

    for prop in LRttiType.GetProperties do
      if prop.Visibility=mvPublished  then    //!!!
      begin
        aautogen := False;
        for attr in prop.GetAttributes do
          if ((attr as ColumnAttribute).IsAutoGenerated) then
          begin
            aautogen := True;
            Break;
          end;

        fldName := prop.Name;
        alength := 0;
        aprecision := 0;
        ascale := 0;
        colProps := [];

        for attr in prop.GetAttributes do
          if (attr is ColumnAttribute) then
          begin
            fldName := (attr as ColumnAttribute).ColumnName;
            alength := (attr as ColumnAttribute).Length;
            aprecision := (attr as ColumnAttribute).Precision;
            ascale := (attr as ColumnAttribute).Scale;
            colProps := (attr as ColumnAttribute).ColProps;
            Break;
          end;

        s := GetColumnDefinition(fldName, colProps, prop.PropertyType.TypeKind, alength, aprecision, ascale, prop.PropertyType.Handle, aautogen);
        if Result='' then
          Result := Result + s + #13#10
        else
          Result := Result + ','+s + #13#10;
      end;

Görüldüğü gibi 2.satırda sadece Published yapılmış property'leri almış oluyoruz. 
Tüm kolon özelliklerini alıyoruz ve GetColumnDefinition adlı metoda gönderip gelen sonucu Sql komutuna ekliyoruz.

5-GetColumnDefinition'ı yazıyoruz;

function TSqlCreateBuilder<T>.GetColumnDefinition(const AColName: string;
  AColProps: TColProps; AColType: TTypeKind; ALen, APrec, AScl: Integer;
  ATypeInfo: Pointer; AAutoGen: boolean): string;
begin
  Result := Format('%0:s %1:s %2:s %3:s', [
    AColName,
    BuildSQLDataType(AColType, ALen, APrec, AScl, ATypeInfo),
    IfThen(cpNotNull in AColProps, 'NOT NULL', 'NULL'),
    IfThen(cpPrimaryKey in AColProps, IfThen(AAutoGen, 'IDENTITY(1,1) ')+'PRIMARY KEY')]);
end;

6-Burada adı geçen BuildSQLDataType adlı metodu yazıyoruz. 

function TSqlCreateBuilder<T>.BuildSQLDataType(AColType: TTypeKind; ALen, APrec,
  AScl: integer; ATypeInfo: Pointer): string;
begin
  Result := 'INTEGER';
  case AColType of
    tkUnknown: ;
    tkInteger, tkSet:
      if APrec > 0 then
        Result := Format('NUMERIC(%0:d, %1:d)', [APrec, AScl]);
    tkEnumeration:
      if ATypeInfo = System.TypeInfo(Boolean) then
        Result := 'BIT';
    tkInt64:
      if APrec > 0 then
        Result := Format('NUMERIC(%0:d, %1:d)', [APrec, AScl])
      else
        Result := 'BIGINT';
    tkChar: Result := Format('CHAR(%d)', [ALen]);
    tkFloat:
      if ATypeInfo = System.TypeInfo(TDate) then
        Result := 'DATE'
      else
      if ATypeInfo = System.TypeInfo(TDateTime) then
       Result := 'DATETIME'
       //Result := 'TIMESTAMP'
      else
      if ATypeInfo = System.TypeInfo(TTime) then
        Result := 'TIME'
      else
        if APrec > 0 then
          Result := Format('NUMERIC(%0:d, %1:d)', [APrec, AScl])
        else
          Result := 'FLOAT';
    tkString, tkLString: Result := Format('VARCHAR(%d)', [ALen]);
    tkClass, tkArray, tkDynArray, tkVariant: Result := 'BLOB';
    tkMethod: ;
    tkWChar: Result := Format('NCHAR(%d)', [ALen]);
    tkWString, tkUString: Result := Format('NVARCHAR(%d)', [ALen]);
    tkRecord: ;
    tkInterface: ;
    tkClassRef: ;
    tkPointer: ;
    tkProcedure: ;
  end;
end;

Görüldüğü gibi burada verilen Entity class'ından kolon tipini bulup onun karşılığını oluşturuyoruz (buradaki tanımlamalar MSSQL içindir).
Programı çalıştırıp Build SQL tuşuna basıldığı zaman;




Bu da son oluyor zaten.

Bununla ilgili kaynak kodları şu linkten indirebilirsiniz.






18 Mart 2018 Pazar

Mobil uygulamalardan bir veritabanına niçin bağlanmamalısınız!

Craig Chapman'ın aynı başlıklı yazısını okuyunca bunu kendi sözcüklerimle de harmanlayarak, aktarmak ve yaymak gerektiğini düşündüm. Özellikle tüm masaüstü Delphi yazılımcıları, böylesi işlerine kolay geldiği için, program mimarisinde değişiklikler olmaması için bu yola gidiyorlar. Ancak bilin ki bu çıkmaz yol!

Elbette mobil uygulamadan veritabanına bağlantıyı tümüyle reddetmek mümkün değil. Hele ki bu veritabanı mobil cihazda tutulan lokal bir veritabanıysa...


  • Ancak herşeyden önce bu veritabanı sunucularının, mobil cihazlar için, client library desteğinin olup olmadığını sorgulamamız gerekiyor. Örneğin, bildiğime göre şu anda Arm işlemcide çalışan Android OS için bilinen SQL sunucular için client kütüphanesi yok. Diğer bilinen SQL sunucular için de aynı durum geçerli. Buna istisna olarak; Bazı veritabanı üreticileri client library de üretiyorlar ancak ben bunlarla çalışmadım. Bazı veritabanlarına da ODBC ya da JDBC kullanarak bağlanmak mümkün. Ancak şu unutulmamalı ki bu yöntem native bağlantı gibi hızlı olmayacaktır.
  • İkinci olarak veritabanı bağlantısının kalıcı olması mobil cihazlarda mümkün değildir. Yani programı açtığınızda bağlantı açılacak, kapanıncaya kadar bu bağlantı üzerinden işlemler yapılacaktır?.. İşte bu mobil cihazlarda pek mümkün görünmemektedir, çünkü arada bağlantı gittiğinde program bağlantı hatası verecektir. Ancak program yapısını SOAP ya da REST client tarzı bir mimariye çevirirseniz bu sistem kullanılabilir. İyi de bunun için mimariyi değiştirmek gerekiyor. O zaman bunun sahicisini yapıp REST sistemi kullanın!
Tüm bu sıkıntıya girmek yerine REST client-server sistemini kullanmak çok daha mantıklı olacaktır. Üstelik Delphi bu konuda inanılmaz destek vermektedir ve sistemlerini müthiş geliştirmiştir. Eski sistemlerinizi kısa sürede bu sisteme geçirebilirsiniz. Burada yaşayacağınız en büyük sorun FMX grid konusunda olacaktır (bu görüş benden). Buna rağmen bu zorlukları aşabilirsiniz. 


Masaüstü ve MS Windows hegemonyası bitti. Yeni bir devir başlıyor ve Delphi de bu dünyadaki yerini sağlamlaştırmaya, genişletmeye çalışıyor. Size de aynısını tavsiye ederim. (Bunlar da benden MÖ).

17 Mart 2018 Cumartesi

Generic (soysal) listeler ve kullanımı - 1

Delphi de diğer dillerdeki gibi Generic listeleri object pascal içine alarak dildeki önemli geliştirmelerden birini gerçekleştirmiş oldu.

Generic nedir: Bir listeye, liste elemanı olacak şeyin (değişken, nesne ve her neyse) tipini vererek, liste içinde yapılan tüm işlemlerin bu tipe göre yapılmasını sağlayabilen listelerdir.

Örnek verirsek, önce bir Console projesi yaratalım. Sonra aşağıdaki kodları yazalım

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Generics.Collections;
var
  tamsayiliste : TList<Integer>;
  i : integer;
begin
  tamsayiliste := TList<Integer>.Create;
  tamsayiliste.Add(181);
  tamsayiliste.Add(22);
  tamsayiliste.Add(11); 
  tamsayiliste.Add(17);
  for i in tamsayiliste do
    WriteLn(i);
  ReadLn;
end.

Bunu çalıştırdığımızda sırayla eklediğimiz rakamları yazacaktır. Bu listeye farklı tipte bir eleman eklemeye çalıştığımızda derleme esnasında hata verecektir. Bu da bize büyük esneklik sağlayan bir gereç veriyor. 
Bize ne sağlar: Her ayrı tip için ayrı bir liste sınıfı yazmamız gerekiyordu.
Önceden TList kullandığımızda elemenın tipi Pointer idi. Haliyle bunu şu şekilde yapıyorduk;

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Generics.Collections;
var
  lst : TList;
  i : integer;
begin
  lst := TList.Create;
  lst.Add(Pointer(181));
  lst.Add(Pointer(22));
  lst.Add(Pointer(11));
  lst.Add(Pointer(17));
  for i:=0 to lst.Count-1 do
    WriteLn(IntToStr(Integer(lst[i])));
  ReadLn;
end.
Eğer string'lerden oluşan bir liste istiyorsak TStringList'i kullanmak ya da TList'i değiştirmek gerekiyordu. Şimdi ise bunlara hiç gerek kalmadı. Yapmamız gereken sadece;

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Generics.Collections;
var
  lstr : TList<string>;
  s : string;
begin
  lstr := TList<string>.Create;
  lstr.Add('Ahmet');
  lstr.Add('okula');
  lstr.Add('gelecek,');
  lstr.Add('ama yarın...');
  for s in lstr do
    Write(s+' ');
  ReadLn;
end.

Bu listeye string'den başka bir tip eklemeye kalksak bize hata verecektir. Bu da bize, üst düzey dillerin çok önem verdiği ve önemli hata kaynaklarından biri olan, Type Safe (Tip Güvenliği)'ni sağlayacaktır. Yani bir tamsayı listesine başka bir tipte değişkeni eklememizi engelleyecektir.

Generic listeleri her tür class'lar için de kullanabiliriz.

program Project1;
{$APPTYPE CONSOLE}

{$R *.res}
uses
  System.SysUtils,
  System.Classes,
  System.Generics.Collections;

type
  TContact = class
    ContactId : Integer;
    ContactName : string;
    BirthPlace : string;
    BirthDay : TDate;
    Gender : string;
    function ToString : string; override;
    constructor Create(aContactId : Integer; aContactName : String; aBirthPlace : String; aBirthDay : TDate; aGender : String);
  end;

{ TContact }

constructor TContact.Create(aContactId: Integer; aContactName, aBirthPlace: String;
  aBirthDay: TDate; aGender: String);
begin
  ContactId := aContactId;
  ContactName := aContactName;
  BirthPlace := aBirthPlace;
  BirthDay := aBirthDay;
  Gender := aGender;
end;

function TContact.ToString: string;
begin
  result := IntToStr(ContactId)+'/'+ContactName+'/'+BirthPlace+'/'+DateToStr(BirthDay)+'/'+Gender;
end;

var
  MyContacts : TList<TContact>;
  fcon : TContact;
begin
  MyContacts := TList<TContact>.Create;
  MyContacts.Add(TContact.Create(1, 'Ahmet Acar', 'Rize', EncodeDate(1977, 1, 10), 'Erkek'));
  MyContacts.Add(TContact.Create(2, 'Çoşkun Pireli', 'Artvin', EncodeDate(1992, 5, 3), 'Erkek'));
  MyContacts.Add(TContact.Create(3, 'Mehmet Kesmez', 'Bolu', EncodeDate(1983, 11, 27), 'Erkek'));
  MyContacts.Add(TContact.Create(4, 'Cahide Küskün', 'Eskişehir', EncodeDate(1996, 6, 12), 'Kadın'));

  for fcon in MyContacts do
    WriteLn(fcon.ToString);
  ReadLn;
end.


Peki bu listeye farklı bir class eklemeye kalkarsak ne olur. Önce yukarıya şöyle bir sınıf tanımlayalım;
type
  TInvalidContact = class
    Id : Integer;
    Name : string;
  end;

daha sonra ise bunu listeye eklemeye çalışalım;
...
...
  MyContacts.Add(TInvalidContact.Create);
...
...
...


İşte Generic listin özelliği burada belli olacak. Bu satıra derleme hatası verecek ([dcc32 Error] Project1.dpr(nn): E2010 Incompatible types: 'TContact' and 'TInvalidContact'). Ve bu tür hatalar da bizi büyük hatalardan koruyacak!...
















1 Mart 2018 Perşembe

Firemonkey ListView veri tabanıyla birlikte kullanımı

FMX kütüphanesinin en yararlı elemanlarından birisi ListView'dur.

İlk amacımız şu görüntüdeki bir ana form elde etmek olacaktır.



1-Bunun için öncelikle yeni bir boş MultiDevice Application oluşturup onu diskte bir klasöre kaydedelim.
2-Form üzerine önce bir ClientDataSet ve bir ListView koyalım onu görüntüdeki gibi düzenleyelim.
3-ClientDataset1 üzerinde sağ-klik yapıp <Load from MyBase Table...> seçeneğini tıklayalım ve açılan Open Dialog'da biolife.xml'i seçelim (biolife.xml kurulu Delphi sürümünün Samples\Data klasörü altındadır.).
4-ListView1 üzerinde sağ klik yapıp Toggle Designmode'a tıklayalım.


Bu şekildeki gibi ListView1 görünümü değişecektir. Şimdi bunun üzerine bir Image alanı ve bir de 2. bir Text alanı ekleyelim.
5-ListView1'in ItemAppearance özelliğinin detaylarını açalım.


6-Burada ItemAppearance özelliğini DynamicAppearance olarak değiştirelim. Artık ListViewItem'a istediğimiz kadar metin ve resim ekleyebiliriz.
7-Bu aşamada Item'e yeni alanlar eklemek için üstteki Structure penceresinden Item'i seçelim. Bunun property editor'unde Add new'a tıklayalım ve TTextObjectAppearance 'ı seçelim. Burada bize 2 adet text alanı verecektir (Text1 ve Text2).
8-Yine aynı şekilde Add new'a tıklayıp TImageObjectAppearance'ı seçelim bu kez.
Text1'e Biolife tablosunun CommonName alanını, Text2'ye ise SpeciesName alanını ve Image3'e de aynı tablonun Graphic alanını yazdıracağız. Şimdi bu alanları ekranda düzenleyelim.

9-Yine üstten Item'ın sırayla Image3, Text1 ve Text2'sini seçip yerleştirelim.








Sonuçta şöyle bir görüntü çıkacak.

10-Şimdi ClientDataSet1 ve ListView1'i birbirine bağlayacağız. Bunun için Menu'den View/LiveBindings Designer'i seçelim.
11-Burada ClientDataSet1 ve ListView1 görülecektir. ClientDataSet1'in * alanını ListView1'in Synch alanı ile birleştirelim. (Yani önce * alanının üzerinde tıklayıp parmağımızı kaldırmadan bunu Synch'ya kadar uzatalım. İki alanın bir okla birbirine bağlandığını ve Designer'in otomatik olarak BindSourceDB1 adlı yeni bir nesneyi eklediğini göreceğiz. Şimdi Common_Name'i tutup Item.Text1'le, Species Name'i tutup Item.Text2 ile ve Graphic'i tutup Item.Image3'le birleştirelim.
12-ListView1 üzerinde sağ klik yapıp menüden ToggleDesignMode'ye basalım. Şimdi formumuz şu hale geldi...

13-BindSourceDB1 üzerinde sağ klik yapıp Add Navigator'ı seçelim ve düzgün bir şekilde yerleştirelim...
14-Programı çalıştıralım...





23 Ocak 2018 Salı

Delphi Sınıf Yapısı - Sanal (Virtual) metotlar

Sanal (virtual) Metotlar

Delphi'de bir metot virtual olarak belirtilmediğinde o metot sanal olmaz statik olur. Peki virtual'ın farkı nedir? Bunu şu şekilde anlatabiliriz;
Bir ata sınıfın tanımlanmış bir sanal metodunu onun çocuğu olan bir sınıfta ezdiğimiz (override) zaman, ata sınıfı göstererek bu metodu çalıştırdığımızda, çocuk sınıfın metodu çalışacaktır.

Örnekleyelim;
uses
  System.SysUtils,
  System.Contnrs;

type
  TMeyve = class
  strict private
    FMeyveCinsi : string;
    FMeyveMiktari : string;
  public
    property MeyveCinsi : string
                 read FMeyveCinsi write FMeyveCinsi;
    property MeyveMiktari: string
                 read FMeyveMiktari write FMeyveMiktari;
    procedure Topla; virtual;
  end;

  TAgaclar = class
  private
    FMeyveAgaclari : TObjectList;
  public
    constructor Create;
    destructor Destroy; override;
    procedure AgacEkle(agac : TMeyve);
    procedure MeyveleriTopla;
  end;

  TElma = class(TMeyve)
    constructor Create;
    procedure Topla; override;
  end;

  TArmut = class(TMeyve)
    constructor Create;
    procedure Topla;
  end;



Burada önce bir TMeyve ata sınıfı tanımlıyoruz, bundan türetilmiş TElma ve TArmut meyveleri var. Bunların ikisinin de Create yapılandırıcısı (constructor) ve Topla metodu var. Ancak TElma'nın Topla metodu öncekini ezmiş TArmut'un Topla metodu bilindik statik metot. Şimdi bunların kodlarını da şu şekilde yazarsak,

 { TMeyve }

procedure TMeyve.Topla;
begin
  Writeln(MeyveCinsi+' toplanmadı');
end;

{ TAgac }

procedure TAgaclar.AgacEkle(agac: TMeyve);
begin
  FMeyveAgaclari.Add(agac);
end;

constructor TAgaclar.Create;
begin
  FMeyveAgaclari := TObjectList.Create;
end;

destructor TAgaclar.Destroy;
begin
  FMeyveAgaclari.Destroy;
  inherited;
end;

procedure TAgaclar.MeyveleriTopla;
var
  meyve: TMeyve;
begin
  for meyve in FMeyveAgaclari do
  begin
    meyve.Topla;
  end;
end;

{ TElma }

constructor TElma.Create;
begin
  MeyveCinsi := 'Elma';
  MeyveMiktari := '15 adet';
end;

procedure TElma.Topla;
begin
  Writeln(MeyveMiktari+' '+MeyveCinsi+' toplandı...');
end;

{ TArmut }

constructor TArmut.Create;
begin
  MeyveCinsi := 'Armut';
  MeyveMiktari := '81 adet';
end;

procedure TArmut.Topla;
begin
  Writeln(MeyveMiktari+' '+MeyveCinsi+' toplandı...');
end;

var
  agaclar : TAgaclar;
begin
  try
    agaclar := TAgaclar.Create;
    try
      agaclar.AgacEkle(TElma.Create);
      agaclar.AgacEkle(TArmut.Create);
      agaclar.MeyveleriTopla;
      Readln;
    finally
      agaclar.Free;     
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Bu örnekte agaclar.MeyveleriTopla metodunda her bir meyvenin Topla metodunu çağırdığımızda elma'nın Topla metodu çalışır ama armutun Topla metodu çalışmaz. Onun yerine TMeyve sınıfının Topla metodu çalışır ve o da armutun toplanamaması olacaktır.

Burada meyve listesini tutmak için TObjectList (geleneksel list, generic değil) kullandım. TList te kullanabilirdim. Ancak TList pointer listesi için kullanıldığından dolayı listenin işi bitince free yapıldığında nesneleri free yapamaz. TObjectList ise free edildiğinde listedeki tüm nesneleri de yok edecektir.

Bu örnek konsol uygulaması olarak çalıştırılabilir. Bir console uygulaması oluşturup içindekiler yerine bunları kopyalarsanız test edebilirsiniz.


Not : Bu örnekler Delphi 10.1 Berlin Delphi 10.2 Tokio için denenmiştir.



Delphi Sınıf Yapısı - Overload metotlar

Sınıflarda overload metotlar

Delphi'de aynı isimde metotların farklı parametrelerle tanımlanması söz konusu olabilmektedir. Bunu metodun sonuna overload; direktifi ekleyerek sağlamaktayız;

Type
 TOrnek = class
   function ToplaminiAl(i1, i2 : integer): integer; overload;
   function ToplaminiAl(r1, r2 : double): double; overload
    ... 
 end;
 ...
function TOrnek.ToplaminiAl(i1, i2 : integer): integer;
begin
  result := i1 + i2;
end;
function TOrnek.ToplaminiAl(r1, r2 : double): double;
begin
  result := r1 + r2;
end;
B
Burada overload metodunun bir örneğini görüyoruz.

20 Ocak 2018 Cumartesi

Delphi Sınıf yapısı - Başlangıç

Class (sınıf) : Giriş
Temel olarak bir sınıf içindeki şu üyeleri taşıyan bir yapıdır;
-Alanlar (fields), ya da sınıf içinde tanımlanmış değişkenlerdir.
-Metotlar (methods), ya da sınıf içinde tanımlanmış prosedür veya fonksiyonlardır.
-Öznitelikler (properties), sınıf içinde tanımlanmış alanlara değer atamak veya bazı koşullara göre belli bir alandan ya da hesaplanmış bir değeri dışarı vermek için kullanılan yapıtaşıdırlar.
-Ve bunların dışında, daha sonraki yazılarda yer alacak farklı şeyler de söz konusudur.

type
  TMeyve = class(TObject)
  strict protected
    FAdi : string;
    FCekirdekMiktari : Integer;
    FAgirlik : Double;
    FRenk : string;
    FTat : string;
    FKoku : string;
  public
    property Adi : string read FAdi;
    property CekirdekMiktari : integer read FCekirdekMiktari;
    property Agirlik : Double read FAgirlik write FAgirlik;
    property Renk : string read FRenk write FRenk;
    property Tat : string read FTat write FTat;
    property Koku : string read FKoku write FKoku;

    procedure Ozellikler; virtual;
  end;

procedure TMeyve.Ozellikler;
begin
  WriteLn('Adı '+Adi);
  WriteLn('--------------------------------------------------');
  WriteLn('Çekirdek miktarı (ortalama adet) : '+IntToStr(CekirdekMiktari));
  WriteLn('Ağırlık (ortalama gram) : '+FloatToStr(Agirlik));
  WriteLn('Renk (en bilinen) : '+Renk);
  WriteLn('Tat : '+Tat);
  WriteLn('Koku : '+Koku); 
end;

Burada bir nesne tanımlaması ve bir metot görülüyor. Farkettiğiniz gibi normal prosedürden, değişkenlerden farkı yok. Ancak en büyük fark, bunlardan 2 adet Create etsek (creating an instance), yani 2 adet nesne oluştursak, her ikisindeki metot ve alanlar kendi nesnelerine ait özellikler-nitelemeler olacaktır ve ikisininkiler birbirinden farklı olacaktır. Yani birinin özelliğini değiştirince diğerinin özelliği değişmeyecektir.

Bir metot varsayılan olarak statik metotdur yani sanal (virtual) değildir.
Bu örnekte Ozellikler metodu kalıtım örneğidir ve sanaldır. TMeyve'den türeyen başka bir sınıf oluşturduğumuzda bu metodu ezebiliriz ya da bunu olduğu gibi kullanabiliriz. Örneğin;

  TAyva = class(TMeyve)
    constructor Create;
  end;

  TChiquitaMuz = class(TMeyve)
  strict private
    FMensei : string;
  public
    property Mensei: string read FMensei write FMensei;
    constructor Create;
    procedure Ozellikler; override;
  end;

{ TAyva }

constructor TAyva.Create;
begin
  inherited;
  FAdi := 'Ayva';  // Adi alanını değiştiremiyorum. Çünkü //setter'i yok...
  FCekirdekMiktari := 20;     // Aynı şekilde
  Agirlik := 235;
  Renk := 'Sarı';
  Tat := 'Ekşi, boğaza yapışan';
  Koku := 'Bilindik özellikli bir kokusu yok';
end;

{ TChiquitaMuz }

constructor TChiquitaMuz.Create;
begin
  FAdi := 'Çikita Muz';  // Adi alanını değiştiremiyorum. Çünkü setter'i yok...
  FCekirdekMiktari := 0;     // Aynı şekilde
  Agirlik := 110;
  Renk := 'Sarı';
  Tat := 'Tatlı';
  Koku := 'Az koku vardır';
  Mensei := 'Nikaragua';
end;

procedure TChiquitaMuz.Ozellikler;
begin
  inherited;
  Writeln('Menşei : '+Mensei);
end;



TAyva'nın TMeyve'dekinden başka bir özniteliği yoktur, bu nedenle sadece constructor metodu bulunur. Constructor metodunun bulunması sebebi ise özniteliklerini tanımlamaktır. TChiquitaMuz'un fazladan Mensei özniteliği vardır. Bu nedenle Ozellikler metodunu ezmek ve onun içinde de Mensei'ni yazdırmak gereklidir. inherited ile TMeyve'de olan özellikler yazılır, hemen altında ise TChiquitaMuz'un özelliği, Mensei yazılır.

Ancak bu örnek tipik bir sanal (virtual) metot örneği değil. Neden değil, çünkü bu işi biraz farklı şekilde de olsa sanal metot kullanmadan da yapabiliriz. Ancak tipik örneklerde, yani nesneyi kullanırken sınıf olarak sadece TMeyve'den türediğini biliyorsak ve hangi meyve olduğunu bilmiyorsak, işte o zaman kalıtımın saf haliyle tipik örneğini uyguluyoruz demektir. Sonraki yazıda en tipik haliyle kalıtımı inceleyeceğiz.





Delphi sınıf yapısı : Class - Görünürlük

Class - Görünürlük

Private, Protected, Public ve Published erişim kısıtlama bildirimleri;

Type
  TSinifAdi = Class(TObject)
    public
      {public alanlar-property (öznitelik)'ler}
      {public metotlar}
    protected
      {protected alanlar-öznitelikler}
      {protected metotlar}
    private
      {private alanlar-öznitelikler}
      {private metotlar}
  end;

-Public bildirimi ile bu alan ya da metoda her yerden erişilebilir. Yani başka bir Unit'deki başka kodun herhangi bir yerinde bu nesnenin yaratılmış örneğinden bu alan ya da metotlar çağrılabilir. Ya da bu metottan türetilmiş başka bir metot içinden direkt olarak (herhangi bir örneğini yaratmadan) kullanılabilir.
-Protected bildirimi bazı erişim kısıtlamaları koyar. Protected üyelere ya aynı Unit içinden, ya da aynı sınıf veya bundan türetilmiş bir sınıf içinden (başka bir Unit içinde olması farketmez) erişilebilir. Bunun dışında erişim yoktur.
-Private bildirimi yapılan bir üyeye yalnızca deklare edildiği Unit içinden erişilebilir.

Bunların yanısıra bir de strict bildirimi vardır. Bu tek başına kullanılmaz. strict private veya strict protected olarak kullanılır. strict private'de aynı sınıf içinden erişilebilir (ondan türetilmiş sınıflardan erişilemez), strict protected'de aynı sınıf ve türetilmiş sınıflar içinden erişilebilir (aynı unit içinde başka sınıflardan erişilemez).

Kaynaklar :
1-http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Private,_Protected,_Public,_and_Published_Declarations
2-http://castle-engine.io/modern_pascal_introduction.html#_visibility_specifiers