DECORATOR === # Decorator - is a structural design pattern - that lets you attach new behaviors to objects - by placing these objects inside special wrapper objects - that contain the behaviors. # Problem ```python # Imagine that you’re working on # - a notification library # - which lets other programs # - notify their users # - about important events. # # The initial version of the library # - was based on the Notifier class # - that had only a few fields # - a constructor and a single send method class Notifier(object): def __init__(self): pass def send(self, message): # a single send method # The method could # - accept a message argument # - from a client # - and send the message # - to a list of emails # - that were passed to the notifier via its constructor print("App::", message) ``` ```python class Application(object): # A third-party app # - which acted # - as a client was supposed # - to create and configure the notifier object once, # - and then use it each time something important happened. def __init__(self): self.notifier = Notifier() def set_notifier(self, notifier: Notifier): self.notifier = notifier def do_something(self): self.notifier.send("Alert!") ``` At some point, - you realize - that users of the library - expect more than just email notifications. ```python # Many of them # - would like to receive an SMS about critical issues. # Others # - would like to be notified on Facebook and, # - of course, the corporate users would love to get Slack notifications. # How hard can that be? # - You extended the Notifier class # - and put the additional notification methods into new subclasses. class SMSNotifier(Notifier): def send(self, message): print("sms::", message) class FacebookNotifier(Notifier): def send(self, message): print("Facebook::", message) class SlackNotifier(Notifier): def send(self, message): print("Slack::", message) ``` Now the client - was supposed to instantiate the desired notification class - and use it for all further notifications. ```python # But then someone reasonably asked you, # - “Why can’t you use several notification types at once? # - If your house is on fire, you’d probably # want to be informed through # every channel.” # You tried # - to address that problem by creating special subclasses # - which combined several notification methods within one # class. class SMSFaceBookNotifier(Notifier): def send(self, message): print("SMS&FaceBook::", message) class SMSSlackNotifier(Notifier): def send(self, message): print("SMS&Slack::", message) class SMSFaceBookSlackNotifier(Notifier): def send(self, message): print("SMS&FaceBock&Slack::", message) class FacebookSlackNotifier(Notifier): def send(self, message): print("FaceBock&Slack::", message) ``` ```python # However, it # - quickly became apparent that this approach # - would bloat the code immensely, # - not only the library code # - but the client code as well. # # You have to find some other way # - to structure notifications classes # - so that their number # - won’t accidentally break some Guinness record. def main(): message = "message" is_facebook_notifier = True is_app_notifier = True if_is_slack_notifier = True if is_facebook_notifier: notifier = FacebookNotifier() notifier.send(message) if is_app_notifier: notifier = Notifier() notifier.send(message) if if_is_slack_notifier: notifier = SlackNotifier() notifier.send(message) if __name__ == "__main__": main() ``` # Solution <!-- Extending a class - is the first thing - that comes to mind - when you need to alter an object’s behavior. However, inheritance - has several serious caveats - that you need to be aware of. Inheritance is static. You - can’t alter the behavior - of an existing object at runtime. You - can only replace the whole object - with another one that’s created - from a different subclass. Subclasses can have just one parent class. - In most languages, inheritance - doesn’t let a class inherit behaviors - of multiple classes at the same time. One of the ways - to overcome these caveats is - by using Aggregation or Composition instead of Inheritance. - Both of the alternatives work almost the same way: - one object has a reference to another - and delegates it some work, - whereas with inheritance, - the object itself is able to do that work, - inheriting the behavior from its superclass. --> <!--“Wrapper” is the alternative - nickname for the Decorator pattern - that clearly expresses the main idea of the pattern. A wrapper - is an object - that can be linked - with some target object. The wrapper - contains the same set of methods as the target - and delegates to it all requests it receives. However, the wrapper - may alter the result - by doing something either before - or after it passes the request to the target. --> <!-- When does a simple wrapper - become the real decorator? As I mentioned, the wrapper - implements the same interface as the wrapped object. - That’s why from the client’s perspective these objects are identical. Make the wrapper’s - reference field accept any object - that follows that interface. This will let you cover an object in multiple wrappers, - adding the combined behavior of all the wrappers to it.--> ```python class Notifier(object): def send(self, message): print("App::", message) class BaseDecorator(object): # In our notifications example, # - let’s leave the simple email notification behavior # - inside the base Notifier class, # - but turn all other notification methods into decorators. def __init__(self, notifier): self.wrappee = notifier def send(self, message): self.wrappee.send(message) ``` ```python class SMSDecorator(BaseDecorator): def send(self, message): self.wrappee.send(message) print("sms::", message) class FacebookDecorator(BaseDecorator): def send(self, message): self.wrappee.send(message) print("Facebook::", message) class SlackDecorator(BaseDecorator): def send(self, message): self.wrappee.send(message) print("Slack::", message) ``` ```python def main(): message = "message" is_facebook_notifier = True is_sms_notifier = True if_is_slack_notifier = True notifier = Notifier() if is_facebook_notifier: notifier = FacebookDecorator(notifier) if is_sms_notifier: notifier = SMSDecorator(notifier) if if_is_slack_notifier: notifier = SlackDecorator(notifier) # Since all decorators implement the same interface # - as the base notifier, # - the rest of the client code # - won’t care whether it # - works with the “pure” notifier object # - or the decorated one. # # The resulting objects # - will be structured as a stack. # # The last decorator # - in the stack # - would be the object # - that the client actually works with. # # We # - could apply the same approach # - to other behaviors such as # - formatting messages # - or composing the recipient list. # # The client # - can decorate the object # - with any custom decorators, # - as long as they # - follow the same interface as the others. # notifier.send(message) notifier.send() if __name__ == "__main__": main() ``` # Applicability Use the Decorator pattern - when you need to be able - to assign extra behaviors - to objects at runtime - without breaking the code that uses these objects. <!-- The Decorator - lets you structure your business logic into layers, - create a decorator - for each layer - and compose objects - with various combinations - of this logic at runtime. The client code - can treat all these objects in the same way, - since they all follow a common interface. --> --- Use the pattern - when it’s awkward - or not possible to extend an object’s behavior - using inheritance. <!-- Many programming languages - have the final keyword - that can be used - to prevent further extension - of a class. For a final class, - the only way - to reuse the existing behavior - would be to wrap the class with your own wrapper, - using the Decorator pattern.-->