Gerçek Hayattan Factory Method Design Pattern Örneği

By Burak Tungut - 5.6.2016 - Kategori Design / Architectural Patterns

Selamlar herkese,

Bu yazımda sizlerle gerçek bir uygulama ile Factory Method Design Pattern'in nasıl uygulanacağını paylaşacağım. Geçtiğimiz aylarda üzerinde çalıştığımız ana modüllerimizden birinde sürekli benzer istek ve değişikliklerin geldiğini fark ettik. Uygulamada ki genel gidişimizde sürekli if-clause'lar üzerine dayanmaya başlayınca buna bir dur demek istedik ve biraz sonra sizlerle paylaşacağım bir geliştirme gerçekleştirdik. Dilerseniz uygulamaya geçmeden önce ihtiyacın ortaya çıkmasına sebep olan istekleri biraz inceleyelim.

Not : Makale aşağıda listelediğim bazı konuları içerir. Aşağıdaki konular hakkında bilgi sahibi iseniz harika! Değilseniz makalenin daha anlaşılabilir olabilmesi için bir kaç saatinizi ayırarak önce bu listeyi halletmenizi önemle rica ediyorum :)

  • Thread Safety
  • Stateless-Stateful
  • Principal & Identity
  • Reflection

"Backend'i Aynı Olsun, Frontend'i Farklı"

Geliştirmekte olduğumuz bir ürünümüz başlangıçta bir kullanıcı tipine sahip iken iş birimlerimizden bu tiplerin çoğalacağı haberlerini aldık. Fakat her kullanıcı aynı veriye erişirken, verilerin gösterim biçimleri farklı olacaktı. Benzetmek gerekirse uygulamada kullanıcı tipi farketmeksızın herkes beş adet ürün görecek lakin kimi kullanıcı tipleri bunları slider ile görecek iken kimileri bir table içerisinde görecekti.

İsteğe çevik bir şekilde cevap vermek adına geliştirmeleri tamamladık. Lakin MVC projemizde fark ettik ki Controller ve Action'larımız hiç değişmez iken bunları execute ettiği View'larda sürekli if blokları yazmak durumunda kaldık. Tıpkı aşağıdaki gibi;

<h2>Product</h2>

@if (userType == FactoryMethod.Core.UserType.New)
{
    <b>Slider ile datayı görüntüle</b>
}
else if (userType == FactoryMethod.Core.UserType.Old)
{
    <b>Table ile datayı görüntüle</b>
}

Bu if-clause'ları View'dan alıp Action'lara koyarsakta çok bir değişiklik olmayacaktı. Buyrun bakalım;

public class ProductController : Controller
{
    UserType userType = UserType.Old;

    public ActionResult Index()
    {
        //Business call

        if (userType == UserType.New)
        {
            return View("IndexOfNewUsers.cshtml");
        }
        else
        {
            return View("IndexOfOldUsers.cshtml");
        }
    }
}

Bu iki yapının arasında kalsak ben kesinlikle ikinci olanı seçerdim. Sonuçta tüm business ve backend aynı iken sadece frontend değişiyor ise View'ları ayıralım. Hangi View'ın execute edileceğine ise bir önceki pipeline katmanında karar verelim.

Başka Kullanıcı Tipleri Eklenmeye Devam Ederse?

Son yaptığımız haliyle uygulama daha kabul edilebilir bir hal alabilir. Fakat bu yapıyı inşaa etmemize neden olan istekler gelmeye devam ederde yeni kullanıcı tipleri eklenmek isterse sürekli her Action'a if blokları mı yazacağız? İşte bu noktada artık bir dur dememiz gerekir.

Bizim artık öyle bir yapıya ihtiyacımız var ki ne View ne de Action bir state'e bağlı olsun. Yani stateless olsun. Action'ımız Business Engine'ler ile konuşsun, gerekli model'i intialize etsin ve View'a versin. View'da asla business içermesin gerekli veriyi istendiği gibi göstersin.

O zaman bu stateless olarak belirlediğimiz scope'un dışında kullanıcı tipine (ya da herhangi stateful bir olgu) göre akışı değiştirecek, ilgili View'ı bulup execute edecek bir yapıya ihtiyacımız var. 

Biraz detaylıda olsa problemimizi tanıdığımıza göre gerçek bir uygulama ile sorunumuzu çözmeye başlayabiliriz.

Principal Implementasyonu

Aslında yapacağımız örneğin ve Factory Method pattern'inin doğrudan IPrincipal tipi ile bir bağlantısı yok. Fakat bizim bir kullanıcı tipi bilgisine ihtiyacımız var. Web projemize authenticate olmuş kullanıcının bilgilerini ise kendimize özel bir IPrincipal tipinde tuttuğumuzu düşünelim. Bir adet UserType adında enumeration'a ihtiyacımız olacak. İlgili değeride bir IPrincipal tipinde property olarak tutacağız.
Ben aşağıdaki gibi bir yapı tasarladım;

