Tasarım Desenleri : Fluent Design

Created with Sketch.

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ış

Yorumunuzu ekleyin