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;
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;
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;
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.
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).
Bununla ilgili kaynak kodları şu linkten indirebilirsiniz.