public class MyPrincipal : GenericPrincipal
{
    public UserType UserType { get; set; }

    public MyPrincipal(UserType userType)
        : base(new GenericIdentity("", ""), null)
    {
        this.UserType = userType;
    }

    public static MyPrincipal Current
    {
        get
        {
            MyPrincipal retVal = null;

            if (Thread.CurrentPrincipal is MyPrincipal)
            {
                retVal = (MyPrincipal)Thread.CurrentPrincipal;
            }

            return retVal;
        }
    }
}

public enum UserType
{
    New = 1,
    Old = 2
}

En baştan IPrincipal'dan implemente edilecek bir tip yazmaktansa hali hazırda IPrincipal implementasyonu olan GenericPrincipal'ı extend edebiliriz. Böylece yeni bir IIdentity implementasyonu yazmaktanda kurtulmuş olduk.

Authenticate olmuş kullanıcı bilgsini cookie ya da bir session store'da tutuğumuzu farz edelim. Uygulamaya gelen her bir request'te ilgili store'dan okunma işlemi gerçekleştirilip MyPrincipal nesnesinin ilgili alanlarına set etmeli ve sonuç olarak çıkan nesneyi de Thead'in CurrentPrincipal'ına set set etmeliyiz.Lakin bir login sayfamız olmadığı için aşağıdaki gibi Random değer atayacak bir yapı tasarladım;

protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
    Random rnd = new Random();
    UserType randomUserType = (UserType)rnd.Next(1, 3);

    MyPrincipal principal = new MyPrincipal(randomUserType);
    Thread.CurrentPrincipal = principal;
}
Böylelikle her request'te rasgele bir UserType seçilecek ve bir MyPrincipal instance'ı yaratılarak içine aktarılacak ve bu object'te CurrentPrincipal'a set edilecek. Artık authenticate olmuş kullanıcının bilgisine, aynı Thread'de olmak şartıyla istediğimiz anda ulaşabiliriz.
 

IViewSelector Tipi ve Implementasyonu

Şimdi uygulamamızın çekirdeğini oluşturacak kısmını yazmaya başlayabiliriz. Önceki başlıklarda hatırlarsanız duruma (state) göre execute edilecek olan View'ın değişiklik gösterebileceğine değinmiştik. Öyleyse öyle bir interface ve method signature tasarlıyalım ki geriye execute edilecek olan View'ın yolunu döndürsün

public interface IViewSelector
{
    string GetPath(string[] parameters);
}

Dikkat ederseniz method signature'da parameters adında bir string array istendiğini göreceksiniz. Bu parametreye şimdilik değinmeyeceğim. Bir kaç adım sonra neden gerek duyduğumuzu göreceksiniz.

Şimdi de bir IViewSelector tipini implemente eden bir class yazalım. IViewSelector tipinden gelecek olan methodu ise öyle dizayn edelim ki execute edilecek olan View'a CurrentPrincipal (MyPrincipal tipinde set edilmiş olacak) içerisindeki değere göre karar versin.

public class UserTypeBasedViewSelector : IViewSelector
{
    public string GetPath(string[] parameters)
    {
        switch (MyPrincipal.Current.UserType)
        {
            case UserType.New: return string.Format("~/Views/{0}/{1}OfNewUsers.cshtml", parameters[0], parameters[1]);
            case UserType.Old: return string.Format("~/Views/{0}/{1}OfOldUsers.cshtml", parameters[0], parameters[1]);
            default: throw new Exception("Unknown UserType");
        }
    }
}

Göreceğiniz üzere parameters string array'i burada kullandık. Aslında kendileri View'ın path'ini oluşturmamızda yardımcı olacaklar. Başlangıçları farketmeksızın her iki return değerinin arasındaki bariz fark birinin "...OfNewUsers.cshtml" 'i geri döndürürken diğerinin "...OfOldUsers.cshtml" 'ı geri döndürmesi.

Parametre olarak alınan string array'e ise Controller ve View isimleri vereceğiz. Böylece MVC uygulamalarındaki default route kurallarınıda tamamiyle yıkmamış olacağız.

Factory Method Implementasyonu

Artık öyle bir Factory Method'a ihtiyacımız var ki parametre olarak IViewSelector'ın concrete implementasyonlarından birini Reflected Type'ini alsın ve işletmesi gereken business'ları işletip geriye ilgili Type'ın bir instance'ını döndürsün.

Aslında bir nevi IViewSelector concrete implementasyonlarının reflect edilmiş Type'leri ile instance'larını pair şekilde tutacak ve gerektiğinde sunacak bir collection tasarlamamız gerekiyor.

public class ViewSelectorFactory
{
    private static readonly Lazy<Dictionary<Type, IViewSelector>> container = new Lazy<Dictionary<Type, IViewSelector>>(() => ReflectAndGetInstances(), true);

    public static IViewSelector Get(Type selectorType)
    {
        return container.Value[selectorType];
    }

