I l@ve RuBoard |
9.12 Using Publish/Subscribe Broadcasting to Loosen the Coupling Between GUI and Business Logic SystemsCredit: Jimmy Retzlaff 9.12.1 ProblemYou need to loosen the coupling between two subsystems, since each is often changed independently. Typically, the two subsystems are the GUI and business-logic subsystems of an application. 9.12.2 SolutionTightly coupling application-logic and presentation subsystems is a bad idea. Publish/subscribe is a good pattern to use for loosening the degree of coupling between such subsystems. The following broadcaster module (broadcaster.py) essentially implements a multiplexed function call in which the caller does not need to know the interface of the called functions: # broadcaster.py _ _all_ _ = ['Register', 'Broadcast', 'CurrentSource', 'CurrentTitle', 'CurrentData'] listeners = {} currentSources = [] currentTitles = [] currentData = [] def Register(listener, arguments=( ), source=None, title=None): if not listeners.has_key((source, title)): listeners[(source, title)] = [] listeners[(source, title)].append((listener, arguments)) def Broadcast(source, title, data={}): currentSources.append(source) currentTitles.append(title) currentData.append(data) listenerList = listeners.get((source, title), [])[:] if source != None: listenerList += listeners.get((None, title), []) if title != None: listenerList += listeners.get((source, None), []) for listener, arguments in listenerList: apply(listener, arguments) currentSources.pop( ) currentTitles.pop( ) currentData.pop( ) def CurrentSource( ): return currentSources[-1] def CurrentTitle( ): return currentTitles[-1] def CurrentData( ): return currentData[-1] The broker module (broker.py) enables the retrieval of named data even when the source of the data is not known: # broker.py _ _all_ _ = ['Register', 'Request', 'CurrentTitle', 'CurrentData'] providers = {} currentTitles = [] currentData = [] def Register(title, provider, arguments=( )): assert not providers.has_key(title) providers[title] = (provider, arguments) def Request(title, data={}): currentTitles.append(title) currentData.append(data) result = apply(apply, providers.get(title)) currentTitles.pop( ) currentData.pop( ) return result def CurrentTitle( ): return currentTitles[-1] def CurrentData( ): return currentData[-1] 9.12.3 DiscussionIn a running application, the broadcaster and broker modules enable loose coupling between objects in a publish/subscribe fashion. This recipe is particularly useful in GUI applications, where it helps to shield application logic from user-interface changes, although the field of application is more general. Essentially, broadcasting is equivalent to a multiplexed function call in which the caller does not need to know the interface of the called functions. broadcaster can optionally supply data for the subscribers to consume. For example, if an application is about to exit, it can broadcast a message to that effect, and any interested objects can perform whatever finalization tasks they need to do. Another example is a user-interface control that can broadcast a message whenever its state changes so that other objects (both within the GUI, for immediate feedback, and outside of the GUI, typically in a business-logic subsystem of the application) can respond appropriately. broker enables the retrieval of named data even when the source of the data is not known. For example, a user-interface control (such as an edit box) can register itself as a data provider with broker, and any code in the application can retrieve the control's value with no knowledge of how or where the value is stored. This avoids two potential pitfalls:
broker and broadcaster work together nicely. For example, consider an edit box used for entering a date. Whenever its value changes, it can broadcast a message indicating that the entered date has changed. Anything depending on that date can respond to that message by asking broker for the current value. Later, the edit box can be replaced by a calendar control. As long as the new control broadcasts the same messages and provides the same data through broker, no other code should need to be changed. Such are the advantages of loose coupling. The following sample.py script shows an example of using broadcaster and broker: # sample.py from _ _future_ _ import nested_scopes import broadcaster import broker class UserSettings: def _ _init_ _(self): self.preferredLanguage = 'English' # The use of lambda here provides a simple wrapper around # the value being provided. Every time the value is requested, # the variable will be reevaluated by the lambda function. # Note the dependence on nested scopes, thus Python 2.1 or later is required. broker.Register('Preferred Language', lambda: self.preferredLanguage) self.preferredSkin = 'Cool Blue Skin' broker.Register('Preferred Skin', lambda: self.preferredSkin) def ChangePreferredSkinTo(self, preferredSkin): self.preferredSkin = preferredSkin broadcaster.Broadcast('Preferred Skin', 'Changed') def ChangePreferredLanguageTo(self, preferredLanguage): self.preferredLanguage = preferredLanguage broadcaster.Broadcast('Preferred Language', 'Changed') def ChangeSkin( ): print 'Changing to', broker.Request('Preferred Skin') def ChangeLanguage( ): print 'Changing to', broker.Request('Preferred Language') broadcaster.Register(ChangeSkin, source='Preferred Skin', title='Changed') broadcaster.Register(ChangeLanguage, source='Preferred Language', title='Changed') userSettings = UserSettings( ) userSettings.ChangePreferredSkinTo('Bright Green Skin') userSettings.ChangePreferredSkinTo('French') Note that the idiom in this recipe is thread-hostile: even if access to the module-level variables was properly controlled, this style of programming is tailor-made for deadlocks and race conditions. Consider the impact carefully before using this approach from multiple threads. In a multithreaded setting, it is probably preferable to use Queue instances to store messages for other threads to consume and architect a different kind of broadcast (multiplexing) by having broker post to appropriate registered Queues. 9.12.4 See AlsoRecipe 9.7 for one approach to multithreading in a GUI setting; Recipe 13.8 to see publish/subscribe used in a distributed processing setting. |
I l@ve RuBoard |