on
Java Core Structural Patterns
Structural patterns show how to compose classes and objects into larger structures while keeping flexibility and efficiency. We can find them in the Gang of Four book. In this post, we will see seven of them.
Adapter
Allows objects with incompatible interfaces to collaborate.
It is useful to make an application work with a legacy class whose source code cannot be modified, a 3rd-party class, or any other class with a weird interface.
Methods takes an instance of different abstract/interface type and returns an implementation of own/another abstract/interface type which decorates/overrides the given instance. JDK’s collection framework provides Arrays class where it uses the adapter pattern:
List<String> cities = Arrays.asList("Iliya", "Aelia");
InputStreamReader(InputStream) (returns a Reader);
OutputStreamWriter(OutputStream) (returns a Writer);
Bridge
Allows splitting a set of closely related classes into two separate hierarchies—which can be developed and used independently.
It attempts to solve this problem by switching from inheritance to composition. The idea is to extract one of the dimensions into a separate class hierarchy so that the original classes will reference an object of the new hierarchy, instead of having all of its state and behaviors within one class.
Methods take an instance of different abstract/interface type and return an implementation of own abstract/interface type that delegates/use the given instance.
JDBC API acts as a link between the databases such as PostgreSQL and their particular implementations. Data access code using JDBC depends only on interfaces such as Connection
, Statement
or ResultSet
, instead of caring about the actual database system the application is connected to.
Connection connection = DriverManager.getConnection(url);
Composite
Allows to compose objects into tree structures and then work with these structures as if they were individual objects.
Methods take an instance of the same abstract/interface type into a tree structure.
Containers objects in Swing are great examples of the composite pattern in Java. JTabbedPane
, JButton
, JCheckBox
, and JFrame
– are descendants of Container
.
JTabbedPane pane = new JTabbedPane();
pane.addTab("1", new Container());
pane.addTab("2", new JButton());
pane.addTab("3", new JCheckBox());
Decorator
Allows attaching new behaviors to objects by placing these objects inside particular wrapper objects that contain the behaviors.
It enhances the behavior of an object without modifying the original object itself.
In the next example, BufferedInputStream
is decorating the FileInputStream
to add the capability to buffer the input.
BufferedInputStream binputStream = new BufferedInputStream(
new FileInputStream(new File("file.txt"))
);
while (binputStream.available() > 0) {
char c = (char) binputStream.read();
System.out.println("Char: " + c);
}
Façade
Provides a simplified interface to a library, a framework, or any other complex set of classes.
It means shields the user from the complex details of the system and provides them with a simplified view
of it, which is easy to use
where methods internally use instances of different independent abstract/interface types.
When you call a shop to place a phone order, an operator is your façade to all the shop’s services and departments.
javax.faces.context.ExternalContext
, which internally uses ServletContext
, HttpSession
, HttpServletRequest
, HttpServletResponse
, etc.
Flyweight
Allows fitting objects into the available amount of RAM by sharing common parts of the state between multiple objects instead of keeping all of the data in each object.
In other words, if we have immutable objects that can share state, as per this pattern, we can cache them to improve system performance.
As example, we list java.lang.Integer#valueOf(int)
(also on Boolean
, Byte
, Character
, Short
, Long
and BigDecimal
).
Proxy
Allows providing a substitute or placeholder for another object.
Like the façade pattern, the proxied object interface is the same as that of the proxy.
A proxy instance serviced by the invocation handler we have just defined is created via a factory method call on the java.lang.reflect.Proxy
class:
Map proxyInstance = (Map) Proxy.newProxyInstance(
DynamicProxyTest.class.getClassLoader(),
new Class[] { Map.class },
(proxy, method, methodArgs) -> {
...
});
Once we have a proxy instance, we can invoke its interface methods as normal:
proxyInstance.put("hello", "world");
Conclusion
In this post, we saw practical usages of structural design patterns implemented in Java.