Overview
This is the second article in a short series dedicated to Design Patterns in Java, and a direct continuation from the previous article - Creational Design Patterns in Java.
Structural Patterns
Structural Patterns are concerned about providing solutions and efficient standards regarding class compositions and object structures. Also, they rely on the concept of inheritance and interfaces to allow multiple objects or classes to work together and form a single working whole.
The Structural Patterns in Java that are covered in this article are:
Adapter
The Adapter pattern, as the name implies, adapts one interface to another. It acts as a bridge between two unrelated, and sometimes even completely incompatible interfaces, similar to how a scanner acts as a bridge between a paper and a computer.
A computer can't store a paper as a PDF document, but a scanner, which combines the functionalities of both, can scan it and allow the computer to store it.
Implementation
The Builder
interface is our most general interface, and it provides a method that accepts a building type and its location:
public interface Builder {
public void build(String type, String location);
}
The AdvancedBuilder
interface provides two methods, one to build a house, and one to build a skyscrapper:
public interface AdvancedBuilder {
public void buildHouse(String location);
public void buildSkyscrapper(String location);
}
These two interfaces are unrelated. Yes, they share the theme, but they're unrelated as far as code is concerned.
At this point, a concrete class implementing the AdvancedBuilder
interface is created:
public class HouseBuilder implements AdvancedBuilder {
@Override
public void buildHouse(String location) {
System.out.println("Building a house located in the " + location + "area!");
}
@Override
public void buildSkyscrapper(String location) {
//don't implement
}
}
And of course, by the same analogy, another concrete class is created:
public class SkyscrapperBuilder implements AdvancedBuilder {
@Override
public void buildSkyscrapper(String location) {
System.out.println("Building a skyscrapper in the " + location + "area!");
}
@Override
public void buildHouse(String location) {
//don't implement
}
}
Here comes the adapter part - to connect these two interfaces, a BuilderAdapter
implementing Builder
is made:
public class BuilderAdapter implements Builder {
AdvancedBuilder advancedBuilder;
public BuilderAdapter(String type) {
if(type.equalsIgnoreCase("House")) {
advancedBuilder = new HouseBuilder();
} else if(type.equalsIgnoreCase("Skyscrapper")) {
advancedBuilder = new SkyscrapperBuilder();
}
}
@Override
public void build(String type, String location) {
if(type.equalsIgnoreCase("House")) {
advancedBuilder.buildHouse(location);
} else if(type.equalsIgnoreCase("Skyscrapper")) {
advancedBuilder.buildSkyscrapper(location);
}
}
}
With the adapter working, we can finally implement the solution and use the Builder
interface's method with the BuilderAdapter
to build the supported building types.
public class BuilderImplementation implements Builder {
BuilderAdapter builderAdapter;
@Override
public void build(String type, String location) {
if(type.equalsIgnoreCase("House") || type.equalsIgnoreCase("Skyscrapper")) {
builderAdapter = new BuilderAdapter(type);
builderAdapter.build(type, location);
} else {
System.out.println("Invalid building type.");
}
}
}
And to observe the result:
public class Main {
public static void main(String[] args) {
BuilderImplementation builderImpl = new BuilderImplementation();
builderImpl.build("house", "Downtown");
builderImpl.build("Skyscrapper", "City Center");
builderImpl.build("Skyscrapper", "Outskirts");
builderImpl.build("Hotel", "City Center");
}
}
Running the piece of code above will yield:
Building a house located in the Downtown area!
Building a skyscrapper in the City Center area!
Building a skyscrapper in the Outskirts area!
Invalid building type.
Bridge
The Bridge pattern is used to segregate abstract classes from their implementations and act as a bridge between them. This way, both the abstract class and the implementation can change structurally without affecting the other.
If this is by any means confusing, refer to the implementation to see its use.
Implementation
As usual, an interface is the starting point:
public interface FeedingAPI {
public void feed(int timesADay, int amount, String typeOfFood);
}
After which, two concrete classes implement it:
public class BigDog implements FeedingAPI {
@Override
public void feed(int timesADay, int amount, String typeOfFood) {
System.out.println("Feeding a big dog, " + timesADay + " times a day with " +
amount + " g of " + typeOfFood);
}
}
public class SmallDog implements FeedingAPI {
@Override
public void feed(int timesADay, int amount, String typeOfFood) {
System.out.println("Feeding a small dog, " + timesADay + " times a day with " +
amount + " g of " + typeOfFood);
}
}
Using the FeedingAPI
interface, an abstract Animal
class is created:
public abstract class Animal {
protected FeedingAPI feedingAPI;
protected Animal(FeedingAPI feedingAPI) {
this.feedingAPI = feedingAPI;
}
public abstract void feed();
}
This is where the Bridge pattern kicks in. A bridge class is created that segregates the abstract Animal
class from its implementation:
public class Dog extends Animal{
private int timesADay, amount;
private String typeOfFood;
public Dog(int timesADay, int amount, String typeOfFood, FeedingAPI feedingAPI) {
super(feedingAPI);
this.timesADay = timesADay;
this.amount = amount;
this.typeOfFood = typeOfFood;
}
public void feed() {
feedingAPI.feed(timesADay, amount, typeOfFood);
}
}
And to observe the result:
public class Main {
public static void main(String[] args) {
Animal bigDog = new Dog(3, 500, "Meat", new BigDog());
Animal smallDog = new Dog(2, 250, "Granules", new SmallDog());
bigDog.feed();
smallDog.feed();
}
}
Running this piece of code will yield:
Feeding a big dog, 3 times a day with 500 g of Meat
Feeding a small dog, 2 times a day with 250 g of Granules
Filter
The Filter pattern is used when we need a way to filter through sets of objects with different custom criteria. We can chain criteria for an even narrower filter, which is done in a decoupled way.
Implementation
Starting off with an Employee
class which we will filter using different Criteria
:
public class Employee {
private String name;
private String gender;
private String position;
public Employee(String name, String gender, String position) {
this.name = name;
this.gender = gender;
this.position = position;
}
//getters
}
The Criteria
interface is fairly simple, and all other specific criteria will implement its method in their own way:
public interface Criteria {
public List<Employee> criteria(List<Employee> employeeList);
}
With the foundation of the filtering system in place, let's define a few different criteria:
CriteriaMale
- A criteria to look for male employeesCriteriaFemale
- A criteria to look for female employeesCriteriaSenior
- A criteria to look for senior employeesCriteriaJunior
- A criteria to look for junior employeesAndCriteria
- A criteria to look for employees who pass both criteria we applyOrCriteria
- A criteria to look for employees who pass either of the criteria we apply
CriteriaMale:
public class CriteriaMale implements Criteria {
@Override
public List<Employee> criteria(List<Employee> employeeList) {
List<Employee> maleEmployees = new ArrayList<>();
for(Employee employee : employeeList) {
if(employee.getGender().equalsIgnoreCase("Male")) {
maleEmployees.add(employee);
}
}
return maleEmployees;
}
}
Simple for
loop that adds all male employees to a list, and returns it.
CriteriaFemale:
public class CriteriaFemale implements Criteria {
@Override
public List<Employee> criteria(List<Employee> employeeList) {
List<Employee> femaleEmployees = new ArrayList<>();
for(Employee employee : employeeList) {
if(employee.getGender().equalsIgnoreCase("Female")) {
femaleEmployees.add(employee);
}
}
return femaleEmployees;
}
}
Same as above, but for female employees.
CriteriaSenior:
public class CriteriaSenior implements Criteria{
@Override
public List<Employee> criteria(List<Employee> employeeList) {
List<Employee> seniorEmployees = new ArrayList<>();
for(Employee employee : employeeList) {
if(employee.getPosition().equalsIgnoreCase("Senior")) {
seniorEmployees.add(employee);
}
}
return seniorEmployees;
}
}
Same as above, but checks the position of the employee, not the gender.
CriteriaJunior:
public class CriteriaJunior implements Criteria {
@Override
public List<Employee> criteria(List<Employee> employeeList) {
List<Employee> juniorEmployees = new ArrayList<>();
for(Employee employee : employeeList) {
if(employee.getPosition().equalsIgnoreCase("Junior")) {
juniorEmployees.add(employee);
}
}
return juniorEmployees;
}
}
Same as above, but for Junior employees.
AndCriteria:
public class AndCriteria implements Criteria {
private Criteria firstCriteria;
private Criteria secondCriteria;
public AndCriteria(Criteria firstCriteria, Criteria secondCriteria) {
this.firstCriteria = firstCriteria;
this.secondCriteria = secondCriteria;
}
@Override
public List<Employee> criteria(List<Employee> employeeList) {
List<Employee> firstCriteriaEmployees = firstCriteria.criteria(employeeList);
return secondCriteria.criteria(firstCriteriaEmployees);
}
}
The list of employees is filtered through by the first criteria, and then the already filtered list is filtered again, with the second criteria.
OrCriteria:
private Criteria firstCriteria;
private Criteria secondCriteria;
public OrCriteria(Criteria firstCriteria, Criteria secondCriteria) {
this.firstCriteria = firstCriteria;
this.secondCriteria = secondCriteria;
}
@Override
public List<Employee> criteria(List<Employee> employeeList) {
List<Employee> firstCriteriaEmployees = firstCriteria.criteria(employeeList);
List<Employee> secondCriteriaEmployees = secondCriteria.criteria(employeeList);
for (Employee employee : secondCriteriaEmployees) {
if(!firstCriteriaEmployees.contains(employee)) {
firstCriteriaEmployees.add(employee);
}
}
return firstCriteriaEmployees;
}
}
Two lists of employees are made, based on the individual criteria. If the first list doesn't contain an employee that the second list does, the employee is added to the list.
This way, both lists are practically merged in the end.
Now that all of the Criteria
implementations are in place, let's make a list of employees that will act as a list retrieved from a database, and then run a few criteria:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
public class Main {
public static void main(String[] args) {
List<Employee> employeeList = new ArrayList<>();
//adding employees to the list
employeeList.add(new Employee("David", "Male", "Senior"));
employeeList.add(new Employee("Scott", "Male", "Senior"));
employeeList.add(new Employee("Rhett", "Male", "Junior"));
employeeList.add(new Employee("Andrew", "Male", "Junior"));
employeeList.add(new Employee("Susan", "Female", "Senior"));
employeeList.add(new Employee("Rebecca", "Female", "Junior"));
employeeList.add(new Employee("Mary", "Female", "Junior"));
employeeList.add(new Employee("Juliette", "Female", "Senior"));
employeeList.add(new Employee("Jessica", "Female", "Junior"));
employeeList.add(new Employee("Mike", "Male", "Junior"));
employeeList.add(new Employee("Chris", "Male", "Junior"));
//initialization of the different criteria classes
Criteria maleEmployees = new CriteriaMale();
Criteria femaleEmployees = new CriteriaFemale();
Criteria seniorEmployees = new CriteriaSenior();
Criteria juniorEmployees = new CriteriaJunior();
//AndCriteria and OrCriteria accept two Criteria as their constructor
arguments and return filtered lists
Criteria seniorFemale = new AndCriteria(seniorEmployees, femaleEmployees);
Criteria juniorOrMale = new OrCriteria(juniorEmployees, maleEmployees);
System.out.println("Male employees: ");
printEmployeeInfo(maleEmployees.criteria(employeeList));
System.out.println("\nFemale employees: ");
printEmployeeInfo(femaleEmployees.criteria(employeeList));
System.out.println("\nSenior female employees: ");
printEmployeeInfo(seniorFemale.criteria(employeeList));
System.out.println("\nJunior or male employees: ");
printEmployeeInfo(juniorOrMale.criteria(employeeList));
}
//simple method to print out employee info
public static void printEmployeeInfo(List<Employee> employeeList) {
for (Employee employee : employeeList) {
System.out.println("Employee info: | Name: "
+ employee.getName() + ", Gender: "
+ employee.getGender() + ", Position: "
+ employee.getPosition() + " |");
}
}
}
Running this piece of code will yield:
Male employees:
Employee info: | Name: David, Gender: Male, Position: Senior |
Employee info: | Name: Scott, Gender: Male, Position: Senior |
Employee info: | Name: Rhett, Gender: Male, Position: Junior |
Employee info: | Name: Andrew, Gender: Male, Position: Junior |
Employee info: | Name: Mike, Gender: Male, Position: Junior |
Employee info: | Name: Chris, Gender: Male, Position: Junior |
Female employees:
Employee info: | Name: Susan, Gender: Female, Position: Senior |
Employee info: | Name: Rebecca, Gender: Female, Position: Junior |
Employee info: | Name: Mary, Gender: Female, Position: Junior |
Employee info: | Name: Juliette, Gender: Female, Position: Senior |
Employee info: | Name: Jessica, Gender: Female, Position: Junior |
Senior female employees:
Employee info: | Name: Susan, Gender: Female, Position: Senior |
Employee info: | Name: Juliette, Gender: Female, Position: Senior |
Junior or male employees:
Employee info: | Name: Rhett, Gender: Male, Position: Junior |
Employee info: | Name: Andrew, Gender: Male, Position: Junior |
Employee info: | Name: Rebecca, Gender: Female, Position: Junior |
Employee info: | Name: Mary, Gender: Female, Position: Junior |
Employee info: | Name: Jessica, Gender: Female, Position: Junior |
Employee info: | Name: Mike, Gender: Male, Position: Junior |
Employee info: | Name: Chris, Gender: Male, Position: Junior |
Employee info: | Name: David, Gender: Male, Position: Senior |
Employee info: | Name: Scott, Gender: Male, Position: Senior |
Composite
The Composite pattern is used when we need a way to treat a whole group of objects in a similar, or the same manner.
This is usually done by the class that "owns" the group of objects and provides a set of methods to treat them equally as if they were a single object.
Implementation
Let's start off with the Employee
class. This class will be instantiated multiple times to form a group of employees:
public class Employee {
private String name;
private String position;
private int wage;
private List<Employee> coworkers;
public Employee(String name, String position, int wage) {
this.name = name;
this.position = position;
this.wage = wage;
coworkers = new ArrayList<Employee>();
}
public void addCoworker(Employee employee) {
coworkers.add(employee);
}
public void removeCoworker(Employee employee) {
coworkers.remove(employee);
}
public List<Employee> getCoworkers() {
return coworkers;
}
public String toString() {
return "Employee : | Name: " + name + ", Position: " + position + ", Wage: "
+ wage + " |";
}
}
The class has a list of Employee
within it, this is our group of objects that we want to target as a single object.
public class StackAbuseJavaDesignPatterns {
public static void main(String[] args) {
Employee employee1 = new Employee("David", "Programmer", 1500);
Employee employee2 = new Employee("Scott", "CEO", 3000);
Employee employee3 = new Employee("Andrew", "Manager", 2000);
Employee employee4 = new Employee("Scott", "Janitor", 500);
Employee employee5 = new Employee("Juliette", "Marketing", 1000);
Employee employee6 = new Employee("Rebecca", "Sales", 2000);
Employee employee7 = new Employee("Chris", "Programmer", 1750);
Employee employee8 = new Employee("Ivan", "Programmer", 1200);
employee3.addCoworker(employee1);
employee3.addCoworker(employee7);
employee3.addCoworker(employee8);
employee1.addCoworker(employee7);
employee1.addCoworker(employee8);
employee2.addCoworker(employee3);
employee2.addCoworker(employee5);
employee2.addCoworker(employee6);
System.out.println(employee2);
for (Employee headEmployee : employee2.getCoworkers()) {
System.out.println(headEmployee);
for(Employee employee : headEmployee.getCoworkers()) {
System.out.println(employee);
}
}
}
}
Here, several employees are instantiated. The CEO has a few employees as close coworkers, and some of them have their own close coworkers, in lower positions.
In the end, the head employees are close coworkers of the CEO, and the regular employees are coworkers of the head employees.
Running the code above will yield:
Employee : | Name: Scott, Position: CEO, Wage: 3000 |
Employee : | Name: Andrew, Position: Manager, Wage: 2000 |
Employee : | Name: David, Position: Programmer, Wage: 1500 |
Employee : | Name: Chris, Position: Programmer, Wage: 1750 |
Employee : | Name: Ivan, Position: Programmer, Wage: 1200 |
Employee : | Name: Juliette, Position: Marketing, Wage: 1000 |
Employee : | Name: Rebecca, Position: Sales, Wage: 2000 |
Decorator
The Decorator pattern is used to alter an individual instance of a class at runtime, by creating a decorator class which wraps the original class.
This way, changing or adding functionalities of the decorator object won't affect the structure or the functionalities of the original object.
It differs from classic inheritance in the fact that it's done at runtime, and applies only to an individual instance, whereas inheritance will affect all instances, and is done at compile time.
Implementation
Following the description above, let's define an interface:
public interface Computer {
void assemble();
}
And by implementing that interface, we'll define a class which we will, using the Decorator pattern, make susceptible to change during runtime:
public class BasicComputer implements Computer {
@Override
public void assemble() {
System.out.print("Assembling a basic computer.");
}
}
Now, for the decorator class:
public abstract class ComputerDecorator implements Computer {
protected Computer computer;
public ComputerDecorator(Computer computer) {
this.computer = computer;
}
@Override
public void assemble() {
this.computer.assemble();
}
}
Our concrete classes will extend this one inheriting its functionality and adding their own functionality in the process:
public class GamingComputer extends ComputerDecorator {
public GamingComputer(Computer computer) {
super(computer);
}
@Override
public void assemble() {
super.assemble();
System.out.print(" Adding characteristics of a gaming computer! ");
}
}
public class WorkComputer extends ComputerDecorator {
public WorkComputer(Computer computer) {
super(computer);
}
@Override
public void assemble() {
super.assemble();
System.out.print(" Adding characteristics of a work computer! ");
}
}
With these concrete classes fully defined, we can observe the result:
public class Main {
public static void main(String[] args) {
Computer gamingComputer = new GamingComputer(new BasicComputer());
gamingComputer.assemble();
System.out.println("\n");
Computer workComputer = new WorkComputer(new GamingComputer(new
BasicComputer()));
workComputer.assemble();
}
}
Running this piece of code will yield:
Assembling a basic computer. Adding characteristics of a gaming computer!
Assembling a basic computer. Adding characteristics of a gaming computer! Adding characteristics of a work computer!
Facade
The Facade pattern provides a simple and top-level interface for the client and allows it to access the system, without knowing any of the system logic and inner-workings.
Implementation
We'll define a ZooKeeper
class that will act like an interface for the user which wants to feed the animals in the Zoo.
We're starting off with an Animal
interface:
public interface Animal {
void feed();
}
And concrete classes that implement it:
public class Lion implements Animal {
@Override
public void feed() {
System.out.println("The lion is being fed!");
}
}
public class Wolf implements Animal {
@Override
public void feed() {
System.out.println("The wolf is being fed!");
}
}
public class Bear implements Animal {
@Override
public void feed() {
System.out.println("The bear if being fed!");
}
}
This is the cue for the ZooKeeper
class:
public class ZooKeeper {
private Animal lion;
private Animal wolf;
private Animal bear;
public ZooKeeper() {
lion = new Lion();
wolf = new Wolf();
bear = new Bear();
}
public void feedLion() {
lion.feed();
}
public void feedWolf() {
wolf.feed();
}
public void feedBear() {
bear.feed();
}
}
By using this interface, the client doesn't concern themselves with the logic behind feeding the animals.
To observe the result:
public class Main {
public static void main(String[] args) {
ZooKeeper zookeeper = new ZooKeeper();
zookeeper.feedLion();
zookeeper.feedWolf();
zookeeper.feedBear();
}
}
Running this piece of code will yield:
The lion is being fed!
The wolf is being fed!
The bear if being fed!
Flyweight
The Flyweight pattern is concerned with reducing the strain on the JVM and its memory. This is crucial for devices without much memory, as well as optimization of the application.
When a certain application needs to create many instances of the same class, a common pool is created so that similar ones can be reused, instead of created each time.
The most well-known implementation of this design pattern is the String Pool in Java. Strings are used perhaps more often than any other object in the language and thus, they consumed a large portion of the resources. By creating a pool of common Strings and assigning multiple reference variables to the ones with the same content, and only creating new Strings when no match is found made a huge impact on the performance of Java.
Implementation
As usual, let's start with an interface:
public interface Attendee {
public void listenToConcert();
}
A concrete class implements this interface:
public class AttendeeImpl implements Attendee {
private String name;
private int age;
public AttendeeImpl(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public void listenToConcert() {
System.out.println(name + " is listening to concert " + age + " years old!");
}
}
These attendees will all be created by an AttendeeFactory
and put into a HashMap
. It's important to note that the method creates a new AttendeeImpl
object if none already exist. On the other hand, if it does exist, the method returns it.
This is the point of the Flyweight pattern. To return a new object only if a matching object doesn't already exist:
public class AttendeeFactory {
private static final HashMap attendees = new HashMap();
public static Attendee getAttendee(String name) {
AttendeeImpl attendeeImpl = (AttendeeImpl)attendees.get(name);
if(attendeeImpl == null) {
attendeeImpl = new AttendeeImpl(name);
attendees.put(name, attendeeImpl);
System.out.println("Creating a new attendee: " + name);
}
return attendeeImpl;
}
}
And to see the result, we'll create 10 attendees with random names from the name pool and random age.
public class StackAbuseJavaDesignPatterns {
private static final String[] names = {"David", "Scott", "Andrew", "Rhett"};
public static void main(String[] args) {
for(int i = 0; i < 10; ++i) {
AttendeeImpl attendeeImpl = (AttendeeImpl) AttendeeFactory.getAttendee(getRandomName());
attendeeImpl.setAge(getRandomAge());
attendeeImpl.listenToConcert();
}
}
private static String getRandomName() {
int randomName = new Random().nextInt(names.length);
return names[randomName];
}
private static int getRandomAge() {
return (int)(Math.random()*80);
}
}
Running this piece of code will yield different values each time, but should look something like this:
Creating a new attendee: Scott
Scott is listening to concert 32 years old!
Scott is listening to concert 1 years old!
Creating a new attendee: Andrew
Andrew is listening to concert 8 years old!
Creating a new attendee: Rhett
Rhett is listening to concert 58 years old!
Andrew is listening to concert 76 years old!
Scott is listening to concert 56 years old!
Rhett is listening to concert 43 years old!
Scott is listening to concert 51 years old!
Creating a new attendee: David
David is listening to concert 31 years old!
David is listening to concert 29 years old!
Proxy
The Proxy pattern is used when we want to limit the capabilities and the functionalities of a class, by using another class which limits it.
By using this proxy class, the client uses the interface it defines, to access the original class. This ensures that the client can't do anything out of order with the original class since all of his requests pass through our proxy class.
Implementation
Let's define a common interface for the original and proxy class:
public interface MediaFile {
void printName();
}
This interface will be implemented by a class, for which we will define a proxy class:
public class MediaFileImpl implements MediaFile {
private String fileName;
public MediaFileImpl(String fileName){
this.fileName = fileName;
loadFromDisk(fileName);
}
@Override
public void printName() {
System.out.println("Displaying " + fileName);
}
private void loadFromDisk(String fileName){
System.out.println("Loading " + fileName);
}
}
public class ProxyMediaFile implements MediaFile {
private MediaFileImpl mediaFileImpl;
private String fileName;
public ProxyMediaFile(String fileName){
this.fileName = fileName;
}
@Override
public void printName() {
if(mediaFileImpl == null){
mediaFileImpl = new MediaFileImpl(fileName);
}
mediaFileImpl.printName();
}
}
With these two concrete classes finished, let's observe the result:
public class Main {
public static void main(String[] args) {
MediaFile mediaFile = new ProxyMediaFile("movie.mp4");
mediaFile.printName();
mediaFile.printName();
}
}
Running this piece of code will yield:
Loading movie.mp4
Displaying movie.mp4
Displaying movie.mp4
Conclusion
With this, all Structural Design Patterns in Java are fully covered, with working examples.
If you'd like to continue reading about Design Patterns in Java, the following article covers Behavioral Design Patterns.