    private static Dictionary<Type, IViewSelector> ReflectAndGetInstances()
    {
        Dictionary<Type, IViewSelector> retVal = new Dictionary<Type, IViewSelector>();

        var interfaceType = typeof(IViewSelector);
        var types = interfaceType.Assembly.GetTypes().Where(t => t.GetInterfaces().Any(i => i == interfaceType));

        foreach (var type in types)
        {
            retVal.Add(type, (IViewSelector)Activator.CreateInstance(type));
        }

        return retVal;
    }
}

 

Çoğu Factory Method örneğinde üretilecek olan tip için seçilimin bir Enumeration üzerinden yapıldığını göreceksinizdir. Ancak plugin based geliştirilen uygulamalarda ne yazık ki buna yer vermek çok mümkün olmuyor. Storngly-Typed olması açısından kesinlikle önerilselerde extension yapmanın haricinde implementation'da yapma zorunluluğu getirmesi her zaman kabul edilebilir olamıyor.

 

Bunu şöyle açıklamak isterim. Yukarıdaki gibi Get methodu Type almaktansa içerisinde iki üç adet üyesi olan bir Enumeration alabilirdi. Fakat daha sonra başka bir IViewSelector implementasyonu gerçekleştirdiğinizde söz konusu Enumeration'a bir üye eklemek zorunda kalacağınız gibi Get methodunda olası yazacağınız switch-case'e de bir case eklemek zorunda kalacaksınızdır.

Burada dikkat etmemiz gereken diğer bir unsur ise static olan üyelerimiz tüm Thread'lere açık ve eş zamanlı çağırılacak durumdadır. Bu nedenle Lazy tipinin IsThreadSafe flag'ini işaretleyerek tek bir Thread'in ilk çağırımı yapmasını sağlıyoruz. Sonraki çağırımlarda sadece Value'ya ulaşılacağı için Thread-Safety konusunda yaşayacağımız bir problem bulunmamakta.

Son Adım! ActionFilter Implementasyonu

Artık öyle bir ActionFilterAttribute yazalım ki hangi Action'a eklersek ekleyelim return View(); olsa dahi gitsin ilgili IViewSelector tipini ViewSelectorFactory sayesinde bulsun çalıştırsın ve return değerini manipüle etsin.

Benim tasarladığım implementasyon aşağıdaki gibi oldu;

[AttributeUsage(AttributeTargets.Method)]
public class ViewSelectorMappingAttribute : ActionFilterAttribute
{
    public Type SelectorType { get; private set; }

    public ViewSelectorMappingAttribute(Type selectorType)
    {
        if (!selectorType.GetInterfaces().Any(i => i == typeof(IViewSelector)))
            throw new ArgumentException("selectorType must be a type of implemented by IViewSelector", "selectorType");

        SelectorType = selectorType;
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);

        ((ViewResult)filterContext.Result).ViewName = GetViewPath(filterContext);
    }

    private string GetViewPath(ActionExecutedContext filterContext)
    {
        var viewSelectorInstance = ViewSelectorFactory.Get(SelectorType);

        if (viewSelectorInstance != null)
        {
            string controller = filterContext.RouteData.Values["controller"].ToString();
            string action = filterContext.RouteData.Values["action"].ToString();

            return viewSelectorInstance.GetPath(new string[] { controller, action });
        }
        else
        {
            throw new Exception(string.Format("'{0}' cannot be found in View Selector Container.", SelectorType.Name));
        }
    }
}

Constructor'da parametre olarak alacağımız Type'in gercekten bir IViewSelector implementasyonuna ait olup-olmadığının kontrolünü yapmayı ihtmal etmeyelim. Strongly-Typed bir yapı izlememiş olabilir ancak ilgili Action invoke edilirken bu hatayı direkt alıyor olmamızda fayda var.

Böyle bir yol izlediğimiz için build'da belki bu hatayı yakalayamayabiliriz ancak regression test'lerden kaçmayacaktır.

Bir Controller ve Action Üzerinde Kullanılışı

Artık her şey hazır! Hemen bir Controller yaratalım;

public class ProductController : Controller
{
    [ViewSelectorMapping(typeof(UserTypeBasedViewSelector))]
    public ActionResult Index()
    {
        return View();
    }
}

Birde /Views/Product/ altında IndexOfNewUsers.cshtml ve IndexOfOldUsers.cshtml adında iki adet View oluşturup içlerine de kendilerine spesifik küçük yazılar yazalım ve projemizi ayağa kaldıralım.

Hatırlarsanız Global.asax'ta bind ettiğimiz Principal içerisindeki UserType değerine random üretiliyordu. Artık /Product/Index adresine browser üzerinden ulaşalım ve bir kaç defa sayfayı yenileyelim. Değişen UserType değerine karşışık farklı ekranlar ile karşılaşacaksınız. Tıpkı aşağıdaki gibi;

Factory Method Örneği Factory Method Örneği

Yorum Bırak

Facebook
Son Yorumlar