Quick Summary:
Structural design patterns in Java organize classes and objects, promote code reusability, and simplify complex systems. They decouple components, manage relationships, and enhance maintainability. In this article, we will explore some commonly used Java structural design patterns and understand how they can enhance the overall architecture of software systems.
Structural design patterns in Java are essential for organizing and composing classes and objects to create robust and flexible software systems. These patterns focus on managing relationships between objects, decoupling components, and simplifying complex subsystems. Developers can employ these patterns to enhance code maintainability, promote reusability, and achieve a modular and scalable architecture.
These structural design pattern in Java allows you to create a class structure in a variety of ways, for as, by leveraging inheritance and composition to break down a huge object into smaller pieces. These Java structural design patterns combine diverse classes and objects to create larger structures with new capabilities. In this article, we will delve into various structural design patterns in Java and explore their practical applications in software development.
The structural design pattern in Java includes the following patterns:
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
Adapter Design Pattern
The adapter pattern in Java is a structural design pattern that allows two unrelated interfaces to communicate with each other. An adapter is a device that connects these disparate interfaces. A mobile charger, for example, might be an adaptor because the mobile battery requires 3 volts to charge, but the standard Socket provides either 120V (US) or 240V (EU) (India). As a result, the mobile charger acts as a converter between the mobile charging socket and the power outlet.
We’ll use an adapter design pattern to try to construct a multi-adapter.To begin, we’ll create two classes: Volt and Socket, which will measure volts and produce steady volts of 120V, respectively.
package com.aglowid.design.adapter;
public class Volt {
private int volts;
public Volt(int v){
this.volts=v;
}
public int getVolts() {
return volts;
}
public void setVolts(int volts) {
this.volts = volts;
}
}
package com.aglowid.design.adapter;
public class Socket {
public Volt getVolt(){
return new Volt(120);
}
}
We now want to construct an adaptor that can generate 3 volts, 12 volts, and 120 volts. So, using those methods, let’s make an adapter interface.
package com.aglowid.design.adapter;
public interface SocketAdapter {
public Volt get120Volt();
public Volt get12Volt();
public Volt get3Volt();
}
Two-way Adapter Pattern
Class Adapter and Object Adapter are two techniques for implementing the Adapter Pattern. Both procedures, however, must accomplish the same result.
Class Adapter: This form extends the source interface, in this case, the socket class, using Java inheritance.
Object Adapter: This form employs Java Composition; the source object is stored in the adapter.
Let’s look at some examples of both of these approaches.
The class adapter implementation of our adapter pattern is shown below.
package com.aglowid.design.adapter;
//Using inheritance for adapter pattern
public class SocketClassAdapterImpl extends Socket implements SocketAdapter{
@Override
public Volt get120Volt() {
return getVolt();
}
@Override
public Volt get12Volt() {
Volt v= getVolt();
return convertVolt(v,10);
}
@Override
public Volt get3Volt() {
Volt v= getVolt();
return convertVolt(v,40);
}
private Volt convertVolt(Volt v, int i) {
return new Volt(v.getVolts()/i);
}
}
Object adapter implementation
package com.aglowid.design.adapter;
public class SocketObjectAdapterImpl implements SocketAdapter{
//Using composition for adapter pattern
private Socket sock = new Socket();
@Override
public Volt get120Volt() {
return sock.getVolt();
}
@Override
public Volt get12Volt() {
Volt v= sock.getVolt();
return convertVolt(v,10);
}
@Override
public Volt get3Volt() {
Volt v= sock.getVolt();
return convertVolt(v,40);
}
private Volt convertVolt(Volt v, int i) {
return new Volt(v.getVolts()/i);
}
}
It’s worth noting that both adapter implementations are nearly identical and implement the Socket Adapter interface. An abstract class can also be used to describe the adapter interface.
Test program to implement our adapter design
package com.aglowid.design.test;
import com.aglowid.design.adapter.SocketAdapter;
import com.aglowid.design.adapter.SocketClassAdapterImpl;
import com.aglowid.design.adapter.SocketObjectAdapterImpl;
import com.aglowid.design.adapter.Volt;
public class AdapterPatternTest {
public static void main(String[] args) {
testClassAdapter();
testObjectAdapter();
}
private static void testObjectAdapter() {
SocketAdapter sockAdapter = new SocketObjectAdapterImpl();
Volt v3 = getVolt(sockAdapter,3);
Volt v12 = getVolt(sockAdapter,12);
Volt v120 = getVolt(sockAdapter,120);
System.out.println("v3 volts using Object Adapter="+v3.getVolts());
System.out.println("v12 volts using Object Adapter="+v12.getVolts());
System.out.println("v120 volts using Object Adapter="+v120.getVolts());
}
private static void testClassAdapter() {
SocketAdapter sockAdapter = new SocketClassAdapterImpl();
Volt v3 = getVolt(sockAdapter,3);
Volt v12 = getVolt(sockAdapter,12);
Volt v120 = getVolt(sockAdapter,120);
System.out.println("v3 volts using Class Adapter="+v3.getVolts());
System.out.println("v12 volts using Class Adapter="+v12.getVolts());
System.out.println("v120 volts using Class Adapter="+v120.getVolts());
}
private static Volt getVolt(SocketAdapter sockAdapter, int i) {
switch (i){
case 3: return sockAdapter.get3Volt();
case 12: return sockAdapter.get12Volt();
case 120: return sockAdapter.get120Volt();
default: return sockAdapter.get120Volt();
}
}
}
The Output of the program is as follows:
v3 volts using Class Adapter=3
v12 volts using Class Adapter=12
v120 volts using Class Adapter=120
v3 volts using Object Adapter=3
v12 volts using Object Adapter=12
v120 volts using Object Adapter=120
Adapter Design Patterns Class Diagram
Adapter Design Pattern Example in JDK
- Some of the adapter design pattern examples I could easily find in JDK classes are;
- util.Arrays#asList()
- io.InputStreamReader(InputStream) (returns a Reader)
- io.OutputStreamWriter(OutputStream) (returns a Writer)
Bridge Design Pattern
We’ll look at the Bridge Design Pattern in Java in this part. A bridge design pattern is used to decouple the interfaces from the implementation and hide the implementation details from the client program when both interfaces and implementations have interface hierarchies.
The bridge design pattern, like the Adapter pattern, is one of Java’s structural patterns. GoF claims that
Create a separation between an abstraction and its implementation so they can change independently.
The bridge design pattern is based on preferring composition over inheritance.
Bridge Design Pattern in Java Example
Suppose we investigate bridge design patterns, for example. It will be easy to understand. Let’s say we have an interface hierarchy in both interfaces and implementations like the below:
Now we will use a bridge design pattern to decouple the interfaces with implementation. UML diagram for the classes and interface after applying the bridge pattern will look like the below image:
The bridge between the Shape and Color interfaces, as well as the usage of composition in implementing the bridge pattern, should be noted. The Java code for the Shape and Color interfaces can be found here.
Color.java
package com.aglowid.design.bridge;
public interface Color {
public void applyColor();
}
Shape.java
package com.aglowid.design.bridge;
public abstract class Shape {
//composition - implementor
protected Color color;
//constructor with implementor as input argument
public Shape(Color c){
this.color=c;
}
abstract public void applyColor();
}
As shown below, we have Triangle and Pentagon, implementation classes.
Triangle.java
package com.aglowid.design.bridge;
public class Triangle extends Shape{
public Triangle(Color c) {
super(c);
}
@Override
public void applyColor() {
System.out.print("Triangle filled with color ");
color.applyColor();
}
}
Pentagon.java
package com.aglowid.design.bridge;
public class Pentagon extends Shape{
public Pentagon(Color c) {
super(c);
}
@Override
public void applyColor() {
System.out.print("Pentagon filled with color ");
color.applyColor();
}
}
Below are the implementation classes for RedColor and Yellow color.
Red Color.java
package com.aglowid.design.bridge;
public class RedColor implements Color{
public void applyColor(){
System.out.println("red.");
}
}
YellowColor.java
package com.aglowid.design.bridge;
public class GreenColor implements Color{
public void applyColor(){
System.out.println("green.");
}
}
Let’s test our bridge pattern implementation with the test program.
package com.aglowid.design.test;
import com.aglowid.design.bridge.GreenColor;
import com.aglowid.design.bridge.Pentagon;
import com.aglowid.design.bridge.RedColor;
import com.aglowid.design.bridge.Shape;
import com.aglowid.design.bridge.Triangle;
public class BridgePatternTest {
public static void main(String[] args) {
Shape tri = new Triangle(new RedColor());
tri.applyColor();
Shape pent = new Pentagon(new GreenColor());
pent.applyColor();
}
}
The Output of the following programming:
Triangle filled with the color red.
Pentagon filled with the color yellow.
When both abstraction and implementation have distinct hierarchies, and we wish to hide the implementation from the client application, the Bridge Design pattern can be employed.
Also Read: – Design Patterns in Java
Composite Design Pattern
When we need to depict a part-whole hierarchy, we use the Composite pattern, one of the Structural design patterns. The composite design pattern in Java can create a structure in which all objects must be treated similarly.
Let’s look at an example from the real world – A diagram is a structure made up of Objects like Circles, Lines, Triangles, and so on, and when we fill the drawing with color (say Yellow), the same color is applied to the Objects as well. The drawing comprises various elements that all do the same functions.
The Composite pattern consists of the following objects
Base Component: The base component serves as the interface for all of the composition’s objects, and the client program interacts with them via the base component. It could be an interface or an abstract class containing methods that are shared by all objects.
Leaf: Defines the behavior of the composition’s elements. It is a component that implements base components and serves as a building block for the composition. There are no references to other components in it.
Composite: It comprises leaf elements and implements the base component’s operators.
Here composite design pattern in Java is used for the drawing scenario.
Composite pattern Base component
The composite pattern base component defines the standard methods for leaves and composites. We can construct a class Shape with a function draw (String fillColor) to draw the form with the specified color.
Shape.java
package com.aglowid.design.composite;
public interface Shape {
public void draw (String fillColor);
}
Composite Design pattern leaf object
Composite design pattern leaf implements base components, the building blocks for composite design patterns.
Triangle.java
package com.aglowid.design.composite;
public class Triangle implements Shape {
@Override
public void draw(String fillColor) {
System.out.println("Drawing Triangle with color "+fillColor);
}
}
Circle.java
package com.aglowid.design.composite;
public class Circle implements Shape {
@Override
public void draw(String fillColor) {
System.out.println("Drawing Circle with color "+fillColor);
}
}
Composite Object
We should provide some helpful methods to add and remove a leaf object from a composite object’s group of leaf objects. We can also offer a mechanism for removing all of the group’s elements.
Drawing.java
package com.aglowid.design.composite;
import java.util.ArrayList;
import java.util.List;
public class Drawing implements Shape{
//collection of Shapes
private List shapes = new ArrayList();
@Override
public void draw(String fillColor) {
for(Shape sh : shapes)
{
sh.draw(fillColor);
}
}
//adding shape to drawing
public void add(Shape s){
this.shapes.add(s);
}
//removing shape from drawing
public void remove(Shape s){
shapes.remove(s);
}
//removing all the shapes
public void clear(){
System.out.println("Clearing all the shapes from drawing");
this.shapes.clear();
}
}
Composite design Class Diagram
Composite implements components and functions similarly to leaf, with the exception that it can include a group of leaf elements.
Composite Design pattern Test program
Testcompositepattern.java
package com.aglowid.design.test;
import com.aglowid.design.composite.Circle;
import com.aglowid.design.composite.Drawing;
import com.aglowid.design.composite.Shape;
import com.aglowid.design.composite.Triangle;
public class TestCompositePattern {
public static void main(String[] args) {
Shape tri = new Triangle();
Shape tri1 = new Triangle();
Shape cir = new Circle();
Drawing drawing = new Drawing();
drawing.add(tri1);
drawing.add(tri1);
drawing.add(cir);
drawing.draw("Red");
drawing.clear();
drawing.add(tri);
drawing.add(cir);
drawing.draw("Green");
}
}
The Output of the program is:
Drawing Triangle with color Red
Drawing Triangle with color Red
Drawing Circle with color Red
Clearing all the shapes from drawing
Drawing Triangle with color Green
Drawing Circle with color Green
Composite Design pattern Key points
- When a set of items has to behave like a single object, the composite pattern should be used.
- A tree-like structure can be created using the composite design pattern.
- The Composite design in Java is well-exemplified by Java. awt.Container#add(Component), which is widely used in Swing.
Also Read: – Top Java Development Tools to utilize
Proxy Design Patterns
Proxy design patterns are one of the structural design patterns in Java and are the simplest pattern to understand. It also intent according to GoF Provide a surrogate or placeholder for another object to control access to it.
Let’s pretend we have a class that can run a system command. Now, if we utilize it, it’s great, but if we send it to a client application, it could cause serious problems since the client program could issue a command that deletes system files or changes settings that you don’t want.
A proxy class can be established here to provide the program with controlled access.
Proxy Design Pattern Main Class
CommandExecuter.java
package com.aglowid.design.proxy;
public interface CommandExecutor {
public void runCommand(String cmd) throws Exception;
}
CommandExecuterImpl.java
package com.aglowid.design.proxy;
import java.io.IOException;
public class CommandExecutorImpl implements CommandExecutor {
@Override
public void runCommand(String cmd) throws IOException {
//some heavy implementation
Runtime.getRuntime().exec(cmd);
System.out.println("'" + cmd + "' command executed.");
}
}
Proxy design pattern – proxy class
Only admin users will have complete access to the above class; if the user is not an admin, only limited commands will be available. Here’s how we implemented our simple proxy class.
CommandExecutorProxy.java
package com.aglowid.design.proxy;
public class CommandExecutorProxy implements CommandExecutor {
private boolean isAdmin;
private CommandExecutor executor;
public CommandExecutorProxy(String user, String pwd){
if("Pankaj".equals(user) && "J@urnalD$v".equals(pwd)) isAdmin=true;
executor = new CommandExecutorImpl();
}
@Override
public void runCommand(String cmd) throws Exception {
if(isAdmin){
executor.runCommand(cmd);
}else{
if(cmd.trim().startsWith("rm")){
throw new Exception("rm command is not allowed for non-admin users.");
}else{
executor.runCommand(cmd);
}
}
}
}
Proxy Design Pattern Client Program
ProxyPatternTest.java
package com.aglowid.design.test;
import com.aglowid.design.proxy.CommandExecutor;
import com.aglowid.design.proxy.CommandExecutorProxy;
public class ProxyPatternTest {
public static void main(String[] args){
CommandExecutor executor = new CommandExecutorProxy("Pankaj", "wrong_pwd");
try {
executor.runCommand("ls -ltr");
executor.runCommand(" rm -rf abc.pdf");
} catch (Exception e) {
System.out.println("Exception Message::"+e.getMessage());
}
}
}
The Output for the program is as follows:
'ls -ltr' command executed.
Exception Message::rm command is not allowed for non-admin users.
Two common uses of the proxy design pattern are controlling access or providing a wrapper implementation for greater efficiency.
Flyweight Design Pattern
When we need to construct a large number of class objects, we use the flyweight design patterns in Java. Because each Object requires memory space, which is critical for low-memory devices like mobile phones and embedded systems, the flyweight design pattern can be used to lessen memory load by sharing objects. One of the better instances of Flyweight pattern implementation in Java is the String Pool.
According to GoF, the flyweight design pattern intends to use sharing to efficiently support large numbers of fine-grained objects.
We must examine the following factors before using the flyweight design pattern:
- The application should be able to create a large number of objects
- The object construction process is memory intensive and time-consuming
- The client program should define the extrinsic qualities of an object; the client program should define intrinsic properties of an object
We must divide Object properties into intrinsic and extrinsic qualities to apply the flyweight pattern. Extrinsic properties are set by the client code and utilized to conduct distinct actions, whereas intrinsic properties make the Object unique. An Object Circle, for example, can have extrinsic attributes like color and width.
We need to develop a Flyweight factory that returns shared objects to apply the flyweight pattern. Let’s imagine we need to make a drawing using lines and ovals for our example. As a result, we’ll have a Shape interface with concrete implementations such as Line and Oval. The Oval class will have an inherent attribute that determines whether to fill the Oval with a given hue, whereas the Line class will not.
Flyweight Desing Pattern Interface & Concrete Classes
Shape.java
package com.aglowid.design.flyweight;
import java.awt.Color;
import java.awt.Graphics;
public interface Shape {
public void draw(Graphics g, int x, int y, int width, int height,Color color);
}
Line.java
package com.aglowid.design.flyweight;
import java.awt.Color;
import java.awt.Graphics;
public class Line implements Shape {
public Line(){
System.out.println("Creating Line object");
//adding time delay
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void draw(Graphics line, int x1, int y1, int x2, int y2,
Color color) {
line.setColor(color);
line.drawLine(x1, y1, x2, y2);
}
}
Oval.java
package com.aglowid.design.flyweight;
import java.awt.Color;
import java.awt.Graphics;
public class Oval implements Shape {
//intrinsic property
private boolean fill;
public Oval(boolean f){
this.fill=f;
System.out.println("Creating Oval object with fill="+f);
//adding time delay
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void draw(Graphics circle, int x, int y, int width, int height,
Color color) {
circle.setColor(color);
circle.drawOval(x, y, width, height);
if(fill){
circle.fillOval(x, y, width, height);
}
}
}
Are you looking to hire java developer?
Get the top-notch web & app solutions from the certified java developers at Aglowid
Flyweight Factory
Client programs will utilize the flyweight factory to instantiate the Object, so we’ll need to preserve a list of Objects in the factory that shouldn’t be available to client programs.
If a client program requests an instance of Object, it should be returned from the HashMap; if one is not found, a new Object should be created and placed in the Map, and then returned. We must ensure that all intrinsic properties are considered while generating the Object.
The code for our flyweight factory class is as follows.
shapeFactory.java
package com.aglowid.design.flyweight;
import java.util.HashMap;
public class ShapeFactory {
private static final HashMap<ShapeType,Shape> shapes = new HashMap<ShapeType,Shape>();
public static Shape getShape(ShapeType type) {
Shape shapeImpl = shapes.get(type);
if (shapeImpl == null) {
if (type.equals(ShapeType.OVAL_FILL)) {
shapeImpl = new Oval(true);
} else if (type.equals(ShapeType.OVAL_NOFILL)) {
shapeImpl = new Oval(false);
} else if (type.equals(ShapeType.LINE)) {
shapeImpl = new Line();
}
shapes.put(type, shapeImpl);
}
return shapeImpl;
}
public static enum ShapeType{
OVAL_FILL,OVAL_NOFILL,LINE;
}
}
In the getShape method, note using Java Enum for type safety, Java Composition (shapes map), and Factory pattern.
Flyweight Design Pattern Client example
DrawingClient.java
package com.aglowid.design.flyweight;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import com.journaldev.design.flyweight.ShapeFactory.ShapeType;
public class DrawingClient extends JFrame{
private static final long serialVersionUID = -1350200437285282550L;
private final int WIDTH;
private final int HEIGHT;
private static final ShapeType shapes[] = { ShapeType.LINE, ShapeType.OVAL_FILL,ShapeType.OVAL_NOFILL };
private static final Color colors[] = { Color.RED, Color.GREEN, Color.YELLOW };
public DrawingClient(int width, int height){
this.WIDTH=width;
this.HEIGHT=height;
Container contentPane = getContentPane();
JButton startButton = new JButton("Draw");
final JPanel panel = new JPanel();
contentPane.add(panel, BorderLayout.CENTER);
contentPane.add(startButton, BorderLayout.SOUTH);
setSize(WIDTH, HEIGHT);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
startButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
Graphics g = panel.getGraphics();
for (int i = 0; i < 20; ++i) {
Shape shape = ShapeFactory.getShape(getRandomShape());
shape.draw(g, getRandomX(), getRandomY(), getRandomWidth(),
getRandomHeight(), getRandomColor());
}
}
});
}
private ShapeType getRandomShape() {
return shapes[(int) (Math.random() * shapes.length)];
}
private int getRandomX() {
return (int) (Math.random() * WIDTH);
}
private int getRandomY() {
return (int) (Math.random() * HEIGHT);
}
private int getRandomWidth() {
return (int) (Math.random() * (WIDTH / 10));
}
private int getRandomHeight() {
return (int) (Math.random() * (HEIGHT / 10));
}
private Color getRandomColor() {
return colors[(int) (Math.random() * colors.length)];
}
public static void main(String[] args) {
DrawingClient drawing = new DrawingClient(500,600);
}
}
Here random number generation is used to generate different types of Shapes in our frame.
If you run the above client program, you’ll notice a delay in the creation of the first Line Object and Oval Object with true and false fill. The application then runs rapidly because it makes use of shared objects.
The frame looks like this after several clicks on the “Draw” button.
Output in the command line confirming that objects are shared.
Creating Line object
Creating Oval Object with fill=true
Creating Oval Object with fill=false
Flyweight Design Pattern Real-life Example
The function value Of()method in all wrapper classes leverages cached objects, demonstrating the application of the Flyweight design principle. The String Pool implementation in Java is the greatest example.
Key points for Flyweight Design Patterns in Java
- In our example, the client code is not compelled to generate objects using the Flyweight factory, but we can force it to ensure that the client code implements the Flyweight pattern, but this is a complete design decision for the application.
- The flyweight pattern adds complexity, and if the number of shared objects is large, there is a trade-off between memory and time, so we must utilize it wisely based on our needs.
- When the number of intrinsic attributes of an Object is large, the Flyweight design is ineffective, making Factory class implementation difficult.
Facade Design Pattern in Java
The Facade Pattern makes communicating with the system easier for client applications. Assume we have a program with a set of interfaces for accessing a MySQL/Oracle database and generating various types of reports, such as HTML reports, PDF reports, and so on. As a result, we’ll have a variety of interfaces to work with various database types. A client program can now use these interfaces to obtain the required database connection and generate reports. However, as the complexity grows and the names of the interface behaviours become more ambiguous, the client application will struggle to keep up. As a result, we can use the Facade approach to create a wrapper interface on top of the existing interface.
According to GoF, it provides,
a set of interfaces in a subsystem with a single, unified interface. The higher-level interface defined by the Facade Pattern simplifies the subsystem’s utilisation.
Assume we have an application with a set of interfaces for accessing a MySql/Oracle database and producing several types of reports, such as HTML and PDF reports.
As a result, we’ll have a variety of interfaces to work with various database types. A client program can now use these interfaces to obtain the required database connection and generate reports.
However, when the complexity rises or the names of the interface behaviors get muddled, the client application will struggle to keep up.
As a result, we can use the Facade design pattern to support the client application by providing a wrapper interface on top of the existing interface.
Facade Design Pattern – Set of Interface
We have two helper interfaces namely SQLHelper and OracleHelper.
SQLHelper.java
package com.aglowid. Facade.design;
import java.sql.Connection;
public class MySqlHelper {
public static Connection getMySqlDBConnection(){
//get MySql DB connection using connection parameters
return null;
}
public void generateMySqlPDFReport(String tableName, Connection con){
//get data from table and generate pdf report
}
public void generateMySqlHTMLReport(String tableName, Connection con){
//get data from table and generate pdf report
}
}
Oracle.java
package com.aglowid.design.facade;
import java.sql.Connection;
public class OracleHelper {
public static Connection getOracleDBConnection(){
//get Oracle DB connection using connection parameters
return null;
}
public void generateOraclePDFReport(String tableName, Connection con){
//get data from table and generate pdf report
}
public void generateOracleHTMLReport(String tableName, Connection con){
//get data from table and generate pdf report
}
}
Facade Design pattern interface
package com.aglowid.design.facade;
import java.sql.Connection;
public class HelperFacade {
public static void generateReport(DBTypes dbType, ReportTypes reportType, String tableName){
Connection con = null;
switch (dbType){
case MYSQL:
con = MySqlHelper.getMySqlDBConnection();
MySqlHelper mySqlHelper = new MySqlHelper();
switch(reportType){
case HTML:
mySqlHelper.generateMySqlHTMLReport(tableName, con);
break;
case PDF:
mySqlHelper.generateMySqlPDFReport(tableName, con);
break;
}
break;
case ORACLE:
con = OracleHelper.getOracleDBConnection();
OracleHelper oracleHelper = new OracleHelper();
switch(reportType){
case HTML:
oracleHelper.generateOracleHTMLReport(tableName, con);
break;
case PDF:
oracleHelper.generateOraclePDFReport(tableName, con);
break;
}
break;
}
}
public static enum DBTypes{
MYSQL, ORACLE;
}
public static enum ReportTypes{
HTML,PDF;
}
}
Also Read: – Creational Design Patterns in Java
Facade Design Pattern Test Program
package com.aglowid.design.test;
import java.sql.Connection;
import com.aglowid.design. Facade.HelperFacade;
import com.aglowid.design. Facade.MySqlHelper;
import com.aglowid.design. Facade.OracleHelper;
public class FacadePatternTest {
public static void main(String[] args) {
String tableName="Employee";
//generating MySql HTML report and Oracle PDF report without using Facade
Connection con = MySqlHelper.getMySqlDBConnection();
MySqlHelper mySqlHelper = new MySqlHelper();
mySqlHelper.generateMySqlHTMLReport(tableName, con);
Connection con1 = OracleHelper.getOracleDBConnection();
OracleHelper oracleHelper = new OracleHelper();
oracleHelper.generateOraclePDFReport(tableName, con1);
//generating MySql HTML report and Oracle PDF report using Facade
HelperFacade.generateReport(HelperFacade.DBTypes.MYSQL, HelperFacade.ReportTypes.HTML, tableName);
HelperFacade.generateReport(HelperFacade.DBTypes.ORACLE, HelperFacade.ReportTypes.PDF, tableName);
}
}
As you can see, adopting a Facade pattern interface to avoid having a lot of code on the client-side is a lot easier and clearer. The JDBC Driver Manager class, which is used to obtain the database connection, is an excellent illustration of the façade design pattern.
Key things to remember while using Facade Design Pattern
- The Facade design pattern is more of a companion for client applications; it does not conceal subsystem interfaces from the client. Whether or not to utilize Facade is entirely based on the client code.
- The facade design pattern can be used at any stage of development, but it is most used as the number of interfaces grows and the system becomes more complicated.
- The Facade is not known to subsystem interfaces, and they should not contain any references to it.
- The facade design pattern should be used for comparable types of interfaces because its goal is to provide a single interface rather than several interfaces that perform the same tasks.
- We can use the Factory pattern with Facade to give a better client system interface.
Java Decorator Pattern
The decorator design pattern in Java changes an object’s functionality during runtime. At the same time, other instances of the same class will be unaffected, therefore each Object will have its behavior changed. The decorator design pattern is a part of the structural design pattern (together with the Adapter, Bridge, and Composite patterns) that employs abstract classes or interfaces with the composition to implement.
To expand the behaviour of an object, we utilize inheritance or composition, but this is done at compile–time and applies to all instances of the class. At runtime, we can’t introduce new functionality to remove current behaviour — this is where the java decorator pattern comes in.
If we want to build many types of automobiles, we can design the Car interface to specify the assembly process, and then we can have a Basic car, which we can then enhance into a Sports car and Luxury car. The implementation hierarchy will be like the image below.
However, if we want a car that has both sports car and luxury car features at runtime, the implementation becomes more complicated, and if we want to specify which features should be introduced first, it becomes even more complicated. Consider how difficult it would be to organize the implementation logic using inheritance and composition if we had ten different types of cars. The decorator pattern in Java is used to solve this type of programming problem.
To use the decorator design pattern, we’ll need the following types.
- Component interface: The methods that will be implemented are defined by the interface or abstract class. The car will be the component interface in our example.
package com.aglowid.design.decorator;
public interface Car {
public void assemble();
}
- Component Implementation: The component interface’s basic implementation. As a component implementation, we can use the BasicCar class.
package com.aglowid.design.decorator;
public class BasicCar implements Car {
@Override
public void assemble() {
System.out.print("Basic Car.");
}
}
- Decorator: Decorator is a class that implements the component interface with a HAS-A connection. Because we want the child decorator classes to be able to access the component variable, we’ll make it protected.
package com.aglowid.design.decorator;
public class CarDecorator implements Car {
protected Car cr;
public CarDecorator(Car c){
this.cr=c;
}
@Override
public void assemble() {
this.cr.assemble();
}
}
Concrete Decorator:
Extending the base decorator’s capabilities and changing the component’s behavior as a result. Concrete decorator classes such as LuxuryCar and SportsCar are possible.
package com.aglowid.design.decorator;
public class SCar extends CarDecorator {
public SCar(Car c) {
super(c);
}
@Override
public void assemble(){
super.assemble();
System.out.print(" Adding features of Sports Car.");
}
}
package com.aglowid.design.decorator;
public class LCar extends CarDecorator {
public LCar(Car c) {
super(c);
}
@Override
public void assemble(){
super.assemble();
System.out.print(" Adding features of Luxury Car.");
}
}
Decorator Design Pattern – Class Diagram
Decorator Design Pattern Test Program
package com.aglowid.design.test;
import com.aglowid.design.decorator.BCar;
import com.aglowid.design.decorator.Car;
import com.aglowid.design.decorator.LCar;
import com.aglowid.design.decorator.SCar;
public class DecoratorPatternTest {
public static void main(String[] args) {
Car SCar = new SCar(new BCar());
SCar.assemble();
System.out.println("\n*****");
C sLuxuryCar = new SCar(new LCar(new BCar()));
sLuxuryCar.assemble();
}
}
Client programs can construct several types of Objects at runtime and determine the order in which they should be executed.
The Output of the above test program is as follows:
Basic Car. Adding features of Sports Car.
*****
Basic Car. Adding features of Luxury Car. Adding features of Sports Car.
Key Points for Decorator Design Pattern
- The Decorator design pattern is beneficial in that it allows for runtime adjustment, making it more versatile. When there are more options, it is easier to sustain and expand.
- The drawback of the decorator design pattern is that it employs many similar elements (decorators).
- The decorator technique is frequently utilized in Java IO classes like FileReader and BufferedReader.
Use case of Structural Design Pattern
An adapter design pattern is used when two interfaces are incompatible and want to establish a relationship using an adapter. The adapter pattern transforms a class’s interface into another interface or class that the client expects, allowing classes that would otherwise be incompatible to operate together. We can use the adapter pattern in these types of incompatible instances.
Wrapping Up!
Structural design patterns are essential in Java development for creating well-structured and maintainable software systems. These patterns enable the organization of classes and objects, managing relationships, and simplifying complex subsystems. By utilizing the Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy patterns, developers can enhance code reuse, promote modularity, and improve the overall architecture of their Java applications. Understanding and applying these structural design patterns in the appropriate contexts empowers developers to create robust and scalable software solutions.
have a unique app Idea?
Hire Certified Developers To Build Robust Feature, Rich App And Websites