Basitleştirilmiş Bir Kural Motoruyla Bildirimsel if İfadeleri

Kod, bir yol gibi görülürse, if ifadeleri araç kullanırken arada sırada karşılaşılan kavşaklar olarak tanımlanabilir. Kavşaklar, gideceğiniz yere ulaşmak istiyorsanız ve daha önemlisi güvenli bir şekilde ulaşmak istiyorsanız, özellikle dikkat etmeniz gereken, yoldaki tehlikeli yerlerdir.

Yoldaki kavşaklar gibi, if ifadeleri de gerekli karmaşıklık noktalarını belirtir. Siz de kod satırlarınızın şehir mimarı ve yapımcısı olarak, onları güvenli ve kodunuzun okuyucularının olabildiğince kolay gezinebileceği şekilde tasarlamanız gerekir.

Birçok if ifadesi, tıpkı kesişen iki yolun bir trafik ışığıyla yönetilebilmesi gibi belirli bir tasarıma ihtiyaç duymaz. Ancak bazılarının karmaşıklığı, kodun doğru yöne girdiğinden ve okuyucularınızın kaybolmayacağından emin olmak için bir kavşak, hatta bir köprülü kavşak tasarlamanızı gerektirir.

Şimdi bu karmaşık if ifadelerine odaklanalım ve bunları basitleştirilmiş bir kural motoruyla kodlayarak bildirimsel bir şekilde ifade edelim.

İyi Müşteriler, Kötü Müşteriler

Örnek olarak, bir müşterinin iyi bir müşteri olarak sınıflandırılıp sınıflandırılmadığına bağlı olarak işlem yapan bir kod parçası düşünelim.

Belirtimin, aşağıdaki koşullardan en az birini sağlaması durumunda müşterinin iyi bir müşteri olduğunu söylediğini varsayalım:
  • Geçen yıl boyunca 1000 TL'den çok harcama yapmış.
  • Satın alınan bir ürünü asla iade etmemiş.
  • Müşteri anketine en az bir kere katılmış
Ve bizim için tüm bu bilgileri kolayca sağlayacak bir Müşteri UPA'mız olduğunu düşünelim:

const bool isAGoodCustomer = customer.purchasedGoodsValue() >= 1000 
                          || !customer.hasReturnedItems()
                          || std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders);

if (isAGoodCustomer)
{
    std::cout << "Dear esteemed customer,";
}
else
{
    std::cout << "Dear customer,";
}

Bunu biraz daha heyecanlı hale getirmek için başka bir madde ekleyelim: müşteri temerrüde düşerse (yani faturalarını ödeyemezse), diğer şartlardan bağımsız olarak iyi bir müşteri değildir.

Bu veya benzerleri, uygulamanıza eklemenizin gerekebileceği özelliklerdendir. Peki bunu, yukarıdaki koda nasıl ekleriz? Olasılıklardan biri, mantık ifadesi üzerine yeni bir boole ifadesi eklemek olacaktır:

const bool isAGoodCustomer = (customer.purchasedGoodsValue() >= 1000 
                          || !customer.hasReturnedItems()
                          || std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders))
                      && !customer.hasDefaulted();

Ancak if ifadesi, okunması zor bir hal alır. Daha anlaşılır olması için if ifadeleri, belirtimleri gibi olabildiğince okunur görünmelidir. Şimdi bunu sağlamak için if ifadelerinin bildirimsel olmasını sağlayan basitleştirilmiş bir kural motoru kullanalım.

Kural Motoru

Kural motoru, bazı kuralları almak ve bunları belirli bir duruma uygulamak için tasarlanmış yazılımdır. Örneğin, bir müşterinin, iyi bir müşteri olup olmadığını belirleyen tüm maddeleri bir kural motoruna söyleyebilir ve daha sonra belirli bir müşterinin, iyi bir müşteri olup olmadığını sorabiliriz. Kural motoru, bu müşteriyi kurallara karşı eşleştirir ve bu kuralların işletilmesinin sonucunu verir.

