الگوی طراحی ناظر (Observer) در زبان C#

توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.

الگوی طراحی Observer یا «ناظر» یک الگوی رفتاری است که به ما این امکان را می‌دهد که مکانیزم اشتراکی ایجاد کنیم تا چندین شیء را در مورد هر رویدادی که برای موجودیتی رخ می‌دهد، مطلع کنیم.

الگوی Observer به اشیاء اجازه می‌دهد در صورتی که وضعیت داخلی‌شان تغییر کند، ناظرین خود را مطلع کنند. این بدان معناست که یک کلاس واحد باید از موجودیت‌هایی که آن را مشاهده می‌کنند آگاه باشد و همچنین لازم است تغییرات در وضعیت خود را به آنها اطلاع دهد. علاوه بر این، ناظرین باید به صورت خودکار از تغییرات باخبر شوند.

مثال کد مرتبط با این مطلب را می‌توانید در GitHub پیدا کنید.

مفهوم‌سازی مسئله

تصور کنید که یک فروشگاه آنلاین داریم. در این فروشگاه، دو موجودیت اصلی داریم: Customer و Store. یکی از مشتریان ما سلیقه خاصی دارد و به یک برند خاص از کنترل‌های از راه دور فوق‌العاده علاقه‌مند است. این کنترل‌ها قرار است به زودی در فروشگاه ما موجود شوند.

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

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

حال ما در یک وضعیت دشوار قرار گرفته‌ایم. یا مشتریان باید وقت خود را برای بررسی موجودی محصول تلف کنند یا فروشگاه باید منابع خود را صرف اطلاع‌رسانی به مشتریان اشتباه کند.

یک شیء که وضعیتش مورد مشاهده قرار می‌گیرد، معمولاً به عنوان موضوع شناخته می‌شود، اما چون این شیء سایر اشیاء را از تغییرات وضعیتش مطلع می‌کند، ما آن را ناشر می‌نامیم. تمام اشیائی که می‌خواهند تغییرات وضعیت ناشر را دنبال کنند، مشترک نامیده می‌شوند.

الگوی طراحی Observer پیشنهاد می‌کند که مکانیزم اشتراک به کلاس ناشر اضافه شود تا اشیاء به صورت جداگانه بتوانند به جریان رویدادهایی که ناشر ارسال می‌کند، اشتراک یا لغو اشتراک کنند.

در عمل، این مکانیزم شامل یک آرایه برای ذخیره لیستی از ارجاعات به اشیاء مشترک و چندین متد عمومی است که امکان افزودن یا حذف مشترکین از لیست را فراهم می‌کنند.

الگوی طراحی ناظر (Observer)

حالا ناشر لیست مشترکین خود را بررسی می‌کند و متد اطلاع‌رسانی خاص آنها را فراخوانی می‌کند.

در کاربردهای واقعی، ممکن است ده‌ها کلاس مشترک مختلف وجود داشته باشد که به رویدادهای همان کلاس ناشر علاقه‌مند باشند. ما نمی‌خواهیم ناشر به همه آن کلاس‌ها وابسته شود.

بنابراین، مهم است که همه مشترکین یک رابط مشترک را پیاده‌سازی کنند و ناشر تنها از طریق این رابط با آنها ارتباط برقرار کند. این رابط باید متد اطلاع‌رسانی را همراه با مجموعه‌ای از پارامترها تعریف کند که ناشر بتواند برخی داده‌های متنی را همراه با اطلاع‌رسانی ارسال کند.

اگر برنامه ما از چندین کلاس ناشر مختلف استفاده کند، می‌توان همه آنها را با استفاده از همان رابط استاندارد طراحی کرد. این رابط فقط نیاز به تعریف چند متد اشتراک دارد و به مشترکین اجازه می‌دهد وضعیت ناشران را بدون وابستگی به پیاده‌سازی‌های خاص آنها مشاهده کنند

ساختار الگوی طراحی Observer

در پیاده‌سازی پایه‌ای، الگوی Observer شامل چهار جزء اصلی است:

