Laboratory Training 6
Design Patterns (Additional Work)
1 Training Task
Modify the previously created program of the individual assignment of Laboratory training #4 by building a code using the design patterns. Be sure to implement the "Facade", "Singleton", "Lazy Initialization" and "Factory Method" patterns. Additionally, you can apply the patterns "Abstract Factory" and "Observer", as well as any other patterns on your own.
2 Instructions
2.1 Design Patterns Summary
Design pattern is a description of the interaction of objects and classes, adapted to solve a specific task in a specific context.
A design pattern names, abstracts, and identifies the key aspects of a common design structure that make it useful for creating a reusable object-oriented design. Design patterns occupy an intermediate position between classes and application frameworks. Design patterns are more abstract than classes, but as architectural elements they are smaller than application frameworks.
The Model-View-Controller design pattern (MVC, Model-View-Controller) was mentioned earlier. It was first used on the Smalltalk-80 system. MVC may even be seen not as a pattern, but as a fundamental design concept whereby the application data model (business logic), user interface (visual representation) and user interaction are separated from each other.
The model provides data and methods for working with data and does not contain information about the visual representation of this data.
The view is responsible for the visual presentation of information. For example, this is a graphical user interface (a window with graphical elements).
The controller provides communication between the user, model, and presentation.
This distribution allows the development of a model regardless of the visual representation, as well as creating several different representations for one model.
For the first time design patterns were systematically outlined in the book "Patterns of Reusable Object-Oriented Software", by the authors of E Gamma R. Helm R. Johnson J. Vlissides (http://www.uml.org.cn/c++/pdf/DesignPatterns.pdf). This book was published in English in 1995 by Addison Wesley Longman, Inc. The further development of the patterns is reflected in the book "Applying UML and Patterns" by Craig Larman.
Pattern identification involves determining such elements:
- name; naming patterns provides the ability to design at a higher level of abstraction;
- problem; a description of when the pattern should be used;
- solution; description of the elements of the design solution, the relationships between them, the functions of each element;
- consequences of applying the pattern and possible compromises
The standard pattern description includes the definition of the following basic specifications:
- Pattern Name and Classification;
- Intent;
- "Also Known As";
- Motivation;
- Applicability;
- Structure;
- Participants;
- Collaborations;
- Consequences;
- Implementation;
- Sample Code;
- Known Uses;
- Related Patterns.
Solving design tasks with the help of design patterns include the following steps:
- finding the appropriate objects,
- determining the degree of detail of the object,
- specifying the interfaces of the object,
- specifying the implementation of the object.
Design patterns are based on reuse mechanisms. These include:
- inheritance and composition;
- delegation;
- parameterized types.
In order to use the design pattern, you need to read its description, make sure you understand the classes and objects mentioned, see the "Sample Code" section, define the appropriate names for the participants, define the classes, define the names of operations, implement operations that perform responsibilities and are responsible for the relationships defined in the design pattern.
2.2 Classification of Design Patterns
The following groups of design patterns can be distinguished:
- creational design patterns; the most famous of them:
- Abstract Factory
- Builder
- Factory Method
- Dependency Injection
- Lazy Initialization
- Prototype
- Object pool
- Singleton
- structural patterns; in the book "Design Patterns: Elements of Reusable Object-Oriented Software", they include:
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- behavioral patterns; these are, for example, the following patterns
- Visitor
- Interpreter
- Iterator
- Command
- Chain of Responsibility
- Mediator
- Observer
- State
- Strategy
- Memento
- Template Method
- Responsibility assignment patterns (General Responsibility Assignment Software Patterns, GRASP), by book "Applying UML and Patterns – An Introduction to Object-Oriented Analysis and Design and Iterative Development" by Craig Larman; for example, these are:
- Information Expert
- Creator
- Low Coupling
- Protected Variations
There are also system patterns, control patterns, and so on. The patterns of the first three groups are most commonly used.
2.3 Examples of Creational Patterns
2.3.1 Factory Method Pattern
Factory Method is a creational pattern that provides subclasses with an interface to create instances of a class. At the time of creation, the derived classes can determine which class to create. The factory delegates the creation of objects to the derived classes of the base class. This allows you to manipulate abstract objects at a higher level. Also known as Virtual Constructor.
The factory method is used in such cases:
- the class does not know in advance whose objects of subclasses it needs to create;
- the class is designed so that the objects it creates are specified by subclasses;
- the class delegates its responsibility to one of several subclasses, and it is planned to localize the data on which class obtains this responsibility.
There are two approaches to implementing the Factory Method in Java. The first approach is based on the use of static methods. Objects of certain types are created depending on the state or parameters. Consider a simple example.
Suppose there is a Shape
interface:
public interface Shape { void draw(); }
This interface is actively used in some library, including the need to create objects that implement the Shape
interface. When using the library, we will need to work with some classes that implement this interface:
public class Circle implements Shape { @Override public void draw() { System.out.println("Circle"); } } public class Square implements Shape { @Override public void draw() { System.out.println("Square"); } }
Now it is possible to create a class factory, in the method of which the object is created by the name of the type and perform the testing:
public class ShapeFactory { public Shape getShape(String shapeType) { switch (shapeType.toUpperCase()) { case "CIRCLE": return new Circle(); case "SQUARE": return new Square(); default: return null; } } public static void main(String[] args) { ShapeFactory shapeFactory = new ShapeFactory(); Shape shape1 = shapeFactory.getShape("CIRCLE"); shape1.draw(); Shape shape2 = shapeFactory.getShape("SQUARE"); shape2.draw(); } }
Another approach is based on the use of virtual methods. Suppose there is an abstract class AbstractNumber
that declares abstract function of finding the sum of two numbers and the abstract factory method, and also implements a method for finding the sum of the elements of an array of numbers. An abstract factory method is used to create an initial value of the sum:
package ua.inf.iwanoff.oop.sixth; public abstract class AbstractNumber { abstract public AbstractNumber getInstance(); // factory method abstract public AbstractNumber sum(AbstractNumber a, AbstractNumber b); public AbstractNumber sum(AbstractNumber[] arr) { AbstractNumber result = getInstance(); for (AbstractNumber elem: arr) { result = sum(result, elem); } return result; } }
Create a derived class IntegerNumber
:
package ua.inf.iwanoff.oop.sixth; public class IntegerNumber extends AbstractNumber{ private int k = 0; public IntegerNumber() { } public IntegerNumber(int k) { this.k = k; } @Override public AbstractNumber getInstance() { return new IntegerNumber(); } @Override public AbstractNumber sum(AbstractNumber a, AbstractNumber b) { IntegerNumber result = new IntegerNumber(); result.k = ((IntegerNumber) a).k + ((IntegerNumber) b).k; return result; } @Override public String toString() { return "IntegerNumber { k = " + k + " }"; } }
Create a derived class ComplexNumber
:
package ua.inf.iwanoff.oop.sixth; public class ComplexNumber extends AbstractNumber { private double re = 0; private double im = 0; public ComplexNumber() { } public ComplexNumber(double re, double im) { this.re = re; this.im = im; } @Override public AbstractNumber getInstance() { return new ComplexNumber(); } @Override public AbstractNumber sum(AbstractNumber a, AbstractNumber b) { ComplexNumber result = new ComplexNumber(); result.re = ((ComplexNumber) a).re + ((ComplexNumber) b).re; result.im = ((ComplexNumber) a).im + ((ComplexNumber) b).im; return result; } @Override public String toString() { return "ComplexNumber { re = " + re + ", im = " + im + " }"; } }
Test all:
package ua.inf.iwanoff.oop.sixth; public class NumbersTest { public static void main(String[] args) { AbstractNumber[] arr1 = { new IntegerNumber(1), new IntegerNumber(2), new IntegerNumber(4) }; AbstractNumber result1 = arr1[0]; // thus determine the type of object System.out.println(result1.sum(arr1)); AbstractNumber[] arr2 = { new ComplexNumber(1, 2), new ComplexNumber(3, 4) }; AbstractNumber result2 = arr2[0]; // thus determine the type of object System.out.println(result2.sum(arr2)); } }
Assigning items to an array is only necessary to determine the real type of object.
2.3.2 Abstract Factory Pattern
The Abstract Factory pattern is used when it is necessary to change the behavior of the system by varying the objects that are created while maintaining the interfaces. It allows you to create groups of interconnected objects that implement common behavior.
An abstract class factory defines the general interface of class factories. Subclasses have specific implementation of methods for creating different objects. Since the abstract factory implements the process of creating class-factories and the procedure of object initialization itself, it isolates the application from the details of class implementation.
Using an abstract factory has the following advantages:
- isolates specific classes;
- simplifies replacement of product families;
- guarantees product compatibility;
- the system should not depend on how the objects of which it consists are created, assembled and represented:
- interrelated family objects must be used together and this restriction must be met.
2.3.3 Lazy Initialization Pattern
The Lazy Initialization pattern assumes the creation of an object immediately before this object is first used. Thus, the initialization is performed "on demand". For example, the field of SomeType
type contains a reference to an object that can be immediately created simultaneously with the field definition:
SomeType field = new SomeType(); void first() { field.setValue(); } void second() { field.getValue(); }
Sometimes a particular object may not be applicable, but its creation requires significant resources. Then it is better to create it in a certain method, which first refers to this object:
SomeType field = null; void first() { field = new SomeType(); field.setValue(); } void second() { field.getValue(); }
The problem is that the order of the method call can be arbitrary. If another method that uses this object is called first, we will get a NullPointerException
. If you create an object in different methods, the object will be new each time, and the previous one will be destroyed. To solve this problem, you can create a getter method that creates the necessary object:
SomeType field = null; public SomeType getField() { if (field == null) { field = new SomeType(); } return field; }
Now it is important to ensure that all calls to the field are made only through the getter:
void first() { getField().setValue(); } void second() { getField().getValue(); }
2.3.4 Singleton Pattern
The Singleton pattern is applied when the only instance of some class (or no one) is allowed. It can be any large object that should not be replicated. For example, an object may be associated with a file in which debugging information is recorded, etc.
Using static data and methods instead of a single object cannot solve this problem, since the corresponding static data members are created automatically when we first access the class, but we need an approach that allows us to control the creation of a single object.
The easiest solution on Java is to apply the "factory method":
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
To avoid loss of scalability of the project, the Singleton pattern should be used only if the instance uniqueness is unconditional, and not as a temporary solution.
2.4 Examples of Structural Patterns
2.4.1 Adapter Pattern
The Adapter pattern turns the interface of one class into another interface, the one that customers expect. For example, there is some class with the necessary functionality, but it would be desirable if the class is part of a certain hierarchy, or so that it implements a certain interface. The Java language provides two ways to implement this pattern – through inheritance (we create a derived class that implements a specific interface) and through composition (we create a reference to the class that needs to be adapted). The second approach is more universal, since it can be applied to both interfaces and classes, to which another class must be adapted (which, moreover, can be implemented with the final
modifier).
Consider the implementation of the pattern on the example of the adaptation of the MyArray
class from the example of Laboratory training # 2 to the requirements of the standard List
interface. To simplify the work, the class can be copied to a new package:
package ua.inf.iwanoff.oop.sixth; import java.util.Arrays; public class MyArray<T> { private Object[] arr = {}; public MyArray(T... arr) { this.arr = arr; } public MyArray(int size) { arr = new Object[size]; } public int size() { return arr.length; } public T get(int i) { return (T)arr[i]; } public void set(int i, T t) { arr[i] = t; } public void add(T t) { Object[] temp = new Object[arr.length + 1]; System.arraycopy(arr, 0, temp, 0, arr.length); arr = temp; arr[arr.length - 1] = t; } public void remove(int i) { Object[] temp = new Object[arr.length - 1]; System.arraycopy(arr, 0, temp, 0, i); System.arraycopy(arr, i + 1, temp, i, arr.length - i - 1); arr = temp; } @Override public String toString() { return Arrays.toString(arr); } }
The AbstractList
class can be used as a base class for our adapter, and the reference to MyArray
will be its field (we use the approach through the composition). The ArrayAdapter
class will be as follows:
package ua.inf.iwanoff.oop.sixth; import java.util.AbstractList; public class ArrayAdapter<E> extends AbstractList<E> { private MyArray<E> arr = new MyArray<>(); @Override public E get(int index) { return arr.get(index); } @Override public int size() { return arr.size(); } @Override public boolean add(E e) { arr.add(e); return true; } @Override public E remove(int index) { E e = arr.get(index); arr.remove(index); return e; } @Override public E set(int index, E element) { E e = arr.get(index); arr.set(index, element); return e; } }
Now this class can be applied, for example, instead of ArrayList
:
package ua.inf.iwanoff.oop.sixth; import java.util.ArrayList; import java.util.List; public class ArrayAdapterDemo { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); list.add(4); list.remove(2); System.out.println(list); list = new ArrayAdapter<>(); list.add(1); list.add(2); list.add(3); list.add(4); list.remove(2); System.out.println(list); } }
The results should be identical.
2.4.2 Facade Pattern
The Facade pattern is a design pattern intended to unite a group of subsystems under one unified interface, providing access to them through one entry point.
Since a facade class can be used to create the only object, it is advisable to implement such a class using the Singleton pattern.
Consider the use of the Facade pattern for the previously discussed example with the country and population censuses. All project classes and FXML documents should be placed in different packages, as required by the MVC pattern:
- the
model
package contains classes that describe the application domain (Census
,XMLCountry
, etc.); - the
view
package contains FXML documents; - the
controller
package contains controller class.
We'll add a new CensusesFacade
class In the model
package. It will contain a reference to the XMLCountry
class. If necessary, it may also contain references to other model objects.
public class CensusesFacade { private static CensusesFacade instance = null; private XMLCountry country; // references to other model classes, if necessary // private modifier makes it impossible to create
// objects not through the getInstance () method: private CensusesFacade() { } // implementation of the Singleton pattern: public static CensusesFacade getInstance() { if (instance == null) { instance = new CensusesFacade(); } return instance; } // Methods corresponding to the functions of the GUI application // and requiring interaction with the model: public void doNew() { //... } public void doOpen(String fileName) { //... } public void doSave(String fileName) { //... } public void doSort() { //... } // Other methods that correspond to functions of the GUI application }
The controller now only interacts with the facade:
public class CensusesController implements Initializable { // Reference to the facade: private CensusesFacade facade = CensusesFacade.getInstance(); // ... // Event handlers: @FXML public void doNew(ActionEvent event) { facade.doNew(); // Update of visual components } // ... @Override public void initialize(URL location, ResourceBundle resources) { // ... } }
2.5 Examples of Behavioral Patterns
2.5.1 Observer Pattern
The Observer pattern is used when it is necessary to determine the "one to many" connection between objects in such a way that when a state of a single object changes, all objects associated with it receive notifications about it and automatically change their state. In Java, this template is called Listener.
Consider the use of the pattern in this example. The OperationObserver
class defines the update interface for the objects that should be notified of the subject's change:
public abstract class OperationObserver { public abstract double valueChanged(Rectangle observed); }
There is a Rectangle
class (subject), which has information about its observers and provides an interface for registering and notifying observers:
import java.util.*; public class Rectangle { private double width; private double height; private ArrayList<OperationObserver> observerList = new ArrayList<>(); public Rectangle(double width, double height) { this.width = width; this.height = height; } public void addObserver(OperationObserver observer) { observerList.add(observer); } public double getWidth() { return width; } public double getHeight() { return height; } public void setwidth(double width) { this.width = width; notifyObservers(); } public void setHeight(double height) { this.height = height; notifyObservers(); } private void notifyObservers() { for (OperationObserver o : observerList) { o.valueChanged(this); } } public String toString() { String s = ""; for (OperationObserver o : observerList) { s += o.toString() + '\n'; } return s; } }
The Perimeter
and Square
classes are "subscribers" who receive messages about changing the size of the rectangle and update their data when they receive the message:
public class Perimeter extends OperationObserver { private double perimeter; public double valueChanged(Rectangle observed) { return perimeter = 2 * (observed.getWidth() + observed.getHeight()); } public String toString() { return "P = " + perimeter; } } public class Square extends OperationObserver { private double square; public double valueChanged(Rectangle observed) { return square = observed.getWidth() * observed.getHeight(); } public String toString() { return "S = " + square; } }
2.6 Solving design Problems using Patterns
Design patterns can be useful for solving a number of problems:
- search for suitable objects; useful templates are Composer, Strategy, State;
- determining the degree of detail of the object; useful are Facade, Flyweight, Abstract Factory, Builder, Visitor, Command;
- specification of object interfaces; useful may be Decorator, Proxy, Visitor;
- specifying the implementation of objects; used templates Abstract Factory, Builder, Factory Method, Prototype, Singleton;
- reuse mechanisms; here used are State, Strategy, Visitor, Chain of Responsibility;
- construction of runtime structures (Composer, Decorator, Chain of Responsibility);
- designing considering future changes; all the design samples, especially the Facade, Bridge, Template Method, may be useful here.
3 Quiz
- What is a design pattern?
- Describe the components of the MVC pattern.
- When and by whom were the design patterns first systematically described?
- What elements do pattern identification include?
- What specifications does the standard pattern description include?
- What mechanisms are the patterns based on?
- Provide a classification of design patterns.
- What is the idea and implementation of the Factory Method pattern?
- How to implement Factory Method pattern in Java?
- What is the purpose of the Abstract Factory pattern?
- Why use the Lazy Initialization pattern?
- How to implement the Lazy Initialization pattern in Java?
- When is the Singleton pattern used?
- How to implement Singleton pattern in Java?
- What is the purpose of the Adapter pattern?
- How to implement the Pattern Adapter in Java?
- When and for what use is the Facade pattern?
- How is the Facade pattern related to the MVC pattern?
- What is the idea and implementation of the Observer pattern?
- Give examples of using the Observer pattern in JavaFX.
- How should patterns be used to solve software design problems?
- Give examples of the use of composition and polymorphism in design patterns.