Kural motoru, bazı iş mantığı kodlarını hafifletmek ve kuralları optimize bir şekilde ele almak için ana uygulamanın dışında çalışan karmaşık yazılım parçalarıdır.

Küçük if ifademiz için bir kural motoru kullanmak, aşırı mühendislik gibi görünebilir. Bununla birlikte, bu basit örnekle bir kural motoru fikrini kullanabilir ve basitleştirilmiş bir sürümünü uygulayabiliriz.

Yazının devamını okumadan önce kural motorları hakkında daha çok bilgi almak isterseniz şu iki yazıyı okuyabilirsiniz:

Hedef Arayüz

Kodun nasıl görünmesini istediğimize karar vererek başlayalım, sonra bu arayüzü uygulamak için bir kural motoru oluşturalım.

Belirtimimize benzeyen bir bildirimsel kod şöyle olabilir:

isAGoodCustomer if (customer.purchasedGoodsValue() >= 1000)
isAGoodCustomer if (!customer.hasReturnedItems())
isAGoodCustomer if (std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders))

isNotAGoodCustomer if (customer.hasDefaulted())

if (isAGoodCustomer)
{
    std::cout << "Dear esteemed customer,";
}
else
{
    std::cout << "Dear customer,";
}

Bu kod olduğu şekilde derlenmeyecektir. Fakat beklenen davranışı sağlayan ve derlenen bir benzerine yaklaştırabiliriz.

Kural Motorunun Gerçeklenmesi

Kural motorumuz iki anlamı olan bazı boole değerleri alabilir:
  • 1000 TL'den çok tutan bir şey satın almış olmak gibi bir yeter koşul. Nihai sonuç olarak true çıktı almak için bir yeter koşul yeterlidir.
  • Temerrüde düşmek gibi bir engelleyici koşul. Eğer bir engelleyici koşulla karşılaşılırsa diğer şartlar ne olursa olsun çıktı false'tur.
Bir If metoduyla yeter koşulları ve bir NotIf metoduyla da engelleyici koşulları girerek başlayalım:

class RulesEngine
{
public:
   void If(bool sufficientCondition) { sufficientConditions.push_back(sufficientCondition); }
   void NotIf(bool preventingCondition) { preventingConditions.push_back(preventingCondition); }

private:
   std::deque<bool> sufficientConditions;
   std::deque<bool> preventingConditions;
};

Burada std::vector<bool> yerine std::deque<bool> kullanıldığına dikkat edin. Bunun nedeni konu dışıdır, ancak ilginizi çektiyse High Integrity C++ (HIC++) Coding Standard — 155 C++ Coding Rules ve Why is vector<bool> not a STL container? yazılarına bakabilirsiniz.

Şimdi kural motoru tüm verileri depoladığına göre, bunları değerlendirmesini sağlamalıyız. C++'ta hoş bir sözdizimi, motoru çağırmak için operator() kullanmaktır. Ancak başka bir dilde değerlendirme, örneğin .get() veya .evaluate() gibi normal bir metod da olabilir.

   bool operator()() const
   {
      auto isTrue = [](bool b){ return b; };
      return std::any_of(sufficientConditions, isTrue) && std::none_of(preventingConditions, isTrue);
   }

return ifadesi ne kadar hoş ve etkileyici, öyle değil mi? Etkileyici bir arayüz ve etkileyici bir uygulama, dikkate değer bir soyutlamanın iyi bir işaretidir.

Ne yazık ki, bu range tabanlı sözdizimi, şu an için gerçek olamayacak kadar iyi. Ancak bu özelliğin C++20'ye dahil edilme olasılığı yüksek. Her neyse, bir taşıyıcıyı alan ve iteratörleriyle STL algoritmalarını çağıran bir sarmalayıcı fonksiyon yazabilir veya STL algoritmalarını bugün oldukları şekilleriyle doğrudan kullanabiliriz:

   bool operator()() const
   {
      auto isTrue = [](bool b){ return b; };
      return std::any_of(begin(sufficientConditions), end(sufficientConditions), isTrue)
          && std::none_of(begin(preventingConditions), end(preventingConditions), isTrue);
   }

