۱۳۹۱ بهمن ۱, یکشنبه

آیا الگوی Service Locator یک Anti-Pattern است؟


ابتدا بهتره یک نگاهی به خود این الگو بندازیم و بیشتر باهاش آشنا بشیم. ویکی پدیای انگلیسی این الگو رو به این شکل تعریف می کنه:

الگوی Service Locator یک الگوی طراحی در توسعه نرم افزار است که پروسه های اختصاص دادن یک سرویس را به کمک یک لایه انتزاعی کپسوله سازی (مخفی) می کند. این الگو از یک رجیستری مرکزی (مرکز ثبت) که با عنوان Service Locator شناخته می شود، استفاده می کند، که با دریافت یک درخواست اطلاعات لازم برای انجام یک عملیات مشخص را برمی گرداند.

به زبان ساده تر، ما یک مرکز ثبت داریم که سرویس ها رو در اونجا ثبت (register) می کنیم و هنگام نیاز، اون سرویس رو فراخوانی (resolve) می کنیم. اگه با IoC Container ها (واگذاری مسئولیت) آشنا باشید این الگو رو راحت تر درک می کنید. درواقع خود همین IoC Container ها یک جور Service Locator پیشرفته اند. اگر با IoC Container ها آشنا نیستید این مقاله آقای وحید نصیری رو مطالعه کنید.

خب، بذارید با کد براتون توضیح بدم. این کد، یک Service Locator پیشنهادی ساده ست که در وبلاگ آقای Mark Seemann ارائه شده:

public static class Locator
{
    private readonly static Dictionary<Type, Func<object>>
        services = new Dictionary<Type, Func<object>>();
 
    public static void Register<T>(Func<T> resolver)
    {
        Locator.services[typeof(T)] = () => resolver();
    }
 
    public static T Resolve<T>()
    {
        return (T)Locator.services[typeof(T)]();
    }
 
    public static void Reset()
    {
        Locator.services.Clear();
    }
}

در این Service Locator، سرویس ها یک دیکشنری ذخیره میشن که به ازای هر نوع، فقط یک تابع برای ایجاد سرویس ذخیره می شه. یک نمونه استفاده از این Service Locator:

public interface ISort
{
     void Sort(IList<int> list);
}

public class MergeSort : ISort
{
      public void Sort(IList<int> list)
      {
          // merge sort algorithm goes here...
      }
}

// 1-initialize
Locator.Register<ISort>(() => new MergeSort());
.
.
.
List<int> numbers = new List<int>() { 6, 2, 5, 8, 1, 3 };
// 2-use
ISort sortAlgorithm = Locator.Resolve<ISort>();
sortAlgorithm.Sort(numbers)


ما یک سرویس به اسم ISort داریم که ازش یک پیاده سازی به نام MergeSort موجوده. در ابتدای برنامه (یا هرجایی، قبل از استفاده از سرویس) سرویس رو در Service Locator ثبت می کنیم (گام اول) و بعد هنگام استفاده ابتدا اون رو Resolve می کنیم (گام دوم).

خب حالا به این موضوع بپردازیم که چرا این الگو یک Anti-Pattern به حساب می آد؟


به نظر آقای Mark، این الگو یک Anti-Pattern به حساب میاد و با ذکر یک مثال این موضوع رو بیان می کنه:

public class OrderProcessor : IOrderProcessor
{
    public void Process(Order order)
    {
        var validator = Locator.Resolve<IOrderValidator>();
        if (validator.Validate(order))
        {
            var shipper = Locator.Resolve<IOrderShipper>();
            shipper.Ship(order);
        }
    }
}

فرض کنید IOrderValidator و IOrderShipper از قبل در Service Locator ما ثبت شده باشند. بنابراین تکه کد بالا باید بدون مشکل اجرا بشه و این اتفاق می افته. ولی چه مشکلی ممکنه پیش بیاد که اجرای این کد به خطا بخوره؟

آقای Mark در دو زمینه این موضوع رو برسی می کنه.



مشکل هنگام استفاده از OrderProcessor بعنوان یک API:

فرض کنید ما صرفا استفاده کننده از OrderProcessor هستیم و به سورس کد اون هیچ دسترسی ای نداریم. منظورم اینه که این کلاس توی یک فایل dll قرار گرفته باشه و ما از نحوه کارش بی خبریم. هنگام کد نویسی، چی ازش می بینیم؟
همونطور که توی تصویر می بینید، این کلاس یک تابع سازنده بدون پارامتر داره، به این معنی که راحت می تونید یک نمونه از اون بسازید و ازش استفاده کنید.
var order = new Order();
//
// fill order
// ...
var orderProcessor = new OrderProcessor();
orderProcessor.Process(order);