نمودار کلاس الگوی طراحی ناظر (Observer)

  1. ناشر (Publisher): ناشر رویدادهای مورد علاقه سایر اشیاء را صادر می‌کند. این رویدادها زمانی رخ می‌دهند که ناشر وضعیت خود را تغییر دهد یا یک رفتار خاص را اجرا کند. ناشران زیرساخت اشتراک را در خود دارند که امکان پیوستن مشترکین جدید یا خروج مشترکین فعلی از لیست را فراهم می‌کند. وقتی یک رویداد جدید رخ می‌دهد، ناشر لیست مشترکین را مرور کرده و متد اطلاع‌رسانی تعریف‌شده در رابط مشترک را برای هر شیء مشترک فراخوانی می‌کند.

  2. مشترک (Subscriber): رابط مشترک، متدی به نام Update (بروزرسانی) را تعریف می‌کند که معمولاً تنها متدی است که در این رابط وجود دارد. این متد ممکن است چندین پارامتر داشته باشد که اجازه می‌دهد ناشر جزئیات رویداد را همراه با بروزرسانی ارسال کند.

  3. مشترک خاص (Concrete Subscriber): مشترکین خاص در پاسخ به اعلان‌های ناشر، اقداماتی انجام می‌دهند. تمام این کلاس‌ها باید رابط مشترک را پیاده‌سازی کنند تا ناشر به کلاس‌های خاص وابسته نشود. معمولاً مشترکین برای مدیریت صحیح بروزرسانی‌ها به اطلاعات متنی نیاز دارند. به همین دلیل، ناشران اغلب داده‌های متنی را به عنوان آرگومان به متد اطلاع‌رسانی ارسال می‌کنند. ناشر می‌تواند خود را به عنوان یک آرگومان ارسال کند تا مشترک بتواند داده‌های مورد نیاز را مستقیماً از ناشر دریافت کند.

  4. مشتری (Client): مشتری اشیاء ناشر و مشترک را به صورت جداگانه ایجاد می‌کند و سپس مشترکین را برای اشیاء منتشرشده ثبت می‌کند.

برای نمایش این الگو، تصور کنید که می‌خواهیم سیستمی پیاده‌سازی کنیم که نوسانات قیمت کالاها را در بازارهای محلی کشاورزان ردیابی کند.

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

ابتدا، کلاس انتزاعی Goods را تعریف می‌کنیم که باید متدهای لازم برای پیوستن یا حذف ناظرین و ردیابی قیمت فعلی یک کالای خاص را پیاده‌سازی کند.

				
					using Observer.Observers;

namespace Observer.Subjects
{
    /// <summary>
    /// The Subject abstract class
    /// </summary>
    public abstract class Goods
    {
        private double pricePerKilo;
        private List<IRestaurant> restaurants = new List<IRestaurant>();

        protected Goods(double pricePerKilo)
        {
            this.pricePerKilo = pricePerKilo;
        }

        public void Attach(IRestaurant restaurant)
        {
            restaurants.Add(restaurant);
        }

        public void Detach(IRestaurant restaurant)
        {
            restaurants.Remove(restaurant);
        }

        public void Notify()
        {
            foreach(IRestaurant restaurant in restaurants)
            {
                restaurant.Update(this);
            }

            Console.WriteLine();
        }

        public double PricePerKilo
        {
            get { return pricePerKilo; }
            set
            {
                if(pricePerKilo != value)
                {
                    pricePerKilo = value;
                    Notify();
                }
            }
        }
    }
}
				
			

ما همچنین به چند موضوع مشخص (ConcreteSubjects) نیاز داریم که قیمت محصولات خاص را نشان دهند، مثل Cucumber و Mutton.

				
					namespace Observer.Subjects
{
    /// <summary>
    /// A ConcreteSubject class
    /// </summary>
    public class Cucumbers : Goods
    {
        public Cucumbers(double pricePerKilo) : base(pricePerKilo)
        {
        }
    }
}
				
			
				
					namespace Observer.Subjects
{
    /// <summary>
    /// A ConcreteSubject class
    /// </summary>
    public class Mutton : Goods
    {
        public Mutton(double pricePerKilo) : base(pricePerKilo)
        {
        }
    }
}
				
			

حالا می‌توانیم شرکت‌کننده ناظر (Observer) خود را تعریف کنیم. به یاد داشته باشید که رستوران‌ها می‌خواهند قیمت سبزیجات را مشاهده کنند، بنابراین ناظر ما به طور طبیعی یک رابط به نام IRestaurant خواهد بود، و این رابط باید متدی را تعریف کند که از طریق آن پیاده‌کنندگان آن بتوانند به‌روزرسانی شوند:

				
					using Observer.Subjects;

namespace Observer.Observers
{
    public interface IRestaurant
    {
        public void Update(Goods goods);
    }
}
				
			

