Design patterns are reusable template solutions designed to solve common problems in software development. Such as Iterative code, reusable patterns, redundant features, etc. These are like customizable blueprints to solve the problem. This concept was first explained by Christopher Alexander and later widely known as the gang of four by four authors (Erich Gamma, John Brissidis, Ralph Johnson, Richard Helm) and later in their book “Design Patterns: Published in “Elements of Reusable Object-Oriented Software”. Design patterns teach developers how to solve common problems without spending a lot of time and effort developing solutions. This greatly increases the need for software developers to know these patterns.
Take a look at the most common design pattern interview questions and answers. These questions are useful for coding interviews and competitive exams.
Before we begin the important Interview questions for the Design Pattern, let us know about the Java training in Electronic City Bangalore provided by eMexo Technologies one of the best Java training institute in Electronic City Bangalore. With the best faculty of industry experts, the latest classroom techniques, lab facilities, and course curriculum to the needs of the industry, we prove to be the best Java training institution in Bangalore. We believe in the perfect balance between theory and practice. Therefore, many real-life live projects are guided during the course to give students real insights. Don’t wait just make your enrollment now and schedule your java course demo class today.
1. What are the design principles in java [OO Design Principles]?
DRY (Don’t repeat yourself) – avoids duplication in code
- Do not write duplicate code. Use abstraction to abstract common things in one place.
- If you have blocks of code in more than one place, consider making their different methods. If you use hard-coded values multiple times, make them public final constants.
- The advantage of this object-oriented design principle is maintenance.
- It is important not to abuse it. Duplication is not for code, but for functionality.
- This does not mean that if you use common code to validate the OrderID and SSN, they are the same or will remain the same in the future.
- By sharing the code of two different features or things, they are permanently closely coupled and the SSN verification code breaks when the order ID format is changed.
- Therefore, be aware of such joins and do not join those that use similar code but are not related.
Encapsulate What Changes– hides implementation details, helps in the maintenance
- There is only one constant in software, it is a “change”. Therefore, it encapsulates code that is expected or suspected to change in the future.
- The advantage of this OOPS design principle is that it is easy to test and maintain proper encapsulated code.
- When programming in Java, follow the principle of making variables and methods private by default and gradually increasing access. From private to non-public and protected.
- Some Java design patterns use encapsulation. The factory design pattern is an example of encapsulation that encapsulates object creation code and provides the flexibility to introduce new products later without affecting existing code.
Open Closed Design Principle – open for extension, closed for modification
- A class, method, or function must be open to extensions (new features) and closed to changes.
- This is another good SOLID design principle that prevents anyone from modifying the tested code.
- Ideally, if you just want to add new features, you should test your code. This is the goal of an open-closed design.
- By the way, the principle of open and close is the acronym SOLID “O”.
Single Responsibility Principle (SRP) – one class should do one thing and do it well
- The single-responsibility principle is another SOLID design principle, which stands for “S” in the SOLID acronym for multiple features of a Java class.
- This will combine the two functions and break the combined function even if you change the function.
- This requires a separate test round to avoid unexpected situations in a production environment.
Dependency Injection or Inversion principle – don’t ask, let the framework give to you
- Don’t ask for dependencies. The Framework provides the dependencies.
- This is very well implemented in the Spring framework. The advantage of this design principle is that the classes injected by the DI framework are easier to test using mock objects and the object creation code is centralized in the framework rather than cluttering the client code. Easy to maintain.
- There are several ways to implement dependency injection. Therefore, Use the bytecode instrumentation used by some Aspect-Oriented Programming (AOP) frameworks such as AspectJ, or use a proxy like the one used in Spring.
- Represents the “D” in the acronym SOLID.
Favor Composition over Inheritance – Code reuse without the cost of inflexibility
- Whenever possible, prioritize composition over heredity.
- Some of you may claim it, but I’ve found the composition to be much more flexible than inheritance.
- In a configuration, you can change the behavior of the class by setting properties at run time. It also uses polymorphism by building classes using interfaces. This gives you the flexibility to replace your implementation with a more appropriate one at any time.
Liskov Substitution Principle (LSP) – Subtype must be substitutable for supertype
- According to Liskov Substitution Principle, subtypes must be able to be used in place of supertypes. Therefore, A method or function that uses a superclass type must be able to work fine with subclassed objects.
- If a class has more functionality than a subclass, it will not support some functionality and violate LSP.
- There is a possibility. Derived classes or subclasses need to improve their functionality, but they cannot reduce it. The acronym SOLID L “.
Interface Segregation principle (ISP)– avoid monolithic interface, reduce pain on client-side
- The segregation principle indicates that if the client is not using the interface, it should not be implemented.
- This mainly happens when the interface contains multiple features and the client needs only one feature and not the other.
- Interface design is a delicate task. Once you release an interface, you cannot change it without breaking the entire implementation. Another advantage of this design principle in
- Java is that it implements fewer methods in a single function due to the drawback of implementing all the methods before the class uses them.
Programming for Interface not implementation – helps in the maintenance, improves flexibility
- Always program for the interface, not for the implementation. This gives you flexible code that works with the new implementation of the interface.
- Therefore, in Java, use the interface type of a variable, the return type of a method, or the argument type of a method.
Delegation principle –don’t do all things by yourself, delegate it
- Instead of doing everything alone, delegate it to each class.
- Typical examples of delegation design principles are the Java equals () and hashCode () methods.
- To compare whether two objects are equal, ask the class itself to do the comparison instead of having the client class do the checking.
- The advantage of this design principle is that the code is not duplicated and the behavior can be changed fairly easily.
2. Builder Design Pattern in Java?
- Builder patterns are creative design patterns. That is, it solves the problem associated with creating objects.
- The Java constructor is used to create an object and can receive the parameters needed to create the object.
- This problem starts when the object can be created with many parameters. Some parameters are required and some are optional.
- Consider the class used to make a cake. Now you need many items like eggs, milk, and flour to make a cake. Many of them are mandatory and some such as cherries and fruits are optional.
- If there are overloaded constructors for different types of cakes, there are many constructors, and in the worst case, they accept many parameters.
- Problems:
- Too many constructors to maintain.
- Many fields are of the same type, which makes them error-prone. For example, sugar and butter are in a cup. So if you give 2 cups of butter instead of 2 cups of sugar, the compiler won’t complain, but you end up with a butter cake that has very little sugar and the cost of wasting butter.
- You can partially solve this problem by creating the Cake and then adding the material, but this causes another problem the object remains in an inconsistent state during construction. Ideally, Cake should be unavailable until it is created.
- Both of these issues can be resolved using the Java Builder design pattern.
- Builder design patterns not only improve readability but also reduce the chance of error by explicitly adding materials and making fully constructed objects available.
For Example
- Within the class created by the object Builder, create a static nested class called Builder.
- In this example, it is a cake. The builder class has exactly the same set of fields as the original class. The Builder class shows the methods for adding materials. In this example sugar ().
- Each method returns the same builder object. The Builder has been enhanced with each method call.
- The Builder.build () method copies all builder field values into the actual class and returns an Item class object.
- The Item class (the class that creates the builder) needs a private constructor to create an object from the build () method and prevent outsiders from accessing its constructor.
Example Program
public class BuilderPatternExample { public static void main(String args[]) { //Creating object using Builder pattern in java Cake whiteCake = new Cake.Builder().sugar(1).butter(0.5). eggs(2).vanila(2).flour(1.5). bakingpowder(0.75).milk(0.5).build(); //Cake is ready to eat 🙂 System.out.println(whiteCake); } } class Cake { private final double sugar; //cup private final double butter; //cup private final int eggs; private final int vanila; //spoon private final double flour; //cup private final double bakingpowder; //spoon private final double milk; //cup private final int cherry; public static class Builder { private double sugar; //cup private double butter; //cup private int eggs; private int vanila; //spoon private double flour; //cup private double bakingpowder; //spoon private double milk; //cup private int cherry; //builder methods for setting property public Builder sugar(double cup){this.sugar = cup; return this; } public Builder butter(double cup){this.butter = cup; return this; } public Builder eggs(int number){this.eggs = number; return this; } public Builder vanila(int spoon){this.vanila = spoon; return this; } public Builder flour(double cup){this.flour = cup; return this; } public Builder bakingpowder(double spoon){this.sugar = spoon; return this; } public Builder milk(double cup){this.milk = cup; return this; } public Builder cherry(int number){this.cherry = number; return this; } //return fully build object public Cake build() { return new Cake(this); } } //private constructor to enforce object creation through builder private Cake(Builder builder) { this.sugar = builder.sugar; this.butter = builder.butter; this.eggs = builder.eggs; this.vanila = builder.vanila; this.flour = builder.flour; this.bakingpowder = builder.bakingpowder; this.milk = builder.milk; this.cherry = builder.cherry; } @Override public String toString() { return “Cake{” + “sugar=” + sugar + “, butter=” + butter + “, eggs=” + eggs + “, vanila=” + vanila + “, flour=” + flour + “, bakingpowder=” + bakingpowder + “, milk=” + milk + “, cherry=” + cherry + ‘}’; } } |
Output
Cake{sugar=0.75, butter=0.5, eggs=2, vanila=2, flour=1.5, bakingpowder=0.0, milk=0.5, cherry=0} |
Advantages:
- Maintenance is easier when the number of fields required to create an object exceeds 4 or 5.
- Explicit method calls make it less error-prone because the user knows what they are passing.
- It is more robust because only fully constructed objects are available to the customer.
Disadvantages:
- Redundancy and code duplication because the builder needs to copy all the fields from the original class or item class.
When to use Builder Design pattern in Java
- The Builder design pattern is a creative pattern and should be used when the number of parameters required by the constructor is more than manageable, usually 4 or up to 5. Do not confuse the
- Builder pattern with the Factory pattern. There are obvious differences in Builders. The factory can be used to create different implementations of the same interface, but the builder is bound to that container class and returns only objects of the outer class.
3. Factory Design Pattern in Java?
- Java factory design pattern is one of the core design patterns often used not only in the JDK but also in various open-source frameworks such as Spring, Struts, and Apache, as well as the Decorator theme pattern in Java.
- The factory design pattern is based on the concept of object-oriented encapsulation. The
- The factory method is used to create another object (often called an item) from the factory and encapsulates the creation code.
- Therefore, instead of using the object creation code on the client-side, encapsulate it in a Java factory method.
- The Factory design pattern is used to create objects or classes in Java and provides loose binding and high cohesion.
- The factory pattern encapsulates the object creation logic so that if you change the way the object is created, you can easily change it later. You can also introduce a new object with a single change to the class.
- The GOF pattern list lists factory patterns as build patterns.
- The factory must be an interface, and the client should either create the factory first or use the factory later to create the object.
Example of static factory method in JDK
- A valueOf () method that returns a factory-created object that corresponds to the value of the passed parameter.
- The getInstance () method creates an instance of a singleton class.
- The newInstance () method is used to create and return a new instance from the factory method on each call.
- getType () and newType () are equivalent to the factory methods getInstance () and newInstance (), but are used when the factory method is in a different class.
Problems solved by Java’s factory method Pattern
- Sometimes our application or framework will not know what kind of object it has to create at run-time it knows only the interface or abstract class and as we know we cannot create an object of interface or abstract class so the main problem is framework knows when it has to create but don`t know what kind of object.
- Whenever we create an object using new() we violate the principle of programming for interface rather than implementation which eventually results in inflexible code and is difficult to change in maintenance. By using the Factory design pattern in Java we get rid of this problem.
- Another problem we can face is class needs to contain objects of other classes or class hierarchies within it; this can be very easily achieved by just using the new keyword and the class constructor.
- The problem with this approach is that it is a very hard-coded approach to creating objects as this creates a dependency between the two classes. Therefore, the factory pattern solves this problem very easily by modeling the interface for creating an object that allows subclasses to decide which class to instantiate at creation time.
- Factory patterns facilitate loose coupling by eliminating the need to include application-specific classes in your code. This pattern is also known as a “virtual constructor” because factory methods are usually implemented as virtual methods. These methods create objects for the product or target class.
For Example
- Let’s look at an example of how the factory pattern is implemented in your code. You need to create multiple currencies.
- The INR, SGD, USD, and code must be extensible to include new currencies. Here, we created the currency as an interface. All currencies are concrete implementations of currency interfaces.
- The factory class creates a currency based on the country and returns the concrete implementation stored in the interface type. This makes the code dynamic and extensible.
Example Program
interface Currency { String getSymbol(); } // Concrete Rupee Class code class Rupee implements Currency { @Override public String getSymbol() { return “Rs”; } } // Concrete SGD class Code class SGDDollar implements Currency { @Override public String getSymbol() { return “SGD”; } } // Concrete US Dollar code class USDollar implements Currency { @Override public String getSymbol() { return “USD”; } } // Factroy Class code class CurrencyFactory { public static Currency createCurrency (String country) { if (country. equalsIgnoreCase (“India”)){ return new Rupee(); }else if(country. equalsIgnoreCase (“Singapore”)){ return new SGDDollar(); }else if(country. equalsIgnoreCase (“US”)){ return new USDollar(); } throw new IllegalArgumentException(“No such currency”); } } // Factory client code public class Factory { public static void main(String args[]) { String country = args[0]; Currency rupee = CurrencyFactory.createCurrency(country); System.out.println(rupee.getSymbol()); } } |
Advantages
- Does the factory method design pattern separate the calling class from the target class, resulting in less coupled and cohesive code? Java factory patterns allow subclasses to provide extended versions of objects.
- This is because creating objects in the factory is more flexible than creating objects directly in the client. The client works at the interface level, so you can always improve your implementation and bring it back from the factory.
- Another advantage of using the factory design pattern in Java is that it promotes code consistency because each time an object is created using a factory rather than on a different client-side, a different constructor is used.
- Code written in Java using the factory design pattern has a centralized way to create objects, and each client retrieves the object from the same location, making it easy to debug and troubleshoot.
- The static factory methods used in the factory design pattern force you to use the interface as an implementation. This is a good method in itself. for example:
- Map synchronizedMap = Collections.synchronizedMap (new HashMap ());
- Static factory methods have an interface return type that allows you to replace the implementation of a new version with a higher-performance version.
- Another advantage of the static factory method pattern is that you can cache frequently used objects and eliminate the creation of duplicate objects.
- The Boolean.valueOf () method is a good example of caching true and false Boolean values.
- The factory method pattern is also recommended by Joshua Bloch of Effective Java.
- The factory method pattern provides an alternative way to create an object. You can also use the factory pattern to hide information related to creating objects.
4. Abstract Factory Design Pattern in Java?
- The abstract factory pattern is another creative design pattern and is considered another layer of the factory pattern abstraction.
- Imagine our car maker deciding to globalize. To globalize, the system needs to be improved to support different car-making styles from country to country.
- For example, take a look at the United States, Asia, and standards in all other countries. First, you need a car factory in each location specified in the problem statement. That is, USACarFactory, AsiaCarFactory, and DefaultCarFactory.
- The application should now be smart enough to identify where it is being used. Therefore, you should be able to use the appropriate car factory without knowing which car factory implementation is used internally.
- This also saves us from someone calling the wrong factory in a particular place. So basically what you need is another layer of abstraction that internally uses the correct implementation of the car factory to identify the location and give the user a single hint.
- This is exactly the problem that the abstract factory pattern is used to solve. Let’s see how an abstract factory solves the above problem from a design point of view.
Package Diagram:
Sequence Diagram:
Implementation in code:
Let’s write all separate car factories for different locations. Begin with Car.java class
Car.java public abstract class Car { public Car(CarType model, Location location){ this.model = model; this.location = location; } protected abstract void construct(); private CarType model = null; private Location location = null; public CarType getModel() { return model; } public void setModel(CarType model) { this.model = model; } public Location getLocation() { return location; } public void setLocation(Location location) { this.location = location; } @Override public String toString() { return ”Model- “ + model + “ built in “ +location; } } |
This adds extra work of creating another enum for storing different locations.
Location.java public enum Location { DEFAULT, USA, ASIA } |
All vehicle types also have additional location properties. I’m writing only for luxury cars. The same applies to small cars and sedans.
LuxuryCar.java public class LuxuryCar extends Car { public LuxuryCar(Location location) { super(CarType.LUXURY, location); construct(); } @Override protected void construct() { System.out.println(“luxury car”); //add accessories } } |
So we have created basic classes. Now let’s have different car factories.
AsiaCarFactory.java public class AsiaCarFactory { public static Car buildCar(CarType model) { Car car = null; switch (model) { case SMALL: car = new SmallCar(Location.ASIA); break; case SEDAN: car = new SedanCar(Location.ASIA); break; case LUXURY: car = new LuxuryCar(Location.ASIA); break; default: //throw some exception break; } return car; } } DefaultCarFactory.java public class DefaultCarFactory { public static Car buildCar(CarType model) { Car car = null; switch (model) { case SMALL: car = new SmallCar(Location.DEFAULT); break; case SEDAN: car = new SedanCar(Location.DEFAULT); break; case LUXURY: car = new LuxuryCar(Location.DEFAULT); break; default: //throw some exception break; } return car; } } USACarFactory.java public class USACarFactory { public static Car buildCar(CarType model) { Car car = null; switch (model) { case SMALL: car = new SmallCar(Location.USA); break; case SEDAN: car = new SedanCar(Location.USA); break; case LUXURY: car = new LuxuryCar(Location.USA); break; default: //throw some exception break; } return car; } } |
Now, there are three different car factories. Next, we need to abstract how to access these factories. Let’s see how?
public class CarFactory { private CarFactory() { //Prevent instantiation } public static Car buildCar(CarType type) { Car car = null; Location location = Location.ASIA; //Read location property somewhere from configuration //Use location specific car factory switch(location) { case USA: car = USACarFactory.buildCar(type); break; case ASIA: car = AsiaCarFactory.buildCar(type); break; default: car = DefaultCarFactory.buildCar(type); } return car; } } |
We are done with writing code. Now, let’s test what we have written till now.
public class TestFactoryPattern { public static void main(String[] args) { System.out.println(CarFactory.buildCar(CarType.SMALL)); System.out.println(CarFactory.buildCar(CarType.SEDAN)); System.out.println(CarFactory.buildCar(CarType.LUXURY)); } } |
Output
(Default location is Asia) Building small car Model- SMALL built in ASIA Building sedan car Model- SEDAN built in ASIA Building luxury car Model- LUXURY built in ASIA |
5. Prototype Design Pattern in Java?
- A prototype is a template for any object before the actual object is built. It has the same meaning in Java. The prototype design pattern is used in scenarios where an application needs to create a set of instances of nearly the same state or slightly different classes.
- This design pattern creates an instance of the actual object (that is, a prototype) at startup, and then whenever a new instance is needed, that prototype is duplicated to create another instance. The main advantage of this pattern is the minimal instantiation process, which is much more costly than the cloning process.
Problem Statement
- Let’s understand this pattern with an example. We are building entertainment applications that frequently require instances of movie, album, and show classes.
- I don’t want to create an instance every time because it costs money. Therefore, create a prototype instance and clone the prototype each time you need a new instance.
Implementation of Prototype Design Pattern
The above class diagram explains the necessary classes and their relationship.
Only one interface, “PrototypeCapable” is a new addition to the solution. The reason to use this interface is the broken behavior of the Cloneable interface. This interface helps in achieving the following goals:
- Ability to clone prototypes without knowing their actual types
- Provides a type reference to be used in the registry
Their workflow will look like this:
PrototypeCapable.java
package com.howtodoinjava.prototypeDemo.contract; public interface PrototypeCapable extends Cloneable { public PrototypeCapable clone() throws CloneNotSupportedException; } |
Movie.java, Album.java and Show.java
package com.howtodoinjava.prototypeDemo.model; import com.howtodoinjava.prototypeDemo.contract.PrototypeCapable; public class Movie implements PrototypeCapable { private String name = null; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public Movie clone() throws CloneNotSupportedException { System.out.println(“Cloning Movie object..”); return (Movie) super.clone(); } @Override public String toString() { return “Movie”; } } public class Album implements PrototypeCapable { private String name = null; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public Album clone() throws CloneNotSupportedException { System.out.println(“Cloning Album object..”); return (Album) super.clone(); } @Override public String toString() { return “Album”; } } public class Show implements PrototypeCapable { private String name = null; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public Show clone() throws CloneNotSupportedException { System.out.println(“Cloning Show object..”); return (Show) super.clone(); } @Override public String toString() { return “Show”; } } |
PrototypeFactory.java
package com.howtodoinjava.prototypeDemo.factory; import com.howtodoinjava.prototypeDemo.contract.PrototypeCapable; import com.howtodoinjava.prototypeDemo.model.Album; import com.howtodoinjava.prototypeDemo.model.Movie; import com.howtodoinjava.prototypeDemo.model.Show; public class PrototypeFactory { public static class ModelType { public static final String MOVIE = “movie”; public static final String ALBUM = “album”; public static final String SHOW = “show”; } private static java.util.Map<String , PrototypeCapable> prototypes = new java.util.HashMap<String , PrototypeCapable>(); static { prototypes.put(ModelType.MOVIE, new Movie()); prototypes.put(ModelType.ALBUM, new Album()); prototypes.put(ModelType.SHOW, new Show()); } public static PrototypeCapable getInstance(final String s) throws CloneNotSupportedException { return ((PrototypeCapable) prototypes.get(s)).clone(); } } |
TestPrototypePattern
package com.howtodoinjava.prototypeDemo.client; import com.howtodoinjava.prototypeDemo.factory.PrototypeFactory; import com.howtodoinjava.prototypeDemo.factory.PrototypeFactory.ModelType; public class TestPrototypePattern { public static void main(String[] args) { try { String moviePrototype = PrototypeFactory.getInstance(ModelType.MOVIE).toString(); System.out.println(moviePrototype); String albumPrototype = PrototypeFactory.getInstance(ModelType.ALBUM).toString(); System.out.println(albumPrototype); String showPrototype = PrototypeFactory.getInstance(ModelType.SHOW).toString(); System.out.println(showPrototype); } catch (CloneNotSupportedException e) { e.printStackTrace(); } } } |
When you run the client code, the following is the output.
Cloning Movie object.. Movie Cloning Album object.. Album Cloning Show object.. Show |