یک فنجان جاوا - دیزاین پترن ها - Observer

میتونین برای آشنایی با الگوهای طراحی (یا همون دیزاین پترن های) زبان جاوا به مقاله ی یک فنجان جاوا - دیزاین پترن ها Design Patterns مراجعه کنین.(همونطور که گفته شده این الگو زیرمجموعه ی الگوهای رفتاری (Behavioral) هست)الگوری Observerطبق تعریف ویکی پدیا:الگوی ناظر یا همان Observer یک الگوی طراحی نرم‌افزار است که در آن یک شی به نام موضوع (subject)، فهرست وابستگی‌هایش را با نام ناظران (observers) نگه می‌دارد و هرگونه تغییر در وضعیتش را به‌طور خودکار و معمولاً با صدا کردن یکی از روش‌های آن به اطلاع آن اشیا می‌رساند.در حقیقت سعی داریم یه الگوی اصطلاحاً رویدادمحور رو پیاده سازی بکنیم، یعنی هربار موردی (شئ، پارامتر یا ...) تغییر میکنه بتونیم بدون صدا زدن تابع یا روش خاصی، و بصورت خودکار از این موضوع مطلع بشیم. توی این الگو، اشیائی که منتظر اعلام تغییر وضعیت هستن رو Observer و یک شئ که به بقیه اشیا تغییر رو اعلام می‌کنه Subject می‌گیم.بریم اولین مثال رو ببینیم:اول یه کلاس abstract به اسم Observer بصورت زیر تعریف میکنیم:abstract class Observer { protected Subject subject; public abstract void update(); }همونطور که میبینیم این کلاس تابعی داره به اسم update میشه که abstractه و طبیعتاً هرکلاسی از این کلاس ارثبری کنه باید این تابع رو پیاده سازی بکنه. و یه شئ هم از کلاس Subject نگه میداره، این کلاس رو بصورت زیر تعریف میکنیم:class Subject { private List<Observer> observers = new ArrayList<>(); private int state; public void add(Observer o) { observers.add(o); } public int getState() { return state; } public void setState(int value) { this.state = value; execute(); } private void execute() { for (Observer observer : observers) { observer.update(); } } }این کلاس لیستی از Observer ها داره. تابعی با اسم add که یه Observer به لیست اضافه میکنه، تابع getState و setState که توابع setter و getter پارامتر state هستن. و در نهایت هم تابع execute که تابع update تمامی observerهای موجود توی لیست رو صدا میزنه.خب حالا سه تا کلاس تعریف میکنیم، کلاس HexObserver (تبدیل state به hex)، کلاس OctObserver (تبدیل عدد state به oct) و در نهایت کلاس BinObserver (تبدیل عدد state به باینری):class HexObserver extends Observer { public HexObserver(Subject subject) { this.subject = subject; this.subject.add(this); } public void update() { System.out.print(" " + Integer.toHexString(subject.getState())); } }class OctObserver extends Observer { public OctObserver(Subject subject) { this.subject = subject; this.subject.add( this ); } public void update() { System.out.print(" " + Integer.toOctalString(subject.getState())); } }class BinObserver extends Observer { public BinObserver(Subject subject) { this.subject = subject; this.subject.add(this); } public void update() { System.out.print(" " + Integer.toBinaryString(subject.getState())); } }هر سه تای این کلاس ها از کلاس Observer ارث بردن و تابع update رو با توجه به عملکرد خودشون پیاده سازی کردن. حالا برای تست قطعه کد زیر رو اجرا میکنیم (یه شئ از کلاس subject به اسم sub تعریف میکنیم، و اون رو به اشیا جدیدی که از کلاسای HexObserver و OctObserver و BinObserver میسازیم میدیم، و 5 عدد توی کنسول از کاربر میگیریم و اون رو به hex و oct و binary تبدیل میکنیم): Subject sub = new Subject(); // Client configures the number and type of Observers new HexObserver(sub); new OctObserver(sub); new BinObserver(sub); Scanner scan = new Scanner(System.in); for (int i = 0; i < 5; i++) { System.out.print("\nEnter a number: "); sub.setState(scan.nextInt()); }خروجی این قطعه کد بصورت زیره:Enter a number: 55 37 67 110111 Enter a number: 12 c 14 1100 Enter a number: -10 fffffff6 37777777766 11111111111111111111111111110110 Enter a number: 112 70 160 1110000 Enter a number: 5 5 5 101همونطور که متوجه شدیم تابع setState علاوه بر اینکه پارامتر state رو ست میکنه، تابع execute رو هم صدا میزنه و اون تابع کارش صدا زدن تابع update مربوط به همه ی observerهاست! به همین راحتی.خب بریم سراغ یه مثال دیگه:میخوایم از observer خودِ زبان جاوا استفاده کنیم. پس کلاس زیر رو مینویسیم (که چون Runnable رو implement کرده تابع run رو پیاده میکنیم که داخلش مادامی که کاربر رشته ای رو وارد کنه، اونو میگیره و با صدا زدن تابع setChanged از کلاس Observable میگه مقدار تغییر کرده و با صدا زدن notifyObservers ورودی که همون رشته ی response هست رو به observerها میفرسته)class EventSource extends Observable implements Runnable { public void run() { while (true) { String response = new Scanner(System.in).next(); setChanged(); notifyObservers(response); } } }خب حالا یه شئ از این کلاس میسازیم:EventSource eventSource = new EventSource();و با ساختار زیر میتونیم به observer بهش اضافه کنیم (چون کلاسمون از Observer ارث برده، تابع addObserver رو داره):eventSource.addObserver((obj, arg) -> { System.out.println("Received response: " + arg) });با این کار یه observer به شئ مون دادیم (و طبیعتاً هر چندتا observer بخوایم میتونیم اضافه کنیم)همونطور که دیدیم کلاس EventSource از نوع Runnable هست پس میتونیم با صدا زدن کد زیر تابع run اون کلاس رو صدا بزنیم و مادامی که کاربر رشته ای وارد کنه ما صرفاً توی کلاسمون با صدا زدن notifyObservers اون رشترو به همه ی observerها میفرستیم:new Thread(eventSource).start();تمام! و همونطور که گفتیم میتونیم چندتا observer داشته باشیم، مثلاً : eventSource.addObserver((obj, arg) -> { System.out.println("Received response 1: " + arg) }); eventSource.addObserver((obj, arg) -> { System.out.println("Received response 2: " + arg) }); eventSource.addObserver((obj, arg) -> { System.out.println("Received response 3: " + arg) });که با هربار وارد شدن رشته توسط کاربر، به سه observer ارسال و با تگ های Received response 1 و Received response 2 و Received response 3 چاپ میشه.خب بریم سراغ یه مثال دیگه.این بار برخلاف دفعات گذشته فرض میکنیم که میخوایم چندتا Subject داشته باشم. پس یه اینترفیس به اسم Subject مینویسیم:public interface Subject { //methods to register and unregister observers public void register(Observer obj); public void unregister(Observer obj); //method to notify observers of change public void notifyObservers(); //method to get updates from subject public Object getUpdate(Observer obj); }همونططور که مشخصه، توابع register و unregister برای اضافه یا حذف کردن observer هستن، تابع notifyObservers قراره observer ها رو مطلع کنه و getUpdate قراره update یه observer رو برگردونه.خب حالا نوب میرسه به کلاس Observer (این کلاس هم به عنوان اینترفیس معرفی میشه):public interface Observer { //method to update the observer, used by subject public void update(); //attach with subject to observe public void setSubject(Subject sub); } و باز هم همونطور که مشخصه، تابع update قراره زمانی که باید observer متوجه تغییر بشه صدا زده بشه و تابع setSubject یه subject یه observer میده.قدم بعدی ساختن یه subject هست. پس کلاس زیر رو مینویسیم:public class MyTopic implements Subject { private List<Observer> observers; private String message; private boolean changed; private final Object MUTEX= new Object(); public MyTopic(){ this.observers=new ArrayList<>(); } @Override public void register(Observer obj) { if(obj == null) throw new NullPointerException("Null Observer"); synchronized (MUTEX) { if(!observers.contains(obj)) observers.add(obj); } } @Override public void unregister(Observer obj) { synchronized (MUTEX) { observers.remove(obj); } } @Override public void notifyObservers() { List<Observer> observersLocal = null; //synchronization is used to make sure any observer registered after message is received is not notified synchronized (MUTEX) { if (!changed) return; observersLocal = new ArrayList<>(this.observers); this.changed=false; } for (Observer obj : observersLocal) { obj.update(); } } @Override public Object getUpdate(Observer obj) { return this.message; } //method to post message to the topic public void postMessage(String msg){ System.out.println("Message Posted to Topic:"+msg); this.message=msg; this.changed=true; notifyObservers(); } } همونطور که میبینیم، تابع register اگه پارامتر ورودیش که Observer هست null نباشه اون رو به لیست observerهامون اضافه میکنه و تابع unregister به observer ور از لیست حذف میکنه (هر دوی این توابع برای اطمینان از صحت عملکرد برنامه از synchronized استفاده میکنن). تابع postMessage هم message رو تغییر میده و observer هارو صدا میزنه.الاآن میخوایم کلاس observer جدیدی ایجاد کنیم:public class MyTopicSubscriber implements Observer { private String name; private Subject topic; public MyTopicSubscriber(String nm){ this.name=nm; } @Override public void update() { String msg = (String) topic.getUpdate(this); if(msg == null){ System.out.println(name+":: No new message"); }else System.out.println(name+":: Consuming message::"+msg); } @Override public void setSubject(Subject sub) { this.topic=sub; } } تابع setSubject به شئ observerمون یه subject اضافه میکنه و تابع update رشته ی msg رو از subject میگیره و اگه موجود باشه نمایشش میده و اگه موجود نباشه رشته ی :: No new message رو بعد از اسم observerمون چاپ میکنه.خب برای تست قطعه کد زیر رو مینویسیم://create subject MyTopic topic = new MyTopic(); //create observers Observer obj1 = new MyTopicSubscriber("Obj1"); Observer obj2 = new MyTopicSubscriber("Obj2"); Observer obj3 = new MyTopicSubscriber("Obj3"); //register observers to the subject topic.register(obj1); topic.register(obj2); topic.register(obj3); //attach observer to subject obj1.setSubject(topic); obj2.setSubject(topic); obj3.setSubject(topic);و با اضافه کردن خط زیر//check if any update is available obj1.update(); خروجی زیر رو میبینیم:Obj1:: No new messageو با خط زیر یه رشته به sunjectمون اضافه میکنیم //now send message to subject topic.postMessage("Test Message"); که طبیعتاً هم تابع postMessage رشترو چاپ میکنه و هم بلافاصله observerهامون هم اون رشترو دریافت میکنن و خروجی بصورت زیر میشه:Message Posted to Topic:Test Message Obj1:: Consuming message::Test Message Obj2:: Consuming message::Test Message Obj3:: Consuming message::Test Messageاین مثال در حقیقت پیاده سازی دیاگرام زیر بود: منتشر شده در ویرگول توسط محمد قدسیان https://virgool.io/@mohammad.ghodsian https://virgool.io/@mohammad.ghodsian/java-observer-design-pattern-rxnwdsjbxh78