
Tasarım Desenleri : Fluent Design
Bir XML belgesini tıpkı aşağıdaki gibi çok sade ve “ANLAŞILIR” bir şekilde oluşturabilseydik daha iyi olmaz mıydı?
procedure TForm1.Button2Click(Sender: TObject);
var
XML: TXML2;
begin
try
XML := New
.Version(1.0)
.Encoding(TEncoding.UTF8)
.NameSpace('')
.Add('Kutuphane'
,New
.Add('Kitap', [ 'ID="1000"', 'Indirimli="Hayir"' ]
,New
.Add('Adi' , 'Mastering Delphi')
.Add('Fiyat' , 50)
.Add('Stok' , 40)
.Add('Yazarlar'
,New
.Add('Yazar', 'Marco CANTU')
)
)
.Add('Kitap', [ 'ID="1001"', 'Indirimli="Evet"' ]
,New
.Add('Adi' ,'PHP, MySQL ve Apache')
.Add('Fiyat' , 65)
.Add('Stok' , 30)
.Add('Yazarlar'
,New
.Add('Yazar', 'Julie C. MELONI')
)
)
.Add('Kitap', [ 'ID="1002"', 'Indirimli="Evet"' ]
,New
.Add('Adi' ,'Delphi Cookbook')
.Add('Fiyat' , 35)
.Add('Stok' , 300)
.Add('Yazarlar'
,New
.Add('Yazar', 'Daniele TETİ')
)
)
)
;
Memo1.Text := XML.SaveToFile(‘C:\Temp\Demo.xml’).AsString;
finally
FreeAndNil(XML);
end;
end;
( Yazının sonunda yukarıdaki örneği kullanacağımız XML sınıfının da kaynak kodlarını bulabilirsiniz. )
Bazen, uzun uzun kod yazmak o kadar sıkıcı, o kadar bunaltıcı oluyor ki insan (bu işi sevmese) illallah edebiliyor. Kendimizi kısır bir döngünün içinde hapsolmuş gibi hissettiğimiz zamanlarda “yahu şunu daha basit bir şekilde halledemiyor muyuz” diye sorarken bulabiliyoruz. Böyle bir ruh halindeyken bunalıp web’de dolaşmaya çıktığım bir vakitte rastladığım sitelerden birinde Fluent Interface konusunda ufak bir örnek ile karşılaştım; biraz inceleyince “HAH!” dedim. “Onca property, onca değişken, onca öznitelik onca yardımcı nesne tanımla, yok parentine şunu ownerine bunu ata gibi kodun arasında kaybolmaya ne gerek var, class’ını zincirleme tanımla olsun bitsin” diyebildim…
Fluent Design, Fluent Interface, seveni de var sevmeyeni de, sanırım dozunda ve yerinde kullandıkça sevmemek elde değil…
Nedir
Fluent Interface konusuna bu forumda da birkaç arkadaşımız göz kırpmış, ufak bir örnek verilmiş fakat nedir, ne değildir pek irdelenmemiş. Bu tekniğin bazı alanlarda işimizi daha basite indirgememize yardım edeceğini düşünüyorum, çünkü kullanımı hakikaten çok basit ve yapısı gerçekten sade… Fakat bu sadeliği hafife almayın, aslında çok büyük bir iş başardığını siz de göreceksiniz…
Bir tanım yapmamız gerekirse o da şöyle bir şey olabilir; Fluent Interface, bir sınıfın kendi metodlarını zincirleme olarak kendi içinde kullanabilmemize imkân tanıyan ve kodun okunabilirliğinden ziyade “anlaşılabilirliğini” hedefleyen bir programlama tekniğidir. Bu teknik, PHP, Java, C# programcıları arasında yaygın bir biçimde kullanılmaktadır. Bununla birlikte Delphi’nin bazı nesnelerinde de bu teknik kullanıla gelmiştir. ( Bakınız http://docwiki.embarcadero.com/Libraries/XE2/en/System.SysUtils.TStringBuilder gibi…)
Olayın can damarında aslında ufak bir “hile” var; Basit bir sınıf tanımlıyorsunuz, ardından o sınıfın tüm fonksiyonlarının RESULT değerini SELF yapıyorsunuz. Böylece sınıfı kullandığınız yerde zincirleme olarak sınıfın tüm elemanlarını birbirine “.” Operatörüyle bağlayabiliyorsunuz. Böylece aradaki oluşturma ve atama işlemlerine girmeden (aslında arka tarafta yapılıyor) doğrudan zincirleme kodlar yazabiliyorsunuz. Bu da size yazılan kodun daha kolay anlaşılabilmesini sağlıyor.
Bu tekniği yansıtan basit bir fluent interface yöntemi için aşağıdaki gibi bir sınıf tanımı başlangıç için yeterli olacaktır.
Interface
type
TKelimeler = class
public
Cumle: String;
function Ekle(aString: String): TKelimeler;
end;
function Yeni: TKelimeler;
Implementation
Görüldüğü gibi normal bir sınıf tanımı yaptık. Tek fark “Ekle” fonksiyonunun veri tipinin sınıf tanımı ile aynı olması. İşte bu nokta bu tekniğin temel yapıtaşını oluşturuyor.
Bunu bir class için değil de bir record için de benzer bir şekilde tanımlayabiliriz. Biz anlatımımızda Class üzerinden devam edeceğiz.
NOT: Verdiğim örnekte özellikle bir TInterfacedObject ve Interface kullanmadım. Bunun sebebini konunun alt kısmında “Dezavantajları” başlıklı bölümde inceleyebilirsiniz.
Kaldığımız yerden bu önemli noktaya devam edip şu “ekle” fonksiyonunun içeride neler çevirdiğine bir bakalım;
function TKelimeler.Ekle(aString: String): TKelimeler;
begin
Cumle := Trim(Cumle + aString) + ‘ ‘;
Result := Self; // Önemli olan bu satır, bu zincir kurmamızı sağlıyor.
end;
Görüldüğü gibi kendisine verilen parametreyi Cumle adlı bir değişkende araya bir boşluk koyarak biriktiriyor ( Cumle := Trim(Cumle + aString) + ‘ ‘; ) ve ardından sonuç olarak (Result := Self) yine kendisini veriyor.
Buraya kadar anlatılanlar, her ne kadar fluent intefacenin basit anlamda yapısını anlamamızı yardımcı olsa da potansiyelini anlamamız açısından pek de doyurucu bir örnek olmadı. (Bunu asıl iş başındayken görmek lazım.) Bu yapıyı biraz daha karmaşıklaştıralım ve sınıfımızı aşağıdaki gibi genişletelim.
interface
uses
System.SysUtils, System.Variants;
Type
TVarArray = array of Variant;
TKelimeler = class // record // (TInterfacedObject, IInterface)
strict private
function OzNitelik(Value: Variant): TKelimeler;
public
Cumle: String;
property Nitelik[Value: Variant]: TKelimeler read OzNitelik; default;
function enter: TKelimeler;
function Ekle(Value: Variant): TKelimeler; overload;
function Ekle(Value: TVarArray): TKelimeler; overload;
function Parantez(Value: TKelimeler; Yoket: Boolean = TRUE): TKelimeler;
end;
function Yeni: TKelimeler;
implementation
Örneğimize devam edecek olursak, senaryomuzu da baştan belirtelim; amacımız TKelimeler nesnesine kelimeler ekleyip bunu bize bir cümle olarak vermesini sağlamak olsun. Fakat bunu yaparken tip dönüşümleriyle uğraşmayalım, aynı zamanda cümlemizin içine parantezler ekleyip yine bu parantezlerde de aynı zincir yapısını kullanabilelim. Hatta herhangi bir fonksiyonu kullanmadan, doğrudan “sadece” değerleri de işleyebilelim. (Biraz abarttık galiba 😀 ) Bunun için nesnemizin mutfağındaki yemek nasıl pişiyor önce onu bir inceleyelim. Buyurun, kodumuzun devamı;
implementation
function Yeni: TKelimeler;
begin
Result := TKelimeler.Create;
end;
{ TKelimeler }
function TKelimeler.Ekle(Value: Variant): TKelimeler;
begin
// Elimize geçirdiğimiz tüm variant değerlerini stringe çeviriyoruz
// bu sayede tip dönüşümleri ile ilgili bir endişeye kapılmamıza gerek kalmıyor.
Cumle := Cumle + VarToStr( Value ) + ' ';
Result := Self;
end;
function TKelimeler.enter: TKelimeler;
begin
// Alt satıra geçiş için özel bir fonksiyon tanımlamış olalım
// (Pek bir esprisi yok aslında… Sadece enter ve satır başı karakterlerini basar)
Cumle := Cumle.Trim + #13#10;
Result := Self;
end;
function TKelimeler.Ekle(Value: TVarArray): TKelimeler;
var
I: Integer;
Begin
// Burada elimize zümre halinde bir grup veri tipi belirsiz parametre geliyor
// Tip dönüşümü meselesini aşmak için hepsini teker teker stringe çevirip
// Cümlemizin sonuna ekliyoruz…
for I := Low(Value) to High(Value)
do Cumle := Cumle + VarToStr( Value[I] ) + ' ';
Result := Self;
end;
function TKelimeler.OzNitelik(Value: Variant): TKelimeler;
begin
// Sınıfımıza bir öznitelik eklemiştik. Bu, bizim “Ekle” fonksiyonumuzu
// daha da kısaltmamıza hizmet ediyor aslında. Konudan uzaklaşmamak adına
// ÖzNitelikler başka bir makalenin konusu olduğu için burada değinmiyorum.
Result := Self.Ekle( Value );
end;
function TKelimeler.Parantez(Value: TKelimeler; Yoket: Boolean = TRUE): TKelimeler;
begin
// İç içe cümleler kurabilmek için yine sınıfımızın bir kopyasını kullanıyoruz.
Cumle := Cumle + '(' + Value.Cumle.Trim + ')';
// Zincir sırasında üretilen kopyayı MEMORY LEAK oluşmaması için yok etmek zorundayız...
if Yoket then FreeAndNil(Value);
Result := Self;
end;
end.
Gelelim, bu yapının nasıl kullanıldığına. Önce en yalın haliyle bir örnekleme yapalım, ardından diğer karmaşık örneklere geçeriz;
var
Replik: TKelimeler;
begin
try
Replik :=Yeni
. Ekle('Ali').Ekle('topu').Ekle('tut')
;
Memo1.Lines.Text := Replik.Cumle;
finally
FreeAndNil(Replik);
end;
end;
Görüldüğü üzere bir sürü “Ekle” fonksiyonu kullandık ve o kadar da sadeymiş gibi durmuyor. Bu örneği biraz daha sadeleştirmekte yarar var, şöyle yapsak nasıl olurdu;
Replik :=Yeni ['Ali']['topu']['tut'] ;
Bir öncekine nazaran daha basit bir örnek olmuş oldu. Peki bu cümleye parantez içinde başka bir cümle eklemek istesek ne yapardık? Şöyle;
Replik :=Yeni['Ali']['topu']['tut']
.Parantez( Yeni ['Kalemi'] ['Bırakma'] [10] [‘kalem’] [‘getir’] ) ;
Görüldüğü üzere nesnemizi recursive bir biçimde iç içe çağırarak okunabilirliği hem bozmamış hem de mevcut bir cümlenin içine başka bir cümleyi ekleyebilmiş olduk.
Farklı veri tiplerinin zincirleme bir reaksiyon içinde nasıl kullanıldığına dair bir örnek teşkil etmesi açısından aşağıdaki kodu inceleyebilirsiniz. Sizin de fark edebileceğiniz gibi bir çok parametre aslında birbirine benzer şekilde kullanılabilmiş. Bir ekle fonksiyonuyla kelime bazında ekleme yapılmış, bir variant array aracılığıyla yine ekle fonksiyonu içinde bir sürü kelime ve sayı tanımlanabilmiş, hatta TDateTime tipinde tarih verileri bile eklenmiş. Normalde aşağıdaki kodun üreteceği veriyi kodlayacak bir kod yazmak istesek klasik yöntemlerle bu daha uzun olur, arada bir çok toplama işlemi yapmamız gerekebilirdi.
var
Replik: TKelimeler;
begin
try
Replik :=Yeni
.Ekle( ['"','Bazen', 'daha', 'yürümeyi', 'öğrenmeden', 'koşmak', 'gerekebilir','"']).enter
.Ekle('Veya').enter
['"']['Bazen']['daha']['yürümeyi']['öğrenmeden']['koşmak']['gerekebilir']['"'].enter
.Ekle('Çalıştır').Ekle('şunu').Ekle('Jarvis!').enter
.Parantez( Yeni ['Demir'] ['Adam'] [1] ).enter
.Ekle( 'VEYA ŞÖYLE BİR ÖRNEK VERELİM' ).enter
.Parantez( Yeni.Ekle('Demir').Ekle('Adam').Ekle(1) ).enter
.Ekle( ['Replik Zamanı =', EncodeTime(1,1,30,0)] ).enter
.Ekle( 'Filmin Vizyon Tarihi =' )[EncodeDate(2008,5,2)].enter
.enter
;
Memo1.Lines.Text := Replik.Cumle;
finally
FreeAndNil(Replik);
end;
end;
Dezavantajlı Olduğu Durumlar
Her güzelin bir kusuru vardır derler, doğru, Fluent Interface’nin de kendine özgü kusurları var. Sanırım bunların kusur mu yoksa nimet mi olduğuna karar vermek, olaya hangi pencereden baktığınızla ilgili, kimisi bunları bir kan davası sebebi olarak görür, kimisi ise bir hoşgörü kaynağı olarak bakar, dolayısıyla bunun takdiri sizin. Gelelim güzelimizin kusurlarına.
- Derleyici Sorunları; TObjects ve türevlerinden ziyade, Interface’leri kullanıyorsanız zincirdeki her çağrı aynı Interface her seferinde geri dönse bile Referance Count yığılmasına sebep olacaktır. Derleyicinin bunu tespit etmesinin bir yolu olduğunu düşünmüyorum (En azından henüz ben keşfedemedim) O nedenle Fluent Interface tanımlarken bir Interface’den miras alınıyorsa (makinanın belleğinde) daha karmaşık bir yığın içeren daha büyük bir kod kümesi üretilmiş olacaktır. O nedenle bu tür tasarımlarda Interface kullanmayı iki kere düşünmek gerekir…
- Hata Ayıklama Sorunları; Fluent Interface kullanımınız sırasında zincirin herhangi bir halkasında hata oluştuğunda Derleyici bunu bir bütün olarak gördüğü için, hatanın nereden kaynaklandığını tespit etmeniz gerçekten çok zaman alır. Eğer hatalı bir metod birden çok kez zincir içinde kullanılmışsa debug yapmak zorlaşır.
- Kod Biçimlendirme Sorunu; Delphi’nin “Format Source” işlevi, Fluent Interface kullanılmış bir zinciri biçimlendirme açısından bir bütün olarak gördüğü için bunu algılayamaz. Bunun tek çözümü sizin bu hizalamaları el ile yapmanızdır.
Artık sözümüzü tutma vakti geldi.
XML Generator
Makalenin başında XML kodu üreten bir örnek Delphi kodu göstermiş ve bunun kaynak kodunu paylaşacağımızı belirtmiştik. Basit ve “ANLAŞILIR” bir şekilde XML kodu üretmek eminin ki hepimizin ortak isteğidir. İşte kodlar;
{*******************************************************}
{ }
{ Fluent XML Class }
{ }
{ Copyright © 2017 Uğur PARLAYAN }
{ ugurparlayan@gmail.com }
{ }
{*******************************************************}
unit XML2_;
interface
uses
System.SysUtils, System.StrUtils, System.Variants, System.Classes, Vcl.Dialogs;
type
TVarArray = array of Variant;
TVarArrayHelper = record helper for TVarArray
public
function Ayracli(aAyrac: String): String;
end;
TEncodingHelper = class Helper for TEncoding
public
function AsEncoderName: String;
end;
TXML2 = class
private
_Version : Double;
_Encoding : TEncoding;
_NameSpace : string;
_Source : String;
strict private
function _if(aKosul: Boolean; aTrue, aFalse: String): String;
function _f(const aFormat: string; const Args: array of const): string;
function _NS: String;
public
function AsString: String; // Bu noktada zincir kırılır...
function Version(Value: Double): TXML2;
function Encoding(Value: TEncoding): TXML2;
function NameSpace(Value: String): TXML2;
function Add(aNode: string): TXML2; overload;
function Add(aNode: string; aValue: Variant): TXML2; overload;
function Add(aNode: string; aSubNode: TXML2): TXML2; overload;
function Add(aNode: string; aAttributes: TVarArray): TXML2; overload;
function Add(aNode: string; aAttributes: TVarArray; aValue: Variant): TXML2; overload;
function Add(aNode: string; aAttributes: TVarArray; aSubNode: TXML2): TXML2; overload;
function SaveToFile(aFileName: TFileName): TXML2;
end;
function New: TXML2; overload;
implementation
function New: TXML2;
begin
Result := TXML2.Create;
end;
{ TXML2 }
function TXML2.AsString: String;
var
Tmp: String;
FS: TFormatSettings;
begin
FS := FormatSettings;
FormatSettings.DecimalSeparator := '.';
Tmp := _Encoding.AsEncoderName;
if (Pos( '',
[ _if(_Version <> 0, _f(' version="%s"', [ formatfloat('0.0',_Version)]), '')
, _if(Tmp.IsEmpty = False, _f(' encoding="%s"', [Tmp]), '')
])
, '')
+ _Source
;
end;
FormatSettings := FS;
Result := StringReplace(_Source, '><', '>'#13#10'<', [rfReplaceAll, rfIgnoreCase]); // CDATA içinde geçerse sıkıntı olabilir...
end;
function TXML2.Version(Value: Double): TXML2;
begin
_Version := Value;
Result := Self;
end;
function TXML2.Encoding(Value: TEncoding): TXML2;
begin
_Encoding := Value;
Result := Self;
end;
function TXML2.NameSpace(Value: String): TXML2;
begin
_NameSpace := Value.Trim;
Result := Self;
end;
function TXML2.Add(aNode: string): TXML2;
begin
_Source := _Source + _f('<%s/>', [aNode.Trim]) ;
Result := Self;
end;
function TXML2.Add(aNode: string; aValue: Variant): TXML2;
begin
_Source := _Source + _f('<%0:s%1:s>%2:s</%0:s%1:s>', [_NS, aNode.Trim, VarToStr(aValue).Trim]) ;
Result := Self;
end;
function TXML2.Add(aNode: string; aSubNode: TXML2): TXML2;
var
Tmp: Variant;
begin
if (Assigned(aSubNode) = TRUE) then begin
Tmp := aSubNode.AsString;
FreeAndNil(aSubNode);
Result := Self.Add(aNode, Tmp);
end else begin
Result := Self;
end;
end;
function TXML2.Add(aNode: string; aAttributes: TVarArray; aValue: Variant): TXML2;
var
Tmp: String;
begin
Tmp := aAttributes.Ayracli(' ').Trim;
_Source := _Source
+ _f('<%0:s%1:s%2:s>%3:s</%0:s%1:s>', [_NS, aNode.Trim, _if(Tmp.IsEmpty = True, '', ' ' + Tmp), VarToStr(aValue).Trim]) ;
Result := Self;
end;
function TXML2.Add(aNode: string; aAttributes: TVarArray; aSubNode: TXML2): TXML2;
var
Tmp: Variant;
begin
if (Assigned(aSubNode) = TRUE) then begin
Tmp := aSubNode.AsString;
FreeAndNil(aSubNode);
Result := Self.Add(aNode, aAttributes, Tmp);
end else begin
Result := Self;
end;
end;
function TXML2.Add(aNode: string; aAttributes: TVarArray): TXML2;
var
Tmp: String;
begin
Tmp := aAttributes.Ayracli(' ').Trim;
_Source := _Source + _f('<%0:s%1:s%2:s/>', [_NS, aNode.Trim, _if(Tmp.IsEmpty = True, '', ' ' + Tmp)]) ;
Result := Self;
end;
function TXML2._f(const aFormat: string; const Args: array of const): string;
begin
Result := Format(aFormat, Args);
end;
function TXML2._if(aKosul: Boolean; aTrue, aFalse: String): String;
begin
if (aKosul = TRUE) then Result := aTrue else Result := aFalse;
end;
function TXML2._NS: String;
begin
Result := _if( (_NameSpace.Trim.IsEmpty = True), '', _NameSpace.Trim+':');
end;
function TXML2.SaveToFile(aFileName: TFileName): TXML2;
var
Dosya : TStreamWriter;
begin
if (directoryExists(ExtractFileDir(aFileName), True) = TRUE) then begin
try
Dosya := TStreamWriter.Create(aFileName, False, TEncoding.UTF8);
Dosya.Write(Self.AsString);
Dosya.Close;
finally
FreeAndNil(Dosya);
end;
end else begin
ShowMessage('Dosya adresinde belirtilen klasör yok');
end;
Result := Self;
end;
{ TVarArrayHelper }
function TVarArrayHelper.Ayracli(aAyrac: String): String;
var
I: Integer;
begin
for I := Low(Self) to High(Self)
do if (I < High(Self) )
then Result := Result + VarToStrDef(Self[I], '').Trim + aAyrac
else Result := Result + VarToStrDef(Self[I], '').Trim;
end;
{ TEncodingHelper }
function TEncodingHelper.AsEncoderName: String;
begin
{---Kaynaklar-----------------------------------------------------------------------}
{ https://docs.microsoft.com/en-us/dotnet/standard/base-types/character-encoding }
{ http://www.iana.org/assignments/character-sets/character-sets.xhtml }
{-----------------------------------------------------------------------------------}
if (Self = TEncoding.ANSI) then Result := 'ANSI' else
if (Self = TEncoding.ASCII) then Result := 'ASCII' else
if (Self = TEncoding.UTF7) then Result := 'UTF-7' else
if (Self = TEncoding.UTF8) then Result := 'UTF-8' else
if (Self = TEncoding.Unicode) then Result := 'UTF-16' else
if (Self = TEncoding.BigEndianUnicode) then Result := 'UTF-16BE' else
if (Self = TEncoding.Default) then Result := 'Windows-1254' else
Result := '';
end;
end.
Yorum yapılmamış