Şimdi ilk kodumuzu kural motorunu kullanarak yeniden yazalım:

auto isAGoodCustomer = RulesEngine{};

isAGoodCustomer.If(customer.purchasedGoodsValue()) >= 1000);
isAGoodCustomer.If(!customer.hasReturnedItems()));
isAGoodCustomer.If(std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders));

isAGoodCustomer.NotIf(customer.hasDefaulted());

if (isAGoodCustomer())
{
    std::cout << "Dear esteemed customer,";
}
else
{
    std::cout << "Dear customer,";
}

Arayüzü Arındırma

Yukarıdaki kod, engelleyici koşulları tanımlayan satır dışında hedefimizden çok da uzak değildir. Kodumuzda aşağıdaki satır yer alır:
isAGoodCustomer.NotIf (customer.hasDefaulted ());
Oysa hedefimiz şuydu:
isNotAGoodCustomer if (customer.hasDefaulted ())
Bunu başarmak için, isNotAGoodCustomer isimli, bir If metoduyla engelleyici koşulları alan ve bunları ana kural motoru isAGoodCustomer'a ileten bir alt kural motoru oluşturabiliriz.

class PreventingRulesEngine
{
  public:
     explicit PreventingRulesEngine(RulesEngine& rulesEngine) : rulesEngine_(rulesEngine) {}
     void If(bool preventingCondition){ rulesEngine_.NotIf(preventingCondition); }
  private:
     RulesEngine& rulesEngine_;
};

Ana kural motoru daha sonra Not terimi altında bir alt PreventingRulesEngine sağlayabilir.

class RulesEngine
{

public:
   RulesEngine() : Not(*this){}

   PreventingRulesEngine Not;

   // ...

Bunu uygulamak için teknik bir incelik var çünkü her iki sınıf da birbirine bağlı. Buna daha sonra döneceğiz, ama önce işletme kodundaki sonuca bir göz atalım:

auto isAGoodCustomer = RulesEngine{};

isGoodCustomer.If(customer.purchasedGoodsValue()) >= 1000);
isGoodCustomer.If(!customer.hasReturnedItems()));
isGoodCustomer.If(std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders));

auto isNotAGoodCustomer = isAGoodCustomer.Not;
isNotAGoodCustomer.If(customer.hasDefaulted());

if (isAGoodCustomer())
{
    std::cout << "Dear esteemed customer,";
}
else
{
    std::cout << "Dear customer,";
}

Görüldüğü gibi hedef koda yeterince yaklaştık.

Tüm Kod Bir Arada

Söylediğimiz gibi, birbirlerine bağlı iki sınıf olan RulesEngine ve PreventingRulesEngine'in nasıl gerçekleneceğine bakalım.

Yalnızca bir başlıktan oluşan bir gerçekleme istiyorsanız, PreventingRulesEngine'i, RulesEngine'in bir iç sınıfı olarak tanımlayabilirsiniz.

class RulesEngine
{
public:
    RulesEngine() : Not(*this){}

    void If(bool sufficientCondition) { sufficientConditions.push_back(sufficientCondition); }
    void NotIf(bool preventingCondition) { preventingConditions.push_back(preventingCondition); }

    class PreventingRulesEngine
    {
      public:
         explicit PreventingRulesEngine(RulesEngine& rulesEngine) : rulesEngine_(rulesEngine) {}
         void If(bool preventingCondition){ rulesEngine_.NotIf(preventingCondition); }
      private:
         RulesEngine& rulesEngine_;
    };
    PreventingRulesEngine Not;

    bool operator()() const
    {
       auto isTrue = [](bool b){ return b; };
       return std::any_of(begin(sufficientConditions), end(sufficientConditions), isTrue)
           && std::none_of(begin(preventingConditions), end(preventingConditions), isTrue);
    }

private:
    std::deque<bool> sufficientConditions;
    std::deque<bool> preventingConditions;
};

