Yazılım Mimarileri ve Tasarım Desenleri Üzerine

Query Object Design Pattern(Sorgu Nesnesi Tasarım Deseni)

Merhaba,

Günümüzde birçok uygulamada veritabanı işlemleri için yaygın olarak ORM(Object Relational Mapping) yaklaşımı tercih ediliyor olsa da birçok küçük ve orta ölçekli şirket tarafından ve hatta geleneksel olarak radikal değişiklikleri kaldıramayacak kadar köklü framework’lere sahip olan büyük şirketler tarafından da Ado.NET’e dayalı olarak uygulama geliştirildiğine de şahit olabilmekteyiz. Tecrübeyle sabittir ki, Ado.NET ile gerçekleştirilen veritabanı çalışmaları, sürekli değişiklik gösteren proje ihtiyaçları neticesinde kaçınılmaz olarak karmaşık hale gelebilmekte ve bu karmaşıklığı yönetmek ve hatta değişiklikler için programcının dikkat becerisine güvenmek gerekmektedir. Halbuki bu karmaşıklığı mümkün mertebe ORM gibi dinamik bir çözümle yönetebilmek ve oluşturulacak sorguları programcının dikkatinden soyutlayabilmek süreç açısından çözümlerin en idealidir kanaatindeyiz. İşte böyle bir durumda veritabanı sorgularının bir nesne üzerinden temsil edilerek geliştiriciden bağımsız bir şekilde yönetilebilmesini sağlayan Query Object Tasarım Desenini kullanabilir ve ideal çözüme bir adım daha yakınlaşarak çalışmalarımıza devam edebiliriz.

Neden Query Object Design Pattern’ı Kullanmalıyız?

Yukarıda giriş paragrafında bahsedildiği üzere uygulama içerisinde ORM kullanılmıyorsa ve kod içerisinde SQL sorguları oluşturulacaksa Query Object Design Pattern ideal bir çözüm sağlamaktadır. Çünkü, geliştiriciler kullanılacak veritabanına ve o veritabanının sorgulama diline tam hakim olamayabilirler. Yahut veritabanına hakim olsalar bile uygulamanın veritabanı şemasını/mantığını bilmeleri/çözmeleri gerekmektedir. Hangi tabloda hangi verilerin tutulduğu, uygulama konfigürasyonlarının nerede tanımlandığı veya tablolar arası ilişkilerin nasıl olması gerektiği vs. gibi birçok durum geliştiricileri ilgilendirecektir. Oysa bir geliştiricinin işi mümkün mertebe elindeki malzemelerle ortaya iş koyabilmek ve bunu yayına sürebilmektir. Bu kadar bilgiyi öğrenmek için vereceği vakit şirket için oldukça maliyetli bir netice ortaya koyabilir. İşte, Quert Object tasarım deseni bu ve bunlara benzer durumların minimum bilgi gereksinimi ile çözülmesini sağlayacak ve geliştiricinin direkt olarak işe odaklanmasını sağlayacaktır.

Quert Object, bir veritabanı sorgusunu temsil eden nesne olarak anlaşılabilir.

Quert Object tasarım deseni sayesinde nesne olarak geliştirilen sorgular ihtiyaç dahilinde kullanıldıktan sonra tekrar ihtiyaca binaen depolanabilmektedir. Yapının güzel yanlarından biriside burasıdır. Ayrıca bu modellemenin ana yararlarından birisinin uygulamayı veritabanı sorgu dilinden tamamen soyutlamasıdır diyebiliriz. Bunu yazımızın devamında daha net bir şekilde inceliyor olacağız.

Query Object Design Pattern Diyagramını İnceleyelim

Yukarıdaki diyagram şemasına göz atarsanız eğer ihtiyaç duyulan SQL sorgusunu ve o sorguda kullanılacak kriterleri temsil eden nesnelerin kullanıldığı bir tasarım görmekteyiz. Modelleme, tasarım açısından bu kadar basittir.

Bu tasarım neticesinde oluşturulan Query Object pattern sayesinde, nesnelerin yorumlanarak SQL çıktısı oluşturan bir model ortaya koyulmaktadır. Eğer ki burada nesne yerine bir veriyi yorumlayarak başka bir çıktı oluşturuyor olsaydık bu Interpreter tasarım deseni olacaktı. Bu benzerlikten dolayı kimi programcılar, Query Object pattern’ın gerçek bir tasarım deseni olmadığı, Interpreter pattern’ın sadece farklı bir modellemesi olduğu yorumunda bulunmaktadırlar. Doğrusu bu yorumda haklılık payları yokta değildir! Çünkü Query Object tasarım deseni sayesinde verilere göre de uygun yorumlamalar yaparak çeşitli SQL çıktıları da sunulabilmektedir.

Quert Object, bir yorumlayıcıdır, yani kendisini bir SQL sorgusuna dönüştürebilen bir nesne yapısıdır.

Martin Fowler

Query Object nesneleri tasarlanırken esnek yapılar kurar ve böylece veritabanı sistemlerinin her özelliklerini kullanılabilir hale getiririz. Böylece efektif kullanım sağlayabilir ve sorgulardaki komplekslik oranını arttırabiliriz. Ayrıca, Query Object’ler ile oluşturulacak SQL çıktılarını, çalışılacak veritabanı sistemine göre farklı şekilde elde edebiliriz. Bu farklılıklar sayesinde birden fazla veritabanı sistemi ile çalışabiliriz.

Özellikle karmaşık sorgulama süreçlerinde bu pattern sayesinde oldukça performanslı bir çalışma sergilenmiş olacaktır. Nihayetinde koda gömülü kompleks bir sorgunun süreçteki ihtiyaçlara istinaden modifiye edilmesi ve bakımı oldukça maliyetli olacaktır. Lakin ihtiyaç doğrultusunda olası değişiklikleri en başta öngörerek bu tasarımın temellerinin atılması büyük avantaj sağlayacaktır.

