Featured image of post BP5 - Observer - Learn Design Pattern From Simple Things

BP5 - Observer - Learn Design Pattern From Simple Things

Some students in a class are attentive, but others are easily distracted. The teacher is indifferent to them; for her, they are all observers and she just performs her duty.

Observer is a behavioral design pattern: 🖤🖤🖤🖤🖤

What is Observer?

Observer is a technique that defines a teacher-students one-to-many dependency between objects so that when one object changes state, all its dependents are notified and reacted automatically . It is also referred to as the publish-subscribe pattern.

Observer diagram

Why use Observer?

  • It defines a subscription mechanism to notify objects about events of the observed object.
  • It supports loose coupling between the subject and observers. The subject only knows a common observer interface and ignores concrete observers.
  • It makes the code modular, easy to maintain, allows adding new observers anytime without modifying the subject or others.

Question: I hate the Observer, what are the alternatives?

Answer: Other behavioral patterns such as Mediator, Chain of Responsibility, or State may suit your problem and context better. For example, Mediator reduces direct communication between components by making them communicate via a mediator object. Chain of Responsibility passes requests along a chain of handlers until one handles it. State changes an object’s behavior when its state changes. You can also code without using any pattern.

The State pattern lets an object change its behavior based on its internal state. It promotes loose coupling by putting the behavior in state objects. This lets the object change its behavior at runtime by changing its state. The State pattern is useful when an object needs different behaviors for different states.

The Observer pattern sets up a one-to-many dependency between objects. It notifies multiple observers when the observed object changes its state. It promotes loose coupling by making the observed object and observers unaware of each other. This allows easy addition or removal of observers without changing the observed object’s interface. The Observer pattern is useful when one object’s changes affect many others.

In summary, while both patterns deal with changing an object’s behavior in response to a change in its state, the State pattern deals with an object’s internal state, while the Observer pattern deals with external changes in the observed object’s state.

Observer

When to use Observer?

Question: When do I use Observer?

Answer: When you have situations like these:

  • need to update several objects when another object changes its state.
  • avoid tight coupling between an object and its dependents while still allowing them to communicate effectively.
  • broadcast events to multiple subscribers without knowing who they are or how many there are.

Input:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Teacher:
    def __init__(self) -> None:
        self._state: str = None

    @property
    def state(self) -> str:
        return self._state

    @state.setter
    def state(self, value: str) -> None:
        self._state = value

    def act(self, action: str) -> None:
        """Change the state of the teacher."""
        self.state = action
        print(f"- TEACHER ACT: I'm {self.state}")


# A class that represents a bad student who gossips when the teacher goes out
class BadStudent:
   def react(self, teacher: Teacher) -> None:
       if teacher.state == "going out":
            print(f"  - BAD_STUDENT REACT: 😃 I'm gossiping in the class")
       else:
            print(f"  - BAD_STUDENT REACT: I don't care!")


# A class that represents a good student who focuses when the teacher speaks
class GoodStudent:
   def react(self, teacher: Teacher) -> None:
       if teacher.state == "speaking":
            print(f"  - GOOD_STUDENT REACT: 😃 I'm focusing on what the teacher says")
       else:
            print(f"  - GOOD_STUDENT REACT: I don't care!")

Expected Output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Teacher said: Hello new student '99' to my class
Teacher said: Hello new student '78' to my class
- TEACHER ACT: I'm replying husband's SMS
  - BAD_STUDENT REACT: I don't care!
  - GOOD_STUDENT REACT: I don't care!
- TEACHER ACT: I'm going out
  - BAD_STUDENT REACT: 😃 I'm gossiping in the class
  - GOOD_STUDENT REACT: I don't care!
- TEACHER ACT: I'm speaking
  - BAD_STUDENT REACT: I don't care!
  - GOOD_STUDENT REACT: 😃 I'm focusing on what the teacher says
Teacher said: GoodBye student '78'
- TEACHER ACT: I'm going out
  - GOOD_STUDENT REACT: I don't care!

How to implement Observer?

Non-Observer implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# A class that represents a teacher who can change their state
class Teacher:
    def __init__(self) -> None:
        self._state: str = None

    @property
    def state(self) -> str:
        return self._state

    @state.setter
    def state(self, value: str) -> None:
        self._state = value

    def act(self, action: str) -> None:
        """Change the state of the teacher."""
        self.state = action
        print(f"- TEACHER ACT: I'm {self.state}")


# A class that represents a bad student who gossips when the teacher goes out
class BadStudent:
   def react(self, teacher: Teacher) -> None:
       if teacher.state == "going out":
            print(f"  - BAD_STUDENT REACT: 😃 I'm gossiping in the class")
       else:
            print(f"  - BAD_STUDENT REACT: I don't care!")