در نهایت، به یک کلاس ConcreteObserver نیاز داریم که نمایانگر رستوران‌های خاص باشد. این کلاس باید متد Update() را از IRestaurant پیاده‌سازی کند:

				
					using Observer.Subjects;

namespace Observer.Observers
{
    /// <summary>
    /// The ConcreteObserver class
    /// </summary>
    public class Restaurant : IRestaurant
    {
        private string name;
        private Dictionary<Goods, double> goodsThresholds;

        public Restaurant(string name)
        {
            this.name = name;
            goodsThresholds = new Dictionary<Goods, double>();
        }

        public void AddGoodsThreshold(Goods good, double threshold)
        {
            goodsThresholds.Add(good, threshold);
        }

        public void Update(Goods goods)
        {
            Console.WriteLine($"Notified {name} of {goods.GetType().Name}'s price change to {Math.Round(goods.PricePerKilo, 2)} per kilo");

            if (goods.PricePerKilo < goodsThresholds[goods])
                Console.WriteLine($"{name} wants to buy some {goods.GetType().Name}!");
        }
    }
}
				
			

رستوران‌ها فقط زمانی کالاها را خریداری می‌کنند که قیمت به زیر یک مقدار آستانه مشخص کاهش پیدا کند، که این مقدار برای هر رستوران متفاوت است.

برای جمع‌بندی همه این موارد، در متد Main() می‌توانیم چند رستوران تعریف کنیم که می‌خواهند قیمت خیار و گوشت گوسفند را مشاهده کنند، و سپس قیمت را نوسان دهیم:

				
					using Observer.Observers;
using Observer.Subjects;

Restaurant bobsBurgers = new Restaurant("Bob's Burgers");
Restaurant krustyBurger = new Restaurant("Krusty Burger");
Restaurant carrotPalace = new Restaurant("Carrot Palace");

Cucumbers cucumbers = new Cucumbers(0.82);
Mutton mutton = new Mutton(12.9);

bobsBurgers.AddGoodsThreshold(cucumbers, 0.77);
bobsBurgers.AddGoodsThreshold(mutton, 10.8);

krustyBurger.AddGoodsThreshold(mutton, 8.2);

carrotPalace.AddGoodsThreshold(cucumbers, 0.89);

cucumbers.Attach(bobsBurgers);
cucumbers.Attach(carrotPalace);

mutton.Attach(krustyBurger);
mutton.Attach(bobsBurgers);

// Create price fluctuation
cucumbers.PricePerKilo = 0.78;
cucumbers.PricePerKilo = 0.65;
cucumbers.PricePerKilo = 1.02;

mutton.PricePerKilo = 12.0;
mutton.PricePerKilo = 10.2;
mutton.PricePerKilo = 6.81;

Console.ReadKey();
				
			

اگر برنامه را اجرا کنیم، مشاهده خواهیم کرد که با تغییر قیمت‌ها، رستوران‌ها مطلع می‌شوند و اگر قیمت به زیر آستانه مشخص‌شده برای هر رستوران کاهش یابد، آن رستوران تصمیم می‌گیرد سفارش خود را ثبت کند. خروجی برنامه به صورت زیر خواهد بود:

خروجی ناظر

همان‌طور که می‌بینید، موضوعات (مانند Cucumbers و Mutton) به‌صورت خودکار ناظران، یعنی رستوران‌ها را از تغییر قیمت‌ها آگاه می‌کنند. این ناظران سپس می‌توانند تصمیم بگیرند که با این اطلاعات چه کاری انجام دهند.

تفاوت بین الگوی Mediator و Observer

تفاوت بین الگوی Mediator (میانجی) و Observer (ناظر) اغلب مبهم است. در بیشتر موارد، می‌توانیم هر یک از این الگوها را پیاده‌سازی کنیم، اما در برخی موارد می‌توانیم هر دو را به‌کار بگیریم.

هدف اصلی الگوی Mediator حذف وابستگی‌ها میان مجموعه‌ای از اجزاء است. این اجزاء به جای وابستگی مستقیم به یکدیگر، به یک شیء میانجی وابسته می‌شوند.
از طرف دیگر، هدف اصلی الگوی Observer ایجاد ارتباطات پویا و یک‌طرفه بین اشیاء است، به‌گونه‌ای که برخی اشیاء به‌عنوان زیرمجموعه یا پیرو اشیاء دیگر عمل می‌کنند.