متاسفانه هنگام استفاده از این کلاس، متوجه وابستگی اون به سرویس های دیگه ای مثل IOrderValidator و IOrderShipper نمی شیم و مسلم هست که (در خط آخر) به خطای KeyNotFoundException می خوریم. چون این دوتا سرویس توی Locator ثبت نشدن. یا باید مستندات استفاده از اون رو کامل می خوندیم، یا با یک reflector، به کد اولیه دسترسی پیدا می کردیم تا از نحوه کارش با خبر بشیم. تازه یک مسئله رنج آور دیگه اینه که اگه بخواهیم از این Locator توی آزمون های واحد (unit test) استفاده کنیم هر بار باید تابع Locator.Reset رو فراخوانی کنیم. چون سرویس ها بطور static ذخیره شدند.


مشکل در نگهداری:

خب حالا بعد از یک مدت باید یک امکان جدید به این کد اضافه بشه. مثلا لازم شده IOrderCollector.Collect، اون بین فراخوانی بشه. تابه Process به این شکل تغییر می کنه:
public void Process(Order order)
{
    var validator = Locator.Resolve<IOrderValidator>();
    if (validator.Validate(order))
    {
        var collector = Locator.Resolve<IOrderCollector>();
        collector.Collect(order);
        var shipper = Locator.Resolve<IOrderShipper>();
        shipper.Ship(order);
    }
}

خب. حالا چی؟ ممکنه خوش شانس باشیم و IOrderCollector قبلا در یک قسمت دیگه یا در ابتدای unit test توی Locator ثبت کرده باشیم. بنابراین همه ی آزمون های ما (بطور اتفاقی) با موفقیت پاس می شوند. اما اگه توی کد محصول نهایی (Production Code) اینقدر خوش شانس نباشیم چی؟ برنامه هایی که از نسخه جدید کلاس OrderProcessor استفاده می کنند ممکنه با خطای شکننده (Breaking Changes) مواجه بشن.

در نهایت آقای Mark راه حل استفاده از Abstract Service Locator (بطور انتزاعی) رو مطرح می کنند که البته از نظر ایشون چنگی به دل نمی زنه! منظور ایشون اینه که بیایم یک واسط از Service Locator طراحی کنیم و اون رو به عنوان وابستگی کلاس OrderProcessor معرفی کنیم. یه همچین چیزی:

public interface IServiceLocator
{
    T Resolve<T>();
}
 
public class Locator : IServiceLocator
{
    private readonly Dictionary<Type, Func<object>> services;
 
    public Locator()
    {
        this.services = new Dictionary<Type, Func<object>>();
    }
 
    public void Register<T>(Func<T> resolver)
    {
        this.services[typeof(T)] = () => resolver();
    }
 
    public T Resolve<T>()
    {
        return (T)this.services[typeof(T)]();
    }
}

خب این IServiceLocator دقیقا همون چیزی هست که قبلا مایکروسافت توی دات نت گنجونده. System.IServiceProvider برای کارهای همه منظوره. در نهایت، کلاس OrderProcessor رو به این شکل تغییر میدیم:
public class OrderProcessor : IOrderProcessor
{
    private readonly IServiceLocator locator;
 
    public OrderProcessor(IServiceLocator locator)
    {
        if (locator == null)
        {
            throw new ArgumentNullException("locator");
        }
 
        this.locator = locator;
    }
 
    public void Process(Order order)
    {
        var validator = 
            this.locator.Resolve<IOrderValidator>();
        if (validator.Validate(order))
        {
            var shipper =
                this.locator.Resolve<IOrderShipper>();
            shipper.Ship(order);
        }
    }
}

حالا کلاس OrderProcessor، توی تابع سازندش، به یک نسخه از IServiceLocator احتیاج داره. بدین صورت کلاس OrderProcessor رو به IServiceLocator وابسته می کنیم تا برنامه نویس قبل از استفاده متوجه بشه که این کلاس، یک وابستگی داره. 

خب؟ این به ما چی میگه؟ تقریبا چیز خاصی عوض نشده. فقط به زبان بی زبانی به کاربر گفته می شه که توی locator باید بعضی سرویس ها (کدوم سرویس ها؟) ثبت شده باشند. بدون اینکه اسمی از سرویس ها برده بشه. خلاصه آقای Mark و همفکرهاش قانع نشدند. بزرگترین بهانه اون ها اینه که چرا رخ دادن خطاهای compile time به runtime  موکول شده؟ جالبه بسیاری از کامنت های پست ایشون، نظرات متفاوتی رو مطرح کردند. 

اما آیا Service Locator یک Pattern خوب به حساب میاد یا یک Anti-Pattern؟ یا شاید از این الگو به درستی (برای مسئله مناسب) استفاده نشده؟ نظر شما چیه؟