# A class that represents a good student who focuses when the teacher speaks
class GoodStudent:
   def react(self, teacher: Teacher) -> None:
       if teacher.state == "speaking":
            print(f"  - GOOD_STUDENT REACT: 😃 I'm focusing on what the teacher says")
       else:
            print(f"  - GOOD_STUDENT REACT: I don't care!")


if __name__ == "__main__":

    # Create a teacher and some students
    teacher = Teacher()
    bad_student = BadStudent()
    good_student = GoodStudent()

    # The teacher changes their state and the students react accordingly

    teacher.act("replying husband's SMS")
    bad_student.react(teacher)
    good_student.react(teacher)

    teacher.act("going out")
    bad_student.react(teacher)
    good_student.react(teacher)

    teacher.act("speaking")
    bad_student.react(teacher)
    good_student.react(teacher)

    teacher.act("going out")
    good_student.react(teacher)
  • This code violates the open-closed principle, which declares that classes should be open for extension but closed for modification.
    • In this case: If we want to add more types of students or more actions for the teacher, we have to modify the existing code!
  • It does not follow the don’t repeat yourself (DRY) principle, which declares that duplication of code should be avoided.
    • In this case: The student’s classes have to repeat the same logic for checking the teacher’s state and reacting to it, which makes them prone to errors and inconsistencies.
  • Using the Observer pattern can overcome these limitations by decoupling the subjects (teachers) from their observers (students), allowing them to communicate through an abstract interface (the Student abstract class), and making them independent of each other’s implementation details. This way, we can add new types of students or new actions for teachers without changing existing code or introducing new dependencies. We can also avoid duplication of code by implementing common logic in abstract classes or methods.

Observer Implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
from __future__ import annotations
from abc import ABC, abstractmethod


class Teacher:
    """A class that represents a teacher who can change their state and notify their students."""

    def __init__(self) -> None:
        self._state: str = None
        self._students: list[Student] = []

    @property
    def state(self) -> str:
        return self._state

    @state.setter
    def state(self, value: str) -> None:
        self._state = value

    @property
    def students(self) -> list[Student]:
        return self._students

    def attach(self, student: Student) -> None:
        """Add a new student to the list of observers."""
        print(f"Teacher said: Hello new student '{id(student)}' to my class")
        self.students.append(student)

    def detach(self, student: Student) -> None:
        """Remove a student from the list of observers."""
        print(f"Teacher said: GoodBye student '{id(student)}'")
        self.students.remove(student)

    def notify_students(self) -> None:
        """Notify all the students about the current state of the teacher."""
        print(f"- TEACHER ACT: I'm {self.state}")
        for student in self.students:
            student.react(self)

    def act(self, action: str) -> None:
        """Change the state of the teacher and notify the students."""
        self.state = action
        self.notify_students()


class Student(ABC):
    """An abstract class that represents a student who can react to a teacher's state."""

    @abstractmethod
    def react(self, teacher: Teacher) -> None:
        pass


class BadStudent(Student):
    """A class that represents a bad student who gossips when the teacher goes out."""

    def react(self, teacher: Teacher) -> None:
        if teacher.state == "going out":
            print(f"  - BAD_STUDENT REACT: 😃 I'm gossiping in the class")
        else:
            print(f"  - BAD_STUDENT REACT: I don't care!")


class GoodStudent(Student):
    """A class that represents a good student who focuses when the teacher speaks."""

    def react(self, teacher: Teacher) -> None:
        if teacher.state == "speaking":
            print(f"  - GOOD_STUDENT REACT: 😃 I'm focusing on what the teacher says")
        else:
            print(f"  - GOOD_STUDENT REACT: I don't care!")


if __name__ == "__main__":
    # Create a teacher object
    teacher = Teacher()

    # Create two student objects
    bad_student = BadStudent()
    good_student = GoodStudent()

    # Attach both students to observe the teacher
    teacher.attach(bad_student)
    teacher.attach(good_student)

    # Perform some actions as a teacher
    teacher.act("replying husband's SMS")
    teacher.act("going out")
    teacher.act("speaking")

    # Detach one student from observing the teacher
    teacher.detach(bad_student)

    # Perform another action as a teacher
    teacher.act("going out")

The improvement of this code compared to the non-observer code is that it follows some design principles that make the code more maintainable, extensible and reusable. Some of these principles are:

  • The open-closed principle: This code allows us to add new types of students or new actions for teachers without changing existing code or introducing new dependencies.
  • The don’t repeat yourself (DRY) principle: This code avoids repeating the same logic for checking the teacher’s state and reacting to it by implementing common logic in abstract classes or methods.

Source Code

Made with the laziness 🦥
by a busy guy