دانلود سورس
دانلود کد و راهنما
- Invoker: از Command میخواهد که درخواست را اجرا کند.
- Command: اطلاعاتی را در رابطه با action، به همراه دارد و هم چنین bind کردن آن به receiver؛ همراه با فراخوانی کردن عملیات مربوطه بر روی command.
- Reciever: میداند که چگونه عملیات مرتبط با command مورد نظر را انجام دهد.
- Client: یک command را ایجاد میکند و receiver را مشخص میکند؛ چه کسی قرار است این command را دریافت کند.
class Command { execute() {}; } //TurnOnPrinter command class TurnOnPrinter extends Command { constructor(printingMachine) { super(); this.printingMachine = printingMachine; this.commandName = "turn on" } execute() { this.printingMachine.turnOn(); } } //TurnOffPrinter command class TurnOffPrinter extends Command { constructor(printingMachine) { super(); this.printingMachine = printingMachine; this.commandName = "turn off" } execute() { this.printingMachine.turnOff(); } } //Print command class Print extends Command { constructor(printingMachine) { super(); this.printingMachine = printingMachine; this.commandName = "print" } execute() { this.printingMachine.print(); } } //Invoker class PrinterControlPanel { pressButton(command) { console.log(`Pressing ${command.commandName} button`); command.execute(); } } //Reciever: class PrintingMachine { turnOn() { console.log('Printing machine has been turned on'); } turnOff() { console.log('Printing machine has been turned off'); } print(){ console.log('The printer is printing your document') } } const printingMachine = new PrintingMachine(); const turnOnCommand = new TurnOnPrinter(printingMachine); const turnOffCommand = new TurnOffPrinter(printingMachine); const printCommand = new Print(printingMachine) const controlPanel = new PrinterControlPanel(); controlPanel.pressButton(turnOnCommand); controlPanel.pressButton(turnOffCommand); controlPanel.pressButton(printCommand);
class PrintingMachine { turnOn() { console.log('Printing machine has been turned on'); } turnOff() { console.log('Printing machine has been turned off'); } print(){ console.log('The printer is printing your document') } }
- turnOn: روشن کردن ماشین (printer)
- turnOff: خاموش کردن ماشین (printer)
- print: چاپ کردن صفحه با استفاده از ماشین (printer)
class TurnOnPrinter extends Command {/*code*/} class TurnOffPrinter extends Command {/*code*/} class Print extends Command {/*code*/}
class Command { execute() {}; }
class TurnOnPrinter extends Command { constructor(printingMachine) { super(); this.printingMachine = printingMachine; this.commandName = "turn on" } execute() { this.printingMachine.turnOn(); } }
class TurnOffPrinter extends Command { //code... this.commandName = "turn off" //code.. } class Print extends Command { //code... this.commandName = "print" //code.. }
class TurnOffPrinter extends Command { //code... execute() { this.printingMachine.turnOff(); } } class Print extends Command { //code... execute() { this.printingMachine.print(); } }
class PrinterControlPanel { pressButton(command) { console.log(`Pressing ${command.commandName} button`); command.execute(); } }
controlPanel.pressButton(turnOnCommand);
Printing machine has been turned on
- اگر میخواهید یک صف درست کنید و درخواستها را در زمانهای متفاوتی اجرا کنید.
- اگر میخواهید عملیاتی از قبیل reset و undo را انجام بدهید.
- اگر میخواهید تاریخچهای از درخواستهای ایجاد شده را داشته باشید.
class Visitor { visit(item){} } class BookVisitor extends Visitor { visit(book) { var cost=0; if(book.getPrice() > 50) { cost = book.getPrice()*0.50 } else{ cost = book.getPrice() } console.log("Book name: "+ book.getName() + "\n" + "ID: " + book.getID() + "\n" + "cost: "+ cost); return cost; } } class Book{ constructor(id,name,price){ this.id = id this.name = name this.price = price } getPrice(){ return this.price } getName(){ return this.name } getID(){ return this.id } accept(visitor){ return visitor.visit(this) } } var visitor = new BookVisitor() var book1 = new Book("#1234","lordOftheRings",80) book1.accept(visitor)
class Book{ constructor(id,name,price){ this.id = id this.name = name this.price = price } //code... }
- id
- name
- price
getPrice(){ return this.price } getName(){ return this.name } getID(){ return this.id }
accept(visitor){ return visitor.visit(this) }
class Visitor { visit(item){} }
class BookVisitor extends Visitor { visit(book) { var cost=0; if(book.getPrice() > 50) { cost = book.getPrice()*0.50 } else{ cost = book.getPrice() } console.log("Book name: "+ book.getName() + "\n" + "ID: " + book.getID() + "\n" + "cost: "+ cost); return cost; } }
- زمانیکه نیاز است عملیاتی مشابه، بر روی شیءهای متفاوتی از یک data structure انجام شود.
- زمانیکه نیاز است عملیاتی خاص، بر روی شیءهای متفاوتی از data structure انجام شود.
- زمانیکه میخواهید توسعه پذیری را برای کتابخانهها (libraries) یا فریم ورکها (frameworks) اضافه کنید.
«حالت» یا state، شیءای است، حاوی اطلاعاتی که برنامه با آن سر و کار دارد. بنابراین مدیریت حالت، روشی است برای ردیابی و مدیریت دادههای مورد استفادهی در برنامه و تقریبا تمام برنامهها، به نحوی به آن نیاز دارند. هر کامپوننت در Blazor، دارای state خاص خودش است و این state از سایر کامپوننتها کاملا مستقل و ایزولهاست. این مورد با بزرگتر شدن برنامه و برقراری ارتباط بین کامپوننتها، مشکل ایجاد میکند. برای مثال اگر قرار است در منوی بالای سایت، تعداد محصولات موجود در سبد خرید یک شخص را نمایش دهیم، این تعداد، حاصل تعامل او با چندین کامپوننت مجزا خواهد بود که اینها الزاما در یک سلسه مراتب هم قرار نمیگیرند و به سادگی نمیتوان اطلاعات را به صورت آبشاری در بین آنها به اشتراک گذاشت. به همین جهت نیاز به روشی برای مدیریت حالت و به اشتراک گذاری آن در بین کامپوننتهای مختلف برنامه وجود دارد و خوشبختانه چون Blazor به همراه یک سیستم تزریق وابستگیهای توکار است، پیاده سازی یک چنین مدیریت کنندهای، سادهاست.
استفاده از الگوی Observer جهت مدیریت حالت برنامههای Blazor
زمانیکه همانند تصویر فوق با یک کامپوننت کار میکنیم، کاربر همواره کارش از تعامل با یک View آغاز میشود. این تعامل سبب صدور رخدادهایی میشود که این رخدادها، حالت و state کامپوننت را تغییر میدهند. تغییر حالت کامپوننت نیز بلافاصله سبب بهروز رسانی View میشود. در این مثال، حالت کامپوننت، داخل همان کامپوننت نگهداری میشود؛ مانند فیلدهایی که در قسمت code@ یک کامپوننت Blazor تعریف میکنیم و محدود به همان کامپوننت هستند.
با بزرگتر شدن برنامه، زمانی خواهد رسید که نیاز است حالت یک کامپوننت را با کامپوننتهای دیگر به اشتراک گذاشت. در این حالت باید این state را از داخل کامپوننت مدنظر استخراج کرد و در جائی دیگر قرار داد که عموما به آن state store گفته میشود:
در تصویر فوق، در بالای آن یک state store را داریم که محل نگهداری و ذخیره سازی حالت اشتراکی بین کامپوننتها است. سپس برای نمونه دو کامپوننت دیگر را داریم که رابطهی بین آنها، همان رابطهی مثلثی است که در تصویر اول این مطلب مشاهده کردیم. برای مثال در اثر تعامل کاربری با View کامپوننت 1، رخدادی صادر خواهد شد. مدیریت این رخداد، سبب تغییر state خواهد شد، اما اینبار این state دیگر داخل کامپوننت 1 قرار ندارد؛ بلکه داخل state store است و این store پس از آگاه شدن از تغییر وضعیت خود، دو کامپوننتی را که از آن تغدیه میکنند، جهت به روز رسانی Viewهایشان، مطلع میکند. همین چرخه در مورد کامپوننت 2 نیز برقرار است. اگر تعاملی با آن صورت گیرد، در نهایت اثر آن به هر دو کامپوننت متصل به state store اشتراکی، اطلاع رسانی میشود تا Viewهای هر دوی آنها به روز رسانی شوند. الگویی را که در اینجا مشاهده میکنید، در اصل یک الگوی Observer است:
در الگوی مشاهدهگر، یک Subject را داریم که تعداد زیادی Observer، مشترک آن هستند. در این مثال ما، Subject، همان State Store است و Observerها دقیقا همان کامپوننتهای مشترک به آن. Observerها به تغییرات Subject گوش فرا داده و بلافاصله بر اساس آن واکنش مناسبی را نشان میدهند.
پیاده سازی الگوی Observer جهت مدیریت حالت برنامههای Blazor
زمانیکه یک برنامهی متداول Blazor را توسط قالب پیشفرض آن ایجاد میکنیم، به همراه یک کامپوننت Counter است:
@page "/counter" <h1>Counter</h1> <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } }
بنابراین در قدم اول نیاز به یک State Store اشتراکی را داریم که بتوانیم توسط آن، مقدار جاری currentCount را ذخیره کرده و سپس تغییرات آنرا جهت به روز رسانی دو View (در کامپوننتهای Counter و NavMenu)، به مشترکین آن اطلاع رسانی کنیم. به همین جهت ابتدا پوشهی جدید Stores را در ریشهی پروژهی Blazor ایجاد میکنیم. نام این پوشه، از این جهت یک اسم جمع است که یک برنامه بنابر نیاز خودش میتواند چندین State Store را داشته باشد. سپس داخل این پوشه، پوشهی دیگری را به نام CounterStore، ایجاد میکنیم.
در اینجا در ابتدا شیء حالت مدنظر را ایجاد میکنیم که برای نمونه بر اساس نیاز برنامه و این مثال، از مقدار نهایی کلیک بر روی دکمهی شمارشگر تشکیل میشود:
namespace BlazorStateManagement.Stores.CounterStore { public class CounterState { public int Count { get; set; } } }
using System; namespace BlazorStateManagement.Stores.CounterStore { public interface ICounterStore { void DecrementCount(); void IncrementCount(); CounterState GetState(); void AddStateChangeListener(Action listener); void BroadcastStateChange(); void RemoveStateChangeListener(Action listener); } }
using System; namespace BlazorStateManagement.Stores.CounterStore { public class CounterStore : ICounterStore { private readonly CounterState _state = new(); private Action _listeners; public CounterState GetState() { return _state; } public void IncrementCount() { _state.Count++; BroadcastStateChange(); } public void DecrementCount() { _state.Count--; BroadcastStateChange(); } public void AddStateChangeListener(Action listener) { _listeners += listener; } public void RemoveStateChangeListener(Action listener) { _listeners -= listener; } public void BroadcastStateChange() { _listeners.Invoke(); } } }
- مخزن حالت پیاده سازی شدهی بر اساس الگوی مشاهدهگر، نیاز دارد تا بتواند لیست مشاهدهگرها را ثبت کند. به همین جهت به همراه متدهای AddStateChangeListener جهت ثبت یک مشاهدهگر جدید و RemoveStateChangeListener، جهت حذف مشاهدهگری از لیست موجود است.
- همچنین الگوی مشاهدهگر باید بتواند تغییرات صورت گرفتهی در حالتی را که نگهداری میکند (CounterState در اینجا)، به مشترکین خود اطلاع رسانی کند. اینکار را توسط متد BroadcastStateChange انجام میدهد. هر زمانیکه این متد فراخوانی شود، Actionهایی که به صورت پارامتر به متد AddStateChangeListener ارسال شدهاند، به صورت خودکار اجرا خواهند شد. این کار سبب میشود تا بتوان منطق خاصی را مانند به روز رسانی UI، در سمت کامپوننتهای مشترک به این مخزن، پیاده سازی کرد.
- در اینجا همچنین متدهایی برای افزایش و کاهش مقدار Count را نیز به همراه اطلاع رسانی به مشترکین، مشاهده میکنید.
پس از این تعریف نیاز است سرویس Store ایجاد شده را به برنامه معرفی کرد:
namespace BlazorStateManagement.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); //... builder.Services.AddScoped<ICounterStore, CounterStore>(); //... } } }
تغییر کامپوننتهای برنامه برای استفاده از سرویس ICounterStore
پس از معرفی سرویس ICounterStore به سیستم تزریق وابستگیهای برنامه، جهت سهولت استفادهی از آن، در ابتدا فضای نام آنرا به فایل سراسری Client\_Imports.razor اضافه میکنیم:
@using BlazorStateManagement.Stores.CounterStore
@page "/counter" @implements IDisposable @inject ICounterStore CounterStore <h1>Counter</h1> <p>Current count: @CounterStore.GetState().Count</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { protected override void OnInitialized() { base.OnInitialized(); CounterStore.AddStateChangeListener(UpdateView); } private void IncrementCount() { CounterStore.IncrementCount(); } private void UpdateView() { StateHasChanged(); } public void Dispose() { CounterStore.RemoveStateChangeListener(UpdateView); } }
- در اینجا در ابتدا سرویس ICounterStore، به کامپوننت تزریق شدهاست.
- سپس در متد رویدادگران آغازین OnInitialized، با استفاده از متد AddStateChangeListener، مشترک سرویس مخزن حالت شمارشگر شدهایم.
- همواره جهت پاکسازی کد و عدم اشتراک بیش از اندازهی به یک مخزن حالت، نیاز است در پایان کار یک کامپوننت، با پیاده سازی implements IDisposable@، کار حذف اشتراک را انجام دهیم. در غیراینصورت هربار که کامپوننت بارگذاری میشود، یک اشتراک جدید از این کامپوننت، به مخزن حالتی که طول عمر Singleton دارد، اضافه خواهد شد که نشانی از نشتی حافظهاست.
- دو قسمت دیگر را هم تغییر دادهایم. اینبار با استفاده از متد ()GetState، این Count اشتراکی را نمایش میدهیم و همچنین عمل به روز رسانی State را هم توسط متد IncrementCount انجام دادهایم.
در ادامه کامپوننت Client\Shared\NavMenu.razor را نیز جهت نمایش مقدار جاری Count، به صورت زیر به روز رسانی میکنیم:
@inject ICounterStore CounterStore <li class="nav-item px-3"> <NavLink class="nav-link" href="counter"> <span class="oi oi-plus" aria-hidden="true"></span> Counter: @CounterStore.GetState().Count </NavLink> </li> @code { protected override void OnInitialized() { base.OnInitialized(); CounterStore.AddStateChangeListener(() => StateHasChanged()); } // ... }
- در اینجا نیز در ابتدا سرویس ICounterStore، به کامپوننت تزریق شدهاست.
- سپس در متد رویدادگران آغازین OnInitialized، با استفاده از متد AddStateChangeListener، مشترک سرویس مخزن حالت شمارشگر شدهایم و هربار که متد BroadcastStateChange ای توسط یکی از کامپوننتهای متصل به مخزن حالت فراخوانی میشود (برای مثال در انتهای متد IncrementCount خود سرویس)، سبب اجرای Action آن که در اینجا StateHasChanged است، خواهد شد. فراخوانی StateHasChanged، کار اطلاع رسانی به UI، جهت رندر مجدد را انجام میدهد. به این ترتیب مقدار جدید Count توسط CounterStore.GetState().Count@ در منو نیز ظاهر خواهد شد:
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorStateManagement.zip
فواید استفاده از فریمورک Seneca
- فریمورک Seneca کدنویسی برای ایجاد درخواستها، ارسال پاسخ به درخواستهای رسیده و تبدیل دادهها را که از روتینهای هر سرویسی میتوانند باشند، سادهتر میکند.
- فریمورک Seneca با معرفی ایده Actionها و Pluginها که جلوتر توضیح داده خواهند شد، روند تبدیل کامپوننتهای یک برنامه Monolithic را به نوع سرویسی، تسهیل میکند. روندی که به صورت عادی میتواند کاری طاقت فرسا باشد و نیاز به Refactoring زیادی دارد.
- سرویسهای نوشته شده با زبانهای برنامهنویسی مختلف یا فریمورکهای متفاوت میتوانند با سرویسهای نوشته شده توسط فریمورک Seneca در ارتباط و تبادل باشند.
نصب فریمورک Seneca
{ "name": "seneca-example", "dependencies": { "seneca": "0.6.5", "express": "latest" } }
var seneca = require("seneca")();
Actionها
seneca.add({role: "accountManagement", cmd: "login"}, function(args, respond){ }); seneca.add({role: "accountManagement", cmd: "register"}, function(args, respond){ });
seneca.act({role: "accountManagement", cmd: "register", username: "parham", password: "12345!"}, function(error, response){ }); seneca.act({role: "accountManagement", cmd: "login", username: "parham", password: "12345!"}, function(error, response){ });
Pluginها
function account(options){ this.add({init: "account"}, function(pluginInfo, respond){ console.log(options.message); respond(); }) this.add({role: "accountManagement", cmd: "login"}, function(args, respond){ }); this.add({role: "accountManagement", cmd: "register"}, function(args, respond){ }); } seneca.use(account, {message: "Plugin Added"});
module.exports = function(options){ this.add({init: "account"}, function(pluginInfo, respond){ console.log(options.message); respond(); }) this.add({role: "accountManagement", cmd: "login"}, function(args, respond){ }); this.add({role: "accountManagement", cmd: "register"},function(args, respond){ }); return "account"; } seneca.use("./account.js", {message: "Plugin Added"});
Serviceها
var seneca = require("seneca")(); seneca.use("./account.js", {message: "Plugin Added"}); seneca.listen({port: "9090", pin: {role: "accountManagement"}});
seneca.client({port: "9090", pin: {role: "accountManagement"}});
در اینجا هم موارد port و pin اختیاری هستند. اگر سرویسی که میخواهیم ثبت کنیم در سرور دیگری باشد، بایستی خاصیت host و با مقدار آدرس IP سرور مورد نظر در زمان ثبت، اعمال شود. سوالی که باقی میماند این است که یک سرویس چطور Actionی از یک سرویس دیگر را فراخوانی میکند؟
در بخش Actionها آمد که برای فراخوانی یک Action از متد act، از نمونهی Seneca استفاده میکنیم. رفتار Seneca به این صورت است که ابتدا بر اساس امضای Action درخواست شده، Actionهای محلی را که به سرویس جاری اضافه شدهاند، جستجو میکند. اگر تطبیقی نیافت به سراغ Actionهای ثبت شده خارجی که دارای خاصیت pin هستند، خواهد رفت و در نهایت اگر آنجا هم موردی نیافت، برای تک تک سرویسهایی که آنها را ثبت کرده، اما خاصیت pin را ندارند، درخواستی را ارسال میکند.
برای اطلاعات بیشتر به بخش مستندات فریمورک Seneca رجوع کنید.
معماری میکروسرویسها
معماری Monolithic چیست؟
مشکلات معماری Monolithic
- در معماری Monolithic زمانیکه ترافیک برنامه در سمت سرور افزایش پیدا میکند، باید برای پاسخگویی، اندازه را افزایش داد. یعنی باید برنامه تحت وب خود را بر روی سرورهای مختلف مجددا اجرا نمود. بخشی به نام Load Balancer، وظیفه توزیع درخواستها را به سرورهای مختلف که بر روی هر یک، یک نسخه از برنامه در حال اجرا است، به عهده دارد. بر اساس توضیحی که از این معماری ارایه شد، در هر یک از این اجراها، کل برنامه با تمام متعلقاتی که دارد، فارغ از اینکه به همه آنها نیاز است یا نه، از منابع سرور استفاده میکند.
- در معماری Monolithic برنامهها بر اساس یک زبان برنامهنویسی مشخص، برای یک فریم ورک مشخص نوشته میشوند. این برنامهها اصطلاحا چند سکویی نیستند و کامپوننتهای نوشته شده برای آنها فقط در فریم ورک جاری قابل استفاده مجدد هستند.
- ممکن است برای هر تغییر ریز و درشت در برنامههای این معماری، نیاز به Build و Deploy مجدد کل برنامه باشد که احتمال از دسترس خارج شدن برنامه هم وجود دارد.
- اگر بخشی از برنامه از کار بیافتد، ممکن است باعث از کار افتادن کل برنامه یا بخشهایی از آن شود.
معماری Microservices
ارزش معماری Microservices
- از آنجایی که سرویسها از طریق زبان مشترک شبکه با یکدیگر در ارتباط هستند، میشود آنها را با زبانهای برنامهنویسی مختلف و بر روی فریمفرکهای متفاوت نوشت.
- بدیهی است که با این معماری، هر سرویس را میشود به صورت جداگانه ایجاد کرد و تغییر داد که باعث سرعت در به روزرسانی و فرآیند گسترش برنامه میشود.
- مانیتور کردن سرویسها سادهتر خواهد بود. از آنجایی که هر سرویس به صورت یک پردازش جداگانه اجرا خواهد شد، تعیین اینکه هر سرویس از چه منابعی و به چه اندازهای استفاده میکند، آسانتر خواهد بود.
- از آنجایی که این سرویسها از طریق شبکه در تبادل هستند، میشود از آنها در سایر برنامهها مجدداً استفاده کرد.
افزایش یک سرویس خاص
مشکلات معماری Microservices
- از آنجایی که برنامههای سمت سرور نوشته شده با این معماری به سرویسهای مختلفی تقسیم میشوند، گسترش و تنظیمات آنها میتواند کاری وقت گیر و طاقت فرسایی باشد.
- از آنجایی که ارتباط بین سرویسها در بستر شبکه انجام میشود، انتظار کندی عملکرد سرویسها دور از ذهن نیست.
- به دلیل ارتباطات شبکهای، احتمال آسیب پذیریهای امنیتی در این نوع برنامهها بیشتر است.
- نوشتن سرویسهایی که در بستر شبکه با سایر سرویسها در ارتباط هستند سختی و مشکلات خود را دارد. برنامهنویس در این شرایط، درگیر برقراری ارتباط، رمزگذاری دادهها در صورت نیاز و تبدیل آنها میشود.
- به دلیل مجزا بودن بخشهای مختلف برنامه، مانیتور کردن و ردیابی عملکرد سرویسها، یکی از کارهای اصلی توسعه دهنده یا استفاده کننده از برنامه است.
- در مجموع سرعت برنامههای نوشته شده با معماری Microservices کندتر از برنامههای نوشته شده با معماری Monolithic است. دلیل آن محیط اجرایی برنامهها است. برنامههایی با معماری Monolithic بر روی حافظه سرور پردازش میشوند.
چه زمانی از معماری Microservices استفاده کنیم؟
مدیریت دادهها:
پیادهسازی معماری Microservicesها توسط فریمفرک Seneca
Feature Toggle
"Feature Toggling" is a set of patterns which can help a team to deliver new functionality to users rapidly but safely
public interface IFeatureToggle { bool FeatureEnabled {get;} }
class ShowMessageToggle : IFeatureToggle
{
public bool FeatureEnabled {
get{
return bool.Parse(ConfigurationManager.AppSettings["ShowMessageEnabled"]);
}
}
class Program { static void Main(string[] args) { var toggle = new ShowMessageToggle(); if (toggle.FeatureEnabled) { Console.WriteLine("This feature is enabled") } else { Console.WriteLine("This feature is disabled"); } } }
Install-Package FeatureToggle
MyAwesomeFeature : SimpleFeatureToggle {}
<add key="MyAwesomeFeature " value="true" />
if (!myAwesomeFeature.FeatureEnabled) { // code to disable stuff (e.g. UI buttons, etc) }
یک لامپ و سوئیچ برق را درنظر بگیرید. زمانیکه لامپ مشاهده میکند سوئیچ برق در حالت روشن قرار گرفتهاست، روشن خواهد شد و برعکس. در اینجا به سوئیچ، subject و به لامپ، observer گفته میشود. هر زمان که حالت سوئیچ تغییر میکند، از طریق یک callback، وضعیت خود را به observer اعلام خواهد کرد. علت استفاده از callbackها، ارائه راهحلهای عمومی است تا بتواند با انواع و اقسام اشیاء کار کند. به این ترتیب هر بار که شیء observer از نوع متفاوتی تعریف میشود (مثلا بجای لامپ یک خودرو قرار گیرد)، نیازی نخواهد بود تا subject را تغییر داد.
عموما به شیءایی که قرار است وضعیت را مشاهده یا رصد کند، Observer گفته میشود و به شیءایی که قرار است وضعیت آن رصد شود Observable یا Subject گفته میشود.
بد نیست بدانید این الگو یکی از کلیدیترین بخشهای معماری لایه بندی MVC نیز میباشد.
همچنین این نکته حائز اهمیت است که این الگو ممکن است باعث نشتی حافظه هم شود و به این مشکل Lapsed Listener Problem میگویند. یعنی یک listener وجود دارد که تاریخ آن منقضی شده، ولی هنوز در حافظه جا خوش کردهاست. این مشکل برای زبانهای شیءگرایی که با سیستمی مشابه GC پیاده سازی میشوند، رخ میدهد. برای جلوگیری از این حالت، برنامه نویس باید این مشکل را با رجیستر کردنها و عدم رجیستر یک شنوده، در مواقع لزوم حل کند. در غیر این صورت این شنونده بی جهت، یک ارتباط را زنده نگه داشته و حافظهی منبع را به هدر میدهد.
مثال: ما یک کلید داریم که سه کلاس RedLED،GreenLED و BlueLED قرار است آن را مشاهده و وضعیت کلید را رصد کنند.
برای پیاده سازی این الگو، ابتدا یک کلاس انتزاعی را با نام Observer که دارای متدی به نام Update است، ایجاد میکنیم. متغیر از نوع کلاس Observable را بعدا ایجاد میکنیم:
public abstract class Observer { protected Observable Observable; public abstract void Update(); }
public class Observable { private readonly List<Observer> _observers = new List<Observer>(); public void Attach(Observer observer) { _observers.Add(observer); } public void Dettach(Observer observer) { _observers.Remove(observer); } public void NotifyAllObservers() { foreach (var observer in _observers) { observer.Update(); } } }
حال کلاس Switch را با ارث بری از کلاس Observable مینویسیم:
public class Switch:Observable { private bool _state; public bool ChangeState { set { _state = value; NotifyAllObservers(); } get { return _state; } } }
برای هر سه چراغ، رنگی هم داریم:
public class RedLED:Observer { private bool _on = false; public override void Update() { _on = !_on; Console.WriteLine($"Red LED is {((_on) ? "On" : "Off")}"); } }
public class GreenLED:Observer { private bool _on = false; public override void Update() { _on = !_on; Console.WriteLine($"Green LED is {((_on) ? "On" : "Off")}"); } }
public class BlueLED:Observer { private bool _on = false; public override void Update() { _on = !_on; Console.WriteLine($"Blue LED is {((_on) ? "On" : "Off")}"); } }
var greenLed=new GreenLED(); var redLed=new RedLED(); var blueLed=new BlueLED(); var switchKey=new Switch(); switchKey.Attach(greenLed); switchKey.Attach(redLed); switchKey.Attach(blueLed); switchKey.ChangeState = true; switchKey.ChangeState = false;
Green LED is On Red LED is On Blue LED is On Green LED is Off Red LED is Off Blue LED is Off
در این مثال ما سه گروه Manager,Employee و Worker را داریم که میخواهیم با استفاده از این الگو برای هر کدام به طور جداگانه، حقوق و دستمزد و اضافه کاری را محاسبه کنیم. با توجه به اینکه فرمول هر یک جداست و این احتمال نیز وجود دارد که هر کدام خواص مخصوص به خود را داشته باشند که در دیگری وجود ندارد و در آینده این احتمال میرود که سمت جدید یا دستورالعملهای جدیدی اضافه شود، بهترین راه حل استفاده از الگوی Visitor است.
الگوی visitor دو بخش مهم دارد؛ یکی Element که قرار است کار روی آن انجام شود. مثل سمتهای مختلف و دیگری Visitor هست که همان دستورالعملهایی چون محاسبه حقوق و دستمزد و ... است که روی المانها صورت میگیرد.
ابتدا برای هر کدام یک اینترفیس را با مشخصات زیر میسازیم:
public interface IElement { void Accept(IElementVisitor visitor); }
public interface IElementVisitor { void Visit(Manager manager); void Visit(Employee manager); void Visit(Worker manager); }
public class Manager: IElement { public int WorkingHour = 8; public int Wife = 1; public int Children = 3; public int OffDays = 6; public int OverHours = 12; public void Accept(IElementVisitor visitor) { visitor.Visit(this); } }
public class Employee: IElement { public int WorkingHour = 8; public int Wife = 1; public int Children = 3; public int OffDays = 6; public int OverHours = 12; public void Accept(IElementVisitor visitor) { visitor.Visit(this); } }
public class Worker:IElement { public int WorkingHour = 8; public int Wife = 1; public int Children = 3; public int OffDays = 6; public int OverHours = 12; public void Accept(IElementVisitor visitor) { visitor.Visit(this); } }
حال وقت آن رسیده تا از روی کلاس Visitor، برای حقوق، دستمزد و اضافه کاری، کلاسهای جدیدی را بسازیم:
class SalaryCalculator:IElementVisitor { public void Visit(Manager manager) { var salary = manager.WorkingHour*10000; salary += manager.Wife*25000; salary += manager.Children*20000; salary -= manager.OffDays*5000; Console.WriteLine("Manager's Salary is " + salary); } public void Visit(Employee employee) { var salary = employee.WorkingHour * 7000; salary += employee.Wife * 15000; salary += employee.Children * 10000; salary -= employee.OffDays * 6000; Console.WriteLine("Employee's Salary is " + salary); } public void Visit(Worker worker) { var salary = worker.WorkingHour * 6000; salary += worker.Wife * 5000; salary += worker.Children * 2000; salary -= worker.OffDays * 7000; Console.WriteLine("Worker's Salary is " + salary); } }
class WageCalculator:IElementVisitor { public void Visit(Manager manager) { var wage = manager.OverHours*30000; Console.WriteLine("Employee's wage is " + wage); } public void Visit(Employee employee) { var wage = employee.OverHours * 20000; Console.WriteLine("Employee's wage is " + wage); } public void Visit(Worker worker) { var wage = worker.OverHours * 15000; Console.WriteLine("Employee's wage is " + wage); } }
class FinancialSystem { private readonly IList<IElement> _elements; public FinanceSystem() { _elements=new List<IElement>(); } public void Attach(IElement element) { _elements.Add(element); } public void Detach(IElement element) { _elements.Remove(element); } public void Accept(IElementVisitor visitor) { foreach (var element in _elements) { element.Accept(visitor); } } }
بدنه اصلی:
IElement manager=new Manager(); IElement employee=new Employee(); IElement worker=new Worker(); var fine=new FinancialSystem(); fine.Attach(manager); fine.Attach(employee); fine.Attach(worker); fine.Accept(new SalaryCalculator()); fine.Accept(new WageCalculator());
Manager's Salary is 135000 Employee's Salary is 65000 Worker's Salary is 17000 Manager's wage is 360000 Employee's wage is 240000 Worker's wage is 180000