Download presentation
Presentation is loading. Please wait.
Published byCalvin Freeman Modified over 6 years ago
1
Design Patterns I Technion – Institute of Technology 236700
Author: Gal Lalouche - Technion 2017 © Technion – Institute of Technology 236700 Spring 2017 Author: Gal Lalouche
2
Design patterns What are design patterns?
Reusable, tested, proven solution templates to common, recurring problems ‘Design Patterns, Elements of Reusable Object-Oriented Software’ by: Gemma, Helm, Johnson, Vlissides A.K.A. Gang of Four, or GoF A common language of software design solutions Author: Gal Lalouche - Technion 2017 ©
3
Categories Behavioural Creational Structural
Describes a process/flow and classes can assign responsibilities between each other Examples taught: Observer, Visitor, Template Creational Revolves around the creation and instantiation of objects Examples taught: Factories, Singleton Structural How classes and objects are composed to form new structures Examples taught: Proxy, Composite Author: Gal Lalouche - Technion 2017 © Factory and abstract Factory were taught in the DI tutorial Template will be taught in the lectures
4
Observer: Motivation The problem: We are developing an IDE
We want to be notified whenever a file changes Example uses: Recompile Updating indexes Update our source control Author: Gal Lalouche - Technion 2017 ©
5
Observer: Alternatives
Invoke all the required functions whenever we modify the file What's the problem? We've increased MyFile's coupling Hard to expand with new behavior (have to modify the class) class MyFile { // this can be invoked in any number of ways… private void onChange() { Compiler.compile(this); indexUpdater.update(this); sourceControl.update(this); } public void write(String s) { // other logic… onChange(); Author: Gal Lalouche - Technion 2017 ©
6
Observer: Solution Decouple the File (Observable) from those who want to be notified on changes (Observers) Both Observable and Observers can be oblivious to each other Pass a listener callback that will be invoked on changes Depending on the number of parameters, this can be a Runnable, Consumer<T>, BiConsumer<T>, or a new custom interface With a custom interface, we could also listen to multiple event types It also makes more sense (and is more declarative) to implement a custom listenable than a standard library one Author: Gal Lalouche - Technion 2017 © class Compiler implement Runnable … // ??? class Compiler impelement FileChangeListener // ah okay
7
Observer: Implementation
Observable: Observers: interface MyFileObserver { // or use the standard Consumer<MyFile> void notify(MyFile f); } class MyFile { private void Collection<MyFileObserver> observers = new HashSet<>(); public final void listen(MyFileObserver o) { observers.add(o); public final void unlisten(MyFileObserver o) { observers.remove(o); private final void onChange() { observers.forEach(l -> l.notify(this)); Author: Gal Lalouche - Technion 2017 © // we can either use lambdas/method references myFile.listen(compiler::compile) // or implement an interface class IndexUpdater implements MyFileObserver { … } myFile.listen(indexUpdater);
8
Observer: Advanced implementations
Abstract class (simply extend for functionality): Save us from writing boiler plate, but uses our single inheritance slot Why can't we use an interface with default methods? Included in the standard library, but it's for a pre-generic version Best to avoid, use RxJava's version instead abstract class Observable<T> { private void Collection<Consumer<T>> listeners = new HashSet<>(); public final void listen(Consumer<T> c) { listeners.add(o); } public final void unlisten(Consumer<T> c) { listeners.remove(o); protected final void onChange(T t) { listeners.forEach(c -> c.accept(t)); class MyFile extends Observable<MyFile> { public void write(String s) { // … writing logic onChange(this); Author: Gal Lalouche - Technion 2017 ©
9
Observer: Applicability
Use the observer pattern when: You want to decouple event creators from their consumers Both observers and observables are decoupled from each other Observables don't care how the observer is implemented: busy wait, Thread.notify, explicit publishing, … Observers don't care what observables do with the information But they might care how long they take; consider invoking actions on a separate thread to avoid starvations You want to expose an event based API This pattern is very common in GUI programming Listening on button clicks, form changes, etc. A generalization of the observer pattern is the publish-subscriber architecture (AKA pubsub) Publishers publish to a topic, subscribers listen to a topic, a central service mediates the two Author: Gal Lalouche - Technion 2017 ©
10
Observer: Implications
Easy to expand with new behaviors Easy to test with mocks Observers and observables are completely decoupled Includes a fair bit of boilerplate But we can often use libraries to avoid that Is inherently stateful Have to keep a mutable list of its listeners It's possible to get around this, but usually not worth the effort Author: Gal Lalouche - Technion 2017 ©
11
Proxy: Motivation Problem:
We have a remote server that we send packets to When sending each packet, we first have to establish a connection, perform a “handshake” protocol, etc. In other words, there is a lot of overhead for every sent packet In order to minimize the overhead of packet transfer, we would like to buffer all packets, and send them 10 at a time… But putting all of that logic in the client would hurt its cohesion interface Server { void sendPacket(Packet p); } Author: Gal Lalouche - Technion 2017 ©
12
Proxy: Alternatives Okay, let’s put the buffer logic in a buffer class, and invoke that Problems? We just moved the logic to another class, but the client still has to account for it, and the client is now coupled with the Buffer class What if we want to replace the buffer logic with a retry logic? What if we want to compose logics (both buffer and retry)? class Buffer { void add(Packet p) { … } int size() { … } // aggregates all packets together, and deletes them from memory Packet getMegaPacketAndClear() { … } } buffer.add(new MessagePacket(…)); buffer.add(new BytePacket(…)); buffer.add(new NumberPacket(…)); if (buffer.size() >= 10) server.send(buffer.getMegaPacketAndClear()); Author: Gal Lalouche - Technion 2017 ©
13
Proxy: Solution Wrap the server with a buffer, but expose the same interface Client code is unchanged If we tested using mocks, our unit tests are unchanged as well! Easy to compose behavior: simply pass Proxy X to Proxy Y Proxies are oblivious to proxies too! class ServerBuffer implements Server { private final Server s; private final Buffer buffer = new BufferImpl(); public ServerBuffer(Server s) { this.s = s; } @Override void sendPacket(Packet p) { buffer.add(p); if (buffer.size() >= 10) server.send(buffer.getMegaPacketAndClear()); } Author: Gal Lalouche - Technion 2017 © Server retryBufferServer = new RetryServer(new BufferServer(server));
14
Proxy: Applicability Use the Proxy pattern when
You want to wrap an existing implementation You want to hide the refinement from the client You want to enable linear composition of behaviors Other types of proxies include: RetryProxy CacheProxy RemoteProxy ProtectiveProxy Author: Gal Lalouche - Technion 2017 ©
15
Proxy: Implications Client is oblivious to the change
(True) unit tests don't need to be modified Can compose proxies The extra layer of abstraction may surprise the client For example, the client may be surprised when the messages are buffered rather than sent immediately (Integration) tests may fail Author: Gal Lalouche - Technion 2017 ©
16
Composite: Motivation
Problem: We want to implement logging in our application There are several ways in which we can log messages Write the message to the console Write it a file Write it in a database Send it to a remote server We usually want to log to more than one service interface Logger { void log(String s); } // specific implementation class ConsoleLogger implements Logger { @Override void log(String s) { System.out.println(s); } class FileLogger implements Logger { … } class RemoteLogger implements Logger { … } Author: Gal Lalouche - Technion 2017 ©
17
Composite: Alternatives I
Okay, so we'll pass around an Iterable<Logger> instead of Logger Problems? Annoying to use & test Very verbose We have to modify all our current logging clients // old client code class MyClass { private final Logger logger; public MyClass(Logger logger) { this.logger = logger; } public void myMethod() { logger.log("My method called"); } // new client code private final Iterable<Logger> loggers; public MyClass(Iterable<Logger> loggers) { this.loggers = loggers; } loggers.forEach(l -> l.log("My method called")); Author: Gal Lalouche - Technion 2017 ©
18
Composite: Alternatives II
I know, this is a proxy pattern! Yes, but not quite While proxy pattern does solve this, it's a very verbose solution // this times how many logger we have class ConsoleProxyLogger implements Logger { private final Logger logger; public ProxyLogger(Logger logger) { this.logger = logger; } public void log(String s) { System.out.println(s); logger.log(s); // logger creation new ConsoleLoggerProxy( new FileLoggerProxy(fileLogger, new SqlLoggerProxy(…))); Author: Gal Lalouche - Technion 2017 ©
19
Composite: Solution The composite pattern is actually a special case of a proxy that accepts several elements: Defining a composite logger is now very easy: Logging clients code is unchanged class CompositeLogger implements Logger { private final Iterable<Logger> loggers; public CompositeLogger(Iterable<Logger> loggers) { this.loggers = loggers; } @Override public void log(String s) { loggers.forEach(s -> log()); Author: Gal Lalouche - Technion 2017 © Logger compLogger = new CompositeLogger(Arrays.asList(consoleLogger, fileLogger, sqlLogger, …);
20
Composite: Applicability
Use composites when you want to treat multiple elements as a single elements Of course, composites can be composed of other composites Popular examples include: compound expressions – literals or an operation on expression UI widgets – a UI widget can be composed of multiple, children widgets, e.g., a form contains multiple text boxes and button, and possibly sub forms Guice Modules – using the Modules.combine method Composites compositions form a directed tree Proxy compositions form a linear list Author: Gal Lalouche - Technion 2017 ©
21
Composite: Implications
Clients is (again) oblivious to the change Can compose multiple composites recursively The (potentially exponential) added number of operations may surprise clients Deep compositions can be very hard to keep track off and reason about e.g., Guice modules It's much easier to follow a list (Proxy) than a tree Author: Gal Lalouche - Technion 2017 ©
Similar presentations
© 2025 SlidePlayer.com. Inc.
All rights reserved.