Design Patterns in Java
One of the unchanging facts of life is that change is the undying constant in every software lifecycle - one that you cannot run away from. The challenge is to adapt to this change with minimum latency and maximum flexibility.
The good news is that someone has probably already solved your design problems and their solutions have evolved into best practices; these agreed-upon best practices are referred to as "Design Patterns". Today, we're going to explore two popular design patterns, and learn how good design can help you write code that is squeaky clean and extensible.
The Adapter Design Pattern
Let's assume that you have an existing legacy system. You're now tasked with making it work with a new third-party library, but this library has a different API compared to the last one you were using. The legacy system now expects a different interface than what the new library provides. Of course, you could be brave (read, foolish) enough to think about changing your legacy code to adapt to new interface but as with every legacy system -- never, ever.
Adapters to the rescue! Simply write an
adapter
(a new wrapping class) in between the systems, which listens for client requests to the older interface, and redirects or translates them into calls to the new interface. This conversion can either be implemented with inheritance or composition.Great design is not just about re-usability, but about extensibility.
Adapters help incompatible classes work together by taking an interface and adapting it to one that the client can parse.
Adapters In Action
Enough chit-chat; let's get down to business, shall we? Our legacy software system uses the following
LegacyVideoController
interface to control the video system.
01
02
03
04
05
06
07
08
09
10
| public interface LegacyVideoController{ /** * Begins the playback after startTimeTicks * from the beginning of the video * @param startTimeTicks time in milliseconds */ public void startPlayback( long startTimeTicks); ... } |
The client code which uses this controller looks like:
1
2
3
4
5
| public void playBackVideo( long timeToStart, LegacyVideoController controller){ if (controller!= null ){ controller.startPlayback(timeToStart); } } |
User Requirements Change!
There's nothing new here, really - it happens quite frequently. User requirements are prone to change all the time and our legacy system now needs to work with a new video controller, having the following interface:
01
02
03
04
05
06
07
08
09
10
11
12
13
| public interface AdvancedVideoController{ /** * Places the controller head after time * from the beginning of the track * @param time time defines how much seek is required */ public void seek(Time time); /** * Plays the track */ public void play(); } |
As a result, the client code breaks, as this new interface is not compatible.
Adapter Saves the Day
So, how do we handle this modified interface without changing our legacy code? You know the answer now, don't you? We write a simple adapter to modify its interface to adapt to the existing one as below:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
| public class AdvancedVideoControllerAdapter implements LegacyVideoController { private AdvancedVideoController advancedVideoController; public AdvancedVideoControllerAdapter(AdvancedVideoController advancedVideoController){ this .advancedVideoController = advancedVideoController; } @Override public void startPlayback( long startTimeTicks) { // Convert long into DateTime Time startTime = getTime(startTimeTicks); // Adapt advancedVideoController.seek(startTime); advancedVideoController.play(); } } |
This adapter implements the target interface, which the client expects so there is no need to change the client code. We compose the adapter with an instance of the adaptee interface.
This "has-a" relationship allows the adapter to delegate the client request to the actual instance.
Adapters also help in decoupling the client's code and the implementation.
We can now simply wrap the new object in this adapter and be done with it, without making any changes to the client code as the new object is now converted/adapted to the same interface.
1
2
3
4
| AdvancedVideoController advancedController = controllerFactory.createController(); // adapt LegacyVideoController controllerAdapter = new AdvancedVideoControllerAdapter(advancedController); playBackVideo( 20 , controllerAdapter); |
An adapter can be a simple pass through or can be intelligent enough to provide some add-ons depending on the complexity of the interface to support. Similarly, one adapter can be used to wrap more than one object if the target interface is complex and the new functionality has been split across two or more classes.
Comparison With Other Patterns
- Decorator : Decorator changes the interface and wraps an object around by adding new responsibility. On the other hand, adapter is used to convert the adaptee interface to target interface which is understood by the client.
- Facade : Facade works by defining a totally new interface abstracting the complexity of earlier interfaces while adapter is used to enable communication between incompatible interfaces by converting one into another.
- Proxy : Proxy provides the same interface. Whereas adapter provides a different interface to its subject.
- Bridge : Bridge is designed upfront to let the abstraction and the implementation vary independently but adapter is used to adapt to an existing interface by delegating the request to adaptee.
The Singleton Design Pattern
Though there are many patterns which deal with creation of objects, one specific pattern stands out. Today we're going to inspect one of the most simple, yet misunderstood, ones: the Singleton pattern.
As the name suggests, the purpose of the singleton is to create a single instance of the class and provide global access to it. Examples can be an application level
Cache
, an object pool of threads, connections etc. For such entities, one and only instance must suffice else they threaten the stability and defeat the purpose of the application.Implementing the Singleton Pattern
A bare-bones implementation in Java would look like:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
| public class ApplicationCache{ private Map<String, Object> attributeMap; // Static instance private static ApplicationCache instance; // Static accessor method public static ApplicationCache getInstance(){ if (instance == null ){ instance == new ApplicationCache(); } return instance; } // private Constructor private ApplicationCache(){ attributeMap = createCache(); // Initialize the cache } } |
In our example, the class holds a static member of the same type as that of the class, which is accessed via a static method. We make use of Lazy Initialization here, delaying the initialization of the cache, until it is actually needed at runtime. The constructor is also made private so that a new instance of this class cannot be created using the
new
operator. To fetch the cache, we invoke:
1
2
| ApplicationCache cache = ApplicationCache.getInstance(); // use cache to improve performance |
It works perfectly fine as long as we are dealing with a single-threaded model. But life, as we know it, is not that simple. In a multi-threaded environment, you need to synchronize the lazy initialization or just do away with it, by creating the cache as soon as the class is loaded, either by using static blocks or initializing while declaring the cache.
Double Checked Locking
We synchronize the lazy initialization to make sure that the initialization code is run only once. This code works with Java version 5.0 and above due to idiosyncrasies associated with the implementation of
synchronized
and volatile
in Java.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public class ApplicationCache{ private Map<String, Object> attributeMap; // volatile so that JVM out-of-order writes do not happen private static volatile ApplicationCache instance; public static ApplicationCache getInstance(){ // Checked once if (instance == null ){ // Synchronized on Class level lock synchronized (ApplicationCache. class ){ // Checked again if (instance == null ){ instance == new ApplicationCache(); } } } return instance; } private ApplicationCache(){ attributeMap = createCache(); // Initialize the cache } } |
We make the instance variable volatile so that the JVM prevents out-of-order writes for it. We also do a double null check (hence the name) for instance while synchronizing the initialization so that any sequence of 2 or more threads does not corrupt the state or result in creation of more than one instance of the cache. We could have synchronized the whole static accessor method instead, but that would have been an overkill as synchronization is only needed until the object is fully initialized; never again while accessing it.
No Lazy Initialization
An easier way would be to do away with the benefits of lazy initialization, also resulting in cleaner code:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
| public class ApplicationCache{ private Map<String, Object> attributeMap; // Initialized while declaration private static ApplicationCache instance = new ApplicationCache(); public static ApplicationCache getInstance(){ return instance; } // private Construcutor private ApplicationCache(){ attributeMap = createCache(); // Initialize the cache } } |
As soon as the class loads and the variables are initialized, we invoke the private constructor to create the one and only instance of the cache. We lose the benefits of lazily initializing the instance but the code is much cleaner. Both methods are thread-safe and you can choose the one suitable for your project environment.
Safeguard Against Reflection and Serialization
Depending on your requirement, you might also want to protect against:
- Code using Reflection API to invoke the private constructor, which can be dealt with by throwing an exception from the constructor, in case it is called more than one time.
- Similarly serializing and de-serializing the instance might also result in two different instances of our cache, which can be handled by overriding the
readResolve()
method from the Serialization API
Design Patterns are Language Agnostic
The title of our tutorial is a bit misleading, I admit, as design patterns are really language-agnostic. They are simply a collection of the best design strategies developed to counter recurring problems faced in software design. Nothing more, nothing less.
For example, below is a quick look at how we could implement a
singleton
in Javascript. The intent remains the same: controlling the creation of the object and maintaining a global point of access, but the implementation differs with the constructs and semantics of each language.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| var applicationCache = function () { // Private stuff var instance; function initCache() { return { proxyUrl: "/bin/getCache.json" , cachePurgeTime: 5000, permissions: { read: "everyone" , write: "admin" } }; } // Public return { getInstance: function () { if (!instance) instance = initCache(); return instance; }, purgeCache: function () { instance = null ; } }; }; |
To quote another example, jQuery also makes heavy use of the Facade design pattern, abstracting away the complexity of a subsystem and exposing a simpler interface to the user.
Closing Remarks
Not every problem requires the use of a specific design pattern
A word of caution is necessary: do not overuse! Not every problem requires the use of a specific design pattern. You need to carefully analyze the situation before settling on a pattern. Learning design patterns also helps in understanding other libraries like jQuery, Spring etc which make heavy use of many such patterns.
I hope that, after reading this article, you're able to get a step closer in your understanding of design patterns. If you have any questions or want to learn additional design pattern, please let me know within the comments below, and I will do my best to answer your questions!
No comments:
Post a Comment