یکی از پیاده‌سازی‌های متداول الگوی Mediator به الگوی Observer متکی است. در این حالت، میانجی نقش ناشر (Publisher) را ایفا می‌کند و اجزاء به‌عنوان مشترکین (Subscribers) به رویدادهای میانجی عمل می‌کنند. هنگامی که میانجی به این شکل پیاده‌سازی می‌شود، بسیار شبیه به الگوی Observer به نظر می‌رسد.

وقتی شک دارید که الگوی پیاده‌شده Mediator است یا Observer، به یاد داشته باشید که Mediator می‌تواند به شکل متفاوتی پیاده‌سازی شود. برای مثال، می‌توانید تمام اجزاء را به‌طور دائمی به یک شیء میانجی متصل کنید. چنین پیاده‌سازی شبیه الگوی Observer نخواهد بود، اما همچنان یک نمونه از الگوی Mediator محسوب می‌شود.

اگر برنامه‌ای دارید که تمام اجزاء آن ناشر هستند و امکان ایجاد ارتباطات پویا بین یکدیگر را فراهم می‌کنند، در واقع مجموعه‌ای توزیع‌شده از Observerها دارید، زیرا هیچ شیء میانجی مرکزی وجود ندارد.

مزایا و معایب الگوی طراحی Observer

مزایا

  • امکان اضافه کردن کلاس‌های مشترک جدید بدون نیاز به تغییر در کد ناشر (رعایت اصل Open/Closed)
  • امکان ایجاد روابط بین اشیاء در زمان اجرا فراهم است.

معایب

  • هیچ ترتیب خاصی برای اطلاع‌رسانی به مشترکین وجود ندارد.

ارتباط الگوی طراحی Observer با سایر الگوها

  • Chain of Responsibility، Command، Mediator و Observer، الگوهایی هستند که به روش‌های مختلفی به ارتباط بین فرستنده‌ها و گیرنده‌های درخواست‌ها می‌پردازند:
    • الگوی طراحی Chain of Responsibility درخواست را به‌صورت زنجیره‌ای و به ترتیب، میان گیرنده‌ها منتقل می‌کند تا یکی از آنها آن را پردازش کند.
    • الگوی طراحی Command کانال‌های ارتباطی یک‌طرفه بین فرستنده‌ها و گیرنده‌ها ایجاد می‌کند.
    • الگوی طراحی Mediator ارتباطات مستقیم بین فرستنده‌ها و گیرنده‌ها را حذف می‌کند و آنها را مجبور می‌کند از طریق یک شیء میانجی با هم ارتباط برقرار کنند.
    • الگوی طراحی Observer به گیرنده‌ها اجازه می‌دهد به‌صورت پویا در دریافت یا لغو دریافت درخواست‌ها مشترک شوند.

نتیجه‌گیری

در این مقاله، الگوی Observer را بررسی کردیم، زمان مناسب برای استفاده از آن را توضیح دادیم و به مزایا و معایب این الگوی طراحی پرداختیم. همچنین ارتباط الگوی Observer با سایر الگوهای طراحی کلاسیک را مورد بررسی قرار دادیم.

الگوی Observer به دنبال این است که به اشیاء ناظر (Observer) این امکان را بدهد که به‌طور خودکار از تغییرات وضعیت یک کلاس موضوع (Subject) مطلع شوند و احتمالاً وضعیت خود را تغییر دهند. به زبان ساده، هر زمان که موضوع تغییر کند، ناظران از این تغییر آگاه می‌شوند.

الگوی طراحی Observer در بسیاری از موارد مفید و بسیار انعطاف‌پذیر است، به شرطی که به‌درستی استفاده شود. با این حال، باید توجه داشت که الگوی Observer، مانند سایر الگوهای طراحی ارائه‌شده توسط گروه “Gang of Four”، یک راه‌حل همه‌جانبه یا جادویی نیست. این بر عهده مهندسان است که زمان مناسب استفاده از یک الگوی خاص را در نظر بگیرند. این الگوها زمانی مفید هستند که به‌عنوان ابزاری دقیق استفاده شوند، نه به‌عنوان یک چکش بزرگ.

©دوات با هدف دسترس‌پذیر کردن دانش انگلیسی در حوزه صنعت نرم‌افزار وجود آمده است. در این راستا از هوش مصنوعی برای ترجمه گلچینی از مقالات مطرح و معتبر استفاده می‌شود. با ما در تماس باشید و انتقادات و پیشنهادات خود را از طریق صفحه «تماس با ما» در میان بگذارید.