Master the art of clean, scalable object-oriented programming in Java with design patterns like Singleton, Factory, and Observer—your ultimate guide to creating flexible, change-friendly code.
In OO design, you’ve learned that classes should not be tightly coupled, but instead loosely coupled. This suggestion means classes shouldn’t know too much about other classes, and interactions should be through well-defined interfaces so that classes can still be modified subsequently. Loose coupling has many advantages, including increasing reusability and making the program more change friendly.
Let’s use an example in Java to explain these features: Customer data must be stored in a data structure. For this data source, a graphical user interface (GUI), such as an input screen, displays and manages the data. When data is entered, deleted, and changed, these changes should be transferred to the data structure. (We’ll describe the other way, from the data structure into the visualization, momentarily.)
Although we already have a link between the input screen and the data structure, we must be careful not to get bogged down in the design because presumably the programming boils down to the two being tightly linked. Most likely, the GUI will somehow know about the data structure, and any change in the input screen will directly call methods of the concrete data structure, which is what we want to avoid.
Moreover, we haven’t considered what will happen if, as a result of further program versions, a graphical representation of the data is now drawn, for example, in the form of a bar chart. What happens if the content of the data structure is changed via another program location and then forces a rebuild of the screen display? In this scenario, we’ll be caught in a tangle of method calls, and our program is no longer change-friendly. What happens if we now want to replace our homemade data structure with an SQL database?
Motivation for Design Patterns
Considerations about basic design criteria go way back. Prior to object-oriented programming (OOP), with structured programming, developers were happy to use various tools to build software more quickly and more easily. Assembler programmers were also happy to use structured programming to increase efficiency—after all, they only used subroutines because they could save a few bytes again.
But after Assembler and structured programming, we’ve now arrived at object orientation, and no revolutionary programming paradigm has since emerged to replace object orientation. The software crisis has led to new concepts. Almost every development team realizes that OO isn’t everything, but might be rather surprised after years of development work and say, “Uh-oh, it’s all crap.” As beautiful as OO can be, having 10,000 classes cavort in a class diagram is just as confusing as a Fortran program with 10,000 lines.
Since good design was often sacrificed for a few milliseconds of runtime in the past, unsurprisingly, some programs are no longer readable. However, as illustrated by the typesetting program TeX (circa 1985), code lives longer than hardware, and the next generation of multi-core processors will soon be yearning for work in our desktop PCs.
Accordingly, a level above the individual classes and objects is missing because the objects themselves are not the problem, rather the coupling causes issues. The coupling is where rules that have become known as design patterns come in handy. These are tips from software designers who had noticed that many problems can be solved in a similar way.
For this reason, they’ve established sets of rules with solution patterns that show an optimal reuse of components and ease of change. Design patterns run throughout the Java class library, and the best known are the observer, singleton, factory, and composite patterns.
Singleton
A singleton is a class of which there’s only one instance in an application. This limitation is useful for things that should only exist exactly once in an application, for instance, in the following examples:
- A graphical application has only one window.
- A console application has only one input/output stream each.
- All print jobs go into a printer maintenance queue.
Indisputably, unique objects must exist, but the way to achieve them is full of variation. Basically, a distinction can be made between the following two approaches:
- A framework takes care of the one-time construction of the object and then returns the object on request.
- You implement a singleton in Java code yourself.
The better solution is to use a framework, for instance, CDI, Spring, Guice, or Jakarta EE (formerly Java EE). But Java SE doesn’t contain any of these frameworks, which is why we want to explicitly highlight them for demonstration.
The technical implementations are versatile; in Java, enumerations (enum) and normal classes are suitable for the implementation of singletons. In the following sections, we’ll assume a scenario where an application wants to access configuration data centrally.
Singletons over Enumerations
A good way to use singletons are as enumerations—at first sight, an enumeration type doesn’t seem to designed for singletons because an enumeration somehow implies more than one element. But the properties of enum are perfect for a singleton, and the library implements some tricks to also create the object only once if possible, such as when the enumeration is serialized over the wire and then deserialized and reconstructed. The idea is to provide exactly one element (often referred to as an INSTANCE) that will eventually become the only instance of the enumeration class, as well as the accompanying methods.
public enum Configuration {
INSTANCE;
private final Properties props = new Properties( System.getProperties() );
public String getVersion() {
return "1.2";
}
public String getUserDir() {
return props.getProperty( "user.dir" );
}
}
In addition to the later public static INSTANCE variable, the Configuration type declares also an internal props variable, which can be used by the enumeration to store or request states there. We do this read only in the example via getUserDir().
A user accesses the enum members as usual in the following example:
System.out.println( Configuration.INSTANCE.getVersion() ); // 1.2
System.out.println( Configuration.INSTANCE.getUserDir() ); // C:\Users\…
Factory Methods
A factory method goes one step further than a singleton. This method doesn’t generate exactly one instance but may generate several. However, the basic idea is that the user doesn’t create a instance via a constructor, but generally via a static method. This approach has an advantage in that a static factory method can do the following things:
- Can return old objects from a cache
- Can move the creation process to subclasses
- Can return null
A constructor always creates an instance of its own class. A constructor can’t return something like null because, with new, a new object is always created. Errors could only be displayed via an exception.
Many examples of factory methods exist in the Java library. A naming convention makes them easy to recognize: They’re called getInstance() most of the time. A search in the API documentation reveals 90 such methods. Many of these methods are parameterized to specify exactly what objects the factory should produce. For example, let’s consider some static factory methods of java.util.Calendar, such as the following:
- getInstance()
- getInstance( java.util.Locale )
- getInstance( java.util.TimeZone )
The non-parameterized method returns a default Calendar object. However, Calendar is itself an abstract base class. Inside the getInstance(...) method, you’ll find the following source code:
static Calendar getInstance() {
…
return new GregorianCalendar();
…
}
In the body of the getInstance(...) factory method, the GregorianCalendar subclass is deliberately selected. This subclass extends Calendar and is possible because, by inheritance, the subclass GregorianCalendar is a Calendar. The caller of getInstance(...) doesn’t see this precise type and receives a Calendar object as desired. This option enables getInstance(...) to test in which country the Java virtual machine (JVM) is running and, depending on that country, select the appropriate Calendar implementation.
Implementing the Observer Pattern with Listeners
Let’s now explore the observer pattern. This pattern, with its origins in Smalltalk-80, is also known under the name Model View Controller (MVC). Let’s take a closer look at this pattern, which is an essential concept when programming GUIs with Swing.
Listeners allow for an implementation of the observer pattern. Event triggers send out special event objects, and interested parties log in and out of the triggers. The classes and interfaces involved follow a specific naming convention; * is used to represent an event name, such as Window, Click, etc.
Working with this pattern, you should keep in mind the following considerations:
- A class for the event objects is called *Event. The event objects can store information such as triggers, timestamps, and other data.
- The interested parties implement a Java interface called *Listener as a listener. You are free to choose any method name, but usually the *Event is passed to it. This interface can also prescribe multiple operations.
- The event trigger provides the add*Listener(*Listener) and remove*Listener(*Listener) methods to subscribe and unsubscribe interested parties. Whenever an event occurs, the trigger creates the event object *Event and informs each listener entered in the list about a call of the method from the listener.
An example will illustrate the types involved.
Radio Advertisements
Let’s say a radio must broadcast AdEvent objects for advertising. The event objects will store the advertising slogan. Consider the following example:
package com.tutego.insel.pattern.listener;
import java.util.EventObject;
public class AdEvent extends EventObject {
private final String slogan;
public AdEvent( Object source, String slogan ) {
super( source );
this.slogan = slogan;
}
public String getSlogan() {
return slogan;
}
}
The AdEvent class extends the Java base class EventObject, a class that traditionally extends all event classes. The parameterized constructor of AdEvent takes the event trigger in the first parameter and passes it to the superclass constructor with super(source), which stores it and makes it available again with getSource(). The use of the base class isn’t necessarily mandatory and has not been generically adapted over the years, so source is merely Object. The second parameter of the AdEvent constructor is our advertising.
The AdListener is the interface that interested parties implement, as shown in the following:
package com.tutego.insel.pattern.listener;
import java.util.EventListener;
interface AdListener extends EventListener {
void advertisement( AdEvent e );
}
Our AdListener implements the EventListener interface (a marker interface), which all Java listeners are supposed to implement. We’ll specify only one advertisement(AdEvent) operation for concrete listeners. You must carefully consider whether the interface should carry @FunctionalInterface because, in the future, the event trigger might want to call another method to report something else, for example.
The radio can now register and deregister interested parties and can send advertising messages via a timer. The exciting thing about this implementation is that the listeners aren’t managed in their own data structure, but instead, a special listener class from the Swing package is used.
package com.tutego.insel.pattern.listener;
import java.util.*;
import javax.swing.event.EventListenerList;
public class Radio {
private final EventListenerList listeners = new EventListenerList();
private final List<String> ads = List.of( "A Bite of Heaven.",
"Bag the sweets and run.",
"Chew on this, for a while.",
"Taste the explosion." );
public Radio() {
new Timer().schedule( new TimerTask() {
@Override public void run() {
Collections.shuffle( ads );
notifyAdvertisement( new AdEvent( this, ads.get(0) ) );
}
}, 0, 500 );
}
public void addAdListener( AdListener listener ) {
listeners.add( AdListener.class, listener );
}
public void removeAdListener( AdListener listener ) {
listeners.remove( AdListener.class, listener );
}
protected synchronized void notifyAdvertisement( AdEvent event ) {
for ( AdListener l : listeners.getListeners( AdListener.class ) )
l.advertisement( event );
}
}
This demo application uses the Radio object and implements a concrete listener, once via an inner anonymous class and once via a lambda expression.
Radio r = new Radio();
class ComplainingAdListener implements AdListener {
@Override public void advertisement( AdEvent e ) {
System.out.println( "Oh no, advertising again: " + e.getSlogan() );
}
}
r.addAdListener( new ComplainingAdListener() );
r.addAdListener( e -> System.out.println( "I hear nothing" ) );
The Java API documentation contains some generic types:
class javax.swing.event.EventListenerList
- EventListenerList(): Creates a container for listeners.
- <T extends EventListener> void add(Class<T> t, T l): Adds a listener l of type T.
- Object[] getListenerList(): Returns an array of all listeners.
- <T extends EventListener> T[] getListeners(Class<T> t): Returns an array of all listeners of type t.
- int getListenerCount(): Specifies the number of all listeners.
- int getListenerCount(Class<?> t): Specifies the number of listeners of type t.
- <T extends EventListener> void remove(Class<T> t, T l): Removes the listener l from the list.
Editor’s note: This post has been adapted from a section of the book Java: The Comprehensive Guide by Christian Ullenboom.
Comments