یک فنجان جاوا - دیزاین پترن ها - 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