Ne Zaman Kullanılmaz!

Bir olgunun artısı olduğu kadar eksisi de olabilmektedir. Dolayısıyla Query Object nesnelerinin getirisi kadar götürüsü de mevcuttur. Buradaki götürü ilgili nesnelerin oluşturulma maliyeti, bir başka deyişle zahmetidir. Evet, bir Query Object oluşturmak/tasarlamak oldukça zahmetlidir. Bundan dolayı; eğer ki çalışılan projede herhangi bir ORM teknolojisi kullanılıyorsa bu pattern’ın uygulanması kesinlikle tavsiye edilmemektedir. Ha nerede ve ne zaman kullanılacağını soruyorsanız eğer bu sorunun cevabını yukarıdaki satırlarda verdiğim kanaatindeyim 🙂

Avantajları

Örnek Tasarım Modellemesi & Kullanım

Bu pattern’ı net bir şekilde örneklendirebilmek için öncelikle bir developer’ın Ado.NET ile koda gömülü sorgularla çalışırken ne gibi bir atmosferde olduğunu simüle ederek bu yaklaşımın gerekliliğini ortaya koymaya çalışalım. Bunun için aşağıdaki gibi tasarlanarak Ado.NET temelleri atılan bir sınıf üzerinden ihtiyaçların giderildiğini varsayarak başlayalım:

    public class DALContext
    {
        readonly static SqlConnection _connection;
        readonly static object _object = new();
        static DALContext()
        {
            lock (_object)
                _connection = new("Server=localhost, 1433;Database=QueryObjectDesignPatternDB;User ID=SA;Password=***");
        }
        async Task<SqlConnection> GetOpenConnectionAsync()
        {
            if (_connection.State == ConnectionState.Closed)
                await _connection.OpenAsync();

            return _connection;
        }
        public async Task CloseConnectionAsync()
        {
            if (_connection.State == ConnectionState.Open)
                await _connection.CloseAsync();
        }
        public async Task<SqlDataReader> ExecuteQueryAsync(string query)
        {
            using SqlCommand command = new(query, await GetOpenConnectionAsync());
            SqlDataReader dr = await command.ExecuteReaderAsync();
            return dr;
        }
    }

Görüldüğü üzere Ado.NET mimarisi eşliğinde veritabanını sorgulamamızı sağlayacak olan temel işlemlerin icra edildiği ‘DALContext’ isimli bir sınıfımız mevcuttur. Bu sınıfı kullanarak aşağıdaki gibi veritabanındaki ‘Products’ tablosuna sorgular gönderilebildiğini varsayalım.

‘Products’ tablosu…

DALContext context = new();
SqlDataReader dr = await context.ExecuteQueryAsync("SELECT * FROM Products");
while (await dr.ReadAsync())
    Console.WriteLine($"Id : {dr["Id"]}\tName : {dr["Name"]}\tStock : {dr["Stock"]}\tPrice : {dr["Price"]}");
await context.CloseConnectionAsync();

Yukarıdaki kodu derleyip çalıştırdığımızda aşağıdaki gibi veritabanından istenilen verilerin elde edildiğini görüyor olacağız.Şimdi bir developer bu ve benzeri bir sorguyu yazarken insani halden ötürü aşağıdaki gibi hata(lar) yapma ihtimali olası mıdır?
SELECT *, FROM Products
SELECT Name, Fiyat FROM Products
SELECT Name, Fiyat FROM Urunler
Tabi ki de olasıdır 🙂 Bu hataların olası olmalarının nedenleri üst paragraflarda da bahsedildiği gibi kâh developer’ın anlık bilinciyle ilgili dikkat dağınıklığı kâh ilgili veritabanı diline hakim olamaması gibi türlü sebepler olabilir. Peki, sorgularımız bu kadar mı? Tabi ki de hayır! Temel düzeyde bir uygulamada bile gelecek verilerin filtrelendiği şartlı sorgular ya da farklı tabloların birleştirildiği join’li sorgular kaçınılmaz olarak kullanılmaktadır. Dolayısıyla bu tarz sorguları developer’ın sorumluluğuna bırakırsak komplekslik arttıkça olası hata oranları da muhtemeldir ki artacaktır.

İşte böyle bir durumda sorguları developer odaklı hatalardan arındırabilmek için oluşturulma sorumluluklarını developerlardan soyutlayarak otomatize hale getirebilmek için Query Object pattern’ını uygulamamız gerekmektedir. Şimdi gelin adım adım bu pattern’ın uygulanmasını inceleyelim.

İşte, görüldüğü üzere Query Object pattern her bir veritabanı sorgusunu, temsil eden bir generic nesne olarak tasarlamamızı ve ihtiyaca dönük sorguları tip güvenli bir şekilde fonksiyonaliteler eşliğinde oluşturmamızı sağlamakta ve böylece geliştiricinin sorgu süreçlerinde yükünü minimuma çekip olası maliyetlerden geliştirme sürecini arındırmaktadır.

Tabi ki de bu pattern’ı inşa ederken genel stratejisinin haricinde geri kalan özellik ve fonksiyonları kendi ihtiyaç ve düşüncenize göre tasarlayabilir ve çok farklı niteliklerde işlevsellikler ekleyerek generate edilecek sorgu çeşitliliğini azami miktarda detaylandırabilir ve arttırabilirsiniz.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek projeyi aşağıdaki github adresinden edinebilirsiniz.
https://github.com/gncyyldz/QueryObjectDesignPatternExample

Mobil sürümden çık