İç içe sınıfları sevmiyorsanız ancak yine de yalnızca tek bir başlık çözümünü istiyorsanız, RulesEngine'i önden deklare edip ardından PreventingRulesEngine metodlarını satıriçi olarak gerçekleyebilirsiniz. Ancak bu, belki de karşılaştığınız en güzel kod olmayacaktır. Bu durumda, kodu bir başlık dosyasıyla bir .cpp dosyasına bölmek muhtemelen daha açıklayıcı olacaktır:

// RulesEngine.hpp

class RulesEngine;

class PreventingRulesEngine
{
  public:
     explicit PreventingRulesEngine(RulesEngine& rulesEngine);
     void If(bool preventingCondition);
  private:
     RulesEngine& rulesEngine_;
};

class RulesEngine
{
public:
   RulesEngine();

   void If(bool sufficientCondition);
   void NotIf(bool preventingCondition);
   PreventingRulesEngine Not;

   bool operator()() const;

private:
   std::deque<bool> sufficientConditions;
   std::deque<bool> preventingConditions;
};


// RulesEngine.cpp

RulesEngine::RulesEngine() : Not(*this){}

void RulesEngine::If(bool sufficientCondition)
{
   sufficientConditions.push_back(sufficientCondition);
}

void RulesEngine::NotIf(bool preventingCondition)
{
    preventingConditions.push_back(preventingCondition);
}

bool RulesEngine::operator()() const
{
   auto isTrue = [](bool b){ return b; };
   return std::any_of(begin(sufficientConditions), end(sufficientConditions), isTrue)
       && std::none_of(begin(preventingConditions), end(preventingConditions), isTrue);
}

PreventingRulesEngine::PreventingRulesEngine(RulesEngine& rulesEngine) : rulesEngine_(rulesEngine) {}

void PreventingRulesEngine::If(bool preventingCondition)
{
    rulesEngine_.NotIf(preventingCondition);
}
Kodların çalıştırılmaya hazır bir sürümünü şuradan alabilirsiniz. Üzerinde geliştirme yapacak başka bir basit kural motoru ararsanız şuradaki projeye de bakabilirsiniz.

Etkileyici Bir Yolculuk

Her if ifadesi için kural motoru kullanmalı mıyız? Kesinlikle hayır, her kavşakta bir döner kavşağa gerek olmadığı gibi bizim de her if ifadesi için kural motoruna ihtiyacımız yok. Ancak basitleştirilmiş kural motorumuz, bazı if ifadelerinin karmaşıklığını azaltabilir ve bildirimsel bir tarz benimseyerek kodu daha açık hale getirebilir.

Kural motorunu zenginleştirmeli miyiz? Aklınıza karmaşık if ifadelerini bildirimsel bir şekilde ifade edebilecek başka metodlar geliyor mu?

Bir sonraki yazıya kadar size kodunuzu yazarken güvenli ve etkileyici bir yolculuk diliyorum.

Kaynak

Bu yazı, Fluent C++'ta yayımlanan Declarative If Statements with a Simplified Rules Engine yazısının sadeleştirilmiş bir Türkçe çevirisidir.

Yorumlar

  1. Merhaba. Bildiğim kadarıyla Eric Niebler C++20 Ranges ile vector'un proxy referans döndürmesi sorununu çözecek: http://ericniebler.com/2015/03/03/iterators-plus-plus-part-3/
    https://en.cppreference.com/w/cpp/iterator

    C++20 ile birlikte vector'u kullanmaya geri dönebilir miyiz?

    YanıtlaSil
  2. Pardon, yukarıdaki yorumumda web sitesi vector'dan sonraki bool'u silmiş

    YanıtlaSil

Yorum Gönder

sen de yaz yaz yaz buraya yaz bütün sözlerini

Bu blogdaki popüler yayınlar

Mızıka Tabları Nasıl Okunur

conio.h

Muhtar Kellesi