-
-
Notifications
You must be signed in to change notification settings - Fork 0
03 observer pattern
Zuko edited this page Jan 21, 2026
·
2 revisions
Decoupled event-driven communication với smart parameter injection
Observer pattern trong core sử dụng Publisher/Subscriber model để:
- Decouple components (không cần biết nhau)
- Event-driven communication
- Smart parameter injection (type-hint aware)
- Thread-safe event dispatching
- Qt signal integration
Singleton access:
from core import Publisher
publisher = Publisher.instance()
# Or via QtAppContext
from core import QtAppContext
publisher = QtAppContext.globalInstance().publisherNotify event:
publisher.notify('event_name', arg1, arg2, key1=value1)Subscribe:
publisher.subscribe(subscriber, event='specific_event')
publisher.subscribe(subscriber) # Global subscriber (all events)Unsubscribe:
publisher.unsubscribe(subscriber, event='specific_event')
publisher.unsubscribe(subscriber) # Unsubscribe from allConnect Qt signal:
publisher.connect(widget, 'clicked', 'button_clicked', extraArg='value')Base class:
from core import Subscriber
class MyHandler(Subscriber):
def __init__(self):
super().__init__(events=['event1', 'event2'])
def onEvent1(self, arg):
# Handle event1
pass
def onEvent2(self, arg1, arg2):
# Handle event2
passEvent handler naming:
- Method name:
on+PascalCase(event_name) - Example:
user_login→onUserLogin
from core import Publisher
publisher = Publisher.instance()
# Publish event
publisher.notify('user.login', userId=123, username='john')from core import Subscriber
class UserHandler(Subscriber):
def __init__(self):
super().__init__(events=['user.login', 'user.logout'])
def onUserLogin(self, userId: int, username: str):
print(f'User {username} (ID: {userId}) logged in')
def onUserLogout(self, userId: int):
print(f'User {userId} logged out')
# Create handler (auto-subscribes)
handler = UserHandler()
# Publish events
publisher = Publisher.instance()
publisher.notify('user.login', userId=123, username='john')
publisher.notify('user.logout', userId=123)from core import Subscriber
class SmartHandler(Subscriber):
def __init__(self):
super().__init__(events=['data.received'])
# Type hints guide parameter matching
def onDataReceived(self, userId: int, data: dict, timestamp: float):
print(f'User {userId} sent data at {timestamp}')
print(f'Data: {data}')
# Publish with mixed args/kwargs
publisher.notify('data.received',
123, # Matched to userId by type (int)
{'key': 'value'}, # Matched to data by type (dict)
timestamp=1234567890.0 # Matched by name
)from core import Subscriber
class GlobalHandler(Subscriber):
def __init__(self):
# No events specified = global subscriber
super().__init__(events=[], isGlobalSubscriber=True)
def update(self, event: str, *args, **kwargs):
"""Called for ALL events"""
print(f'Event: {event}, Args: {args}, Kwargs: {kwargs}')
# Receives all events
handler = GlobalHandler()from core import Publisher
from PySide6.QtWidgets import QPushButton
button = QPushButton('Click Me')
publisher = Publisher.instance()
# Connect Qt signal to event
publisher.connect(button, 'clicked', 'button.clicked', buttonId='submit')
# Handler
class ButtonHandler(Subscriber):
def __init__(self):
super().__init__(events=['button.clicked'])
def onButtonClicked(self, buttonId: str):
print(f'Button {buttonId} clicked')from core import BaseController, BaseCtlHandler
from PySide6.QtWidgets import QWidget
class MyController(BaseController, QWidget):
slot_map = {
'saveClicked': ['saveButton', 'clicked'],
'cancelClicked': ['cancelButton', 'clicked']
}
def setupUi(self, widget):
# UI setup
pass
class MyControllerHandler(BaseCtlHandler):
def onSaveClicked(self):
data = self.widgetManager.get('dataInput').text()
# Process data...
# Publish event for other components
publisher = Publisher.instance()
publisher.notify('data.saved', data=data)
def onCancelClicked(self):
publisher = Publisher.instance()
publisher.notify('operation.cancelled')-
By name (kwargs):
userId=123matchesuserIdparameter -
By type hint:
arg: intmatches first unused int argument - By position: First unused arg matches first unmatched parameter
class Handler(Subscriber):
def __init__(self):
super().__init__(events=['complex.event'])
def onComplexEvent(self, userId: int, username: str, data: dict, optional: str = 'default'):
pass
# All these work:
publisher.notify('complex.event', 123, 'john', {'key': 'value'})
publisher.notify('complex.event', userId=123, username='john', data={'key': 'value'})
publisher.notify('complex.event', 123, username='john', data={'key': 'value'})
publisher.notify('complex.event', {'key': 'value'}, 123, 'john') # Type-based matchingIf smart injection fails:
- Try calling with no args
- Try calling with first arg only
- Try calling with first two args
- Try calling with all args
- Raise TypeError if all fail
graph TB
Publisher --> GlobalSubs["Global Subscribers<br/>List"]
Publisher --> EventSubs["Event-Specific Subscribers<br/>Dict[event, List]"]
Component1[Component A] -->|notify| Publisher
Component2[Component B] -->|notify| Publisher
Publisher -->|update| Handler1[Handler 1]
Publisher -->|update| Handler2[Handler 2]
Publisher -->|update| Handler3[Handler 3]
QtSignal[Qt Signal] -->|connect| Publisher
Handler1 -.->|no coupling| Component1
Handler2 -.->|no coupling| Component2
style Publisher fill:#e8f5e9
style GlobalSubs fill:#fff4e1
style EventSubs fill:#e1f5ff
# Use descriptive event names with namespaces
publisher.notify('user.login', userId=123)
publisher.notify('data.saved', recordId=456)
publisher.notify('task.completed', taskId='abc')
# Use type hints for smart injection
def onUserLogin(self, userId: int, username: str):
pass
# Subscribe to specific events only
class MyHandler(Subscriber):
def __init__(self):
super().__init__(events=['user.login', 'user.logout'])
# Unsubscribe when done
handler = MyHandler()
# ... use handler ...
Publisher.instance().unsubscribe(handler)
# Use kwargs for clarity
publisher.notify('user.login', userId=123, username='john')# Don't use generic event names
publisher.notify('event', data) # Bad
publisher.notify('user.login', userId=123) # Good
# Don't forget type hints
def onUserLogin(self, userId, username): # Harder to match
pass
# Don't subscribe to all events unless needed
class MyHandler(Subscriber):
def __init__(self):
super().__init__(events=[]) # Receives nothing
# Should specify events
# Don't call handler methods directly
handler.onUserLogin(123, 'john') # Wrong! Use publisher.notify
# Don't create circular dependencies
class HandlerA(Subscriber):
def onEventA(self):
publisher.notify('event.b') # OK
class HandlerB(Subscriber):
def onEventB(self):
publisher.notify('event.a') # Circular! Avoid- ✅
notify(): Thread-safe (QMutex) - ✅
subscribe()/unsubscribe(): Thread-safe (QMutex) - ✅ Event handlers called in publisher's thread
⚠️ Handler exceptions logged but don't stop other handlers
# Define application-wide events
class AppEvents:
APP_READY = 'app.ready'
APP_SHUTDOWN = 'app.shutdown'
USER_LOGIN = 'user.login'
USER_LOGOUT = 'user.logout'
DATA_CHANGED = 'data.changed'
# Publish
publisher.notify(AppEvents.USER_LOGIN, userId=123)
# Subscribe
class AppHandler(Subscriber):
def __init__(self):
super().__init__(events=[
AppEvents.APP_READY,
AppEvents.USER_LOGIN
])# Component A
class DataService:
def saveData(self, data):
# Save to database
publisher = Publisher.instance()
publisher.notify('data.saved', data=data)
# Component B (no dependency on Component A)
class UIHandler(Subscriber):
def __init__(self):
super().__init__(events=['data.saved'])
def onDataSaved(self, data: dict):
# Update UI
pass# Task
class MyTask(AbstractTask):
def handle(self):
publisher = Publisher.instance()
for i in range(100):
# Do work...
publisher.notify('task.progress', taskId=self.uuid, progress=i)
publisher.notify('task.completed', taskId=self.uuid)
# UI Handler
class TaskProgressHandler(Subscriber):
def __init__(self):
super().__init__(events=['task.progress', 'task.completed'])
def onTaskProgress(self, taskId: str, progress: int):
# Update progress bar
pass
def onTaskCompleted(self, taskId: str):
# Show completion message
pass- BaseController - Controller integration
- QtAppContext - Publisher access
- Common Use Cases - Practical examples
Q: Handler not called
# Check event name matches
class MyHandler(Subscriber):
def __init__(self):
super().__init__(events=['user.login']) # Must match exactly
def onUserLogin(self): # Method name must be on + PascalCase(event)
pass
publisher.notify('user.login') # Must match event in __init__Q: Parameter injection fails
# Add type hints
def onUserLogin(self, userId: int, username: str): # With type hints
pass
# Or use kwargs
publisher.notify('user.login', userId=123, username='john')Q: Handler called multiple times
# Check for duplicate subscriptions
handler = MyHandler() # Subscribes once
handler = MyHandler() # Creates new instance, subscribes again
# Unsubscribe old handlers
Publisher.instance().unsubscribe(oldHandler)