DESIGN PATTERNS COLLECTIONAL PATTERNS

Collectional Pattern

Next in the series will be a collection of collection patterns. The main function, like its name suggests, is intended to help deal with operations on sets for purposes such as:

 

handling, manipulating groups, collections of objects

combine classes and objects to create a larger structure

Best design for a class whose instances will not contain duplicated data

Allows defining operations on a set of objects

Some of the basic patterns we will look at in this post are

 

Composite

Iterator

Flyweight

Visitor

Pattern

1. Composite

General introduction

The Composite pattern is used to provide an interface for both single and composite components so that the client can view these components uniformly. That is, the client can treat the composite component and the single component equally.

 

Eg

Example with Unix's file system. The file system will include 2 basic components: files and directories . In which, file can be considered as a single element and directory as a composite element (containing many other files and directories). Therefore, the composite pattern will be suitable for this case.

 

Consider the simple case of getting the size of FileSystemComponent:

 

For FileComponent, will return the size of the file

For DirComponent, will return the total size of the files and directories contained in it

Using the Composite pattern in this case makes it possible for the client to get the dimensions of a FileSystemComponentuniform without knowing how the internals are implemented.

 

For example, in this model, the client wants to get the size of the component just call FileSystemComponetObject.getComponentSize()it, no matter what type of component it is. However, the above design still has some disadvantages such as

 

The client must check the object's data type by itself

The client must be cast to the correct type of the subclass to be able to use its own functions

Usually, when we work with the directory system, we won't need to do this directly. Simple example with the function cdto move to the directory, this is the function of DirComponentbut not in FileComponent. When we call it cd ${fileName}will return an exception

 

It can be seen that calling any function, checking that the function supports the type we want to use will not be taken care of by the client, but by ServiceProviderthe operating system here. The separation of client responsibilities makes the process more convenient to use and limits coupling. To meet this, one turns to a second model

 

At this point, the client can call private functions without casting the return object's type. Private functions will be returned by default, exceptionand subclasses if used will override these functions.

 

public abstract class FileSystemComponent {

public abstract int getComponentSize();

 

public void addComponent(FileSystemComponent fc) {

throw new IllegalComponentStateException(

"Method not supported"

);

}

 

public FileSystemComponent getComponent(int location) {

throw new IllegalComponentStateException(

"Method not supported"

);

}

}

 

Subclasses that do not have their own functions will inherit from the parent class entirely, and unsupported functions will still return exceptions.

 

public class FileComponent extends FileSystemComponent{

@Override

public int getComponentSize() {

return 0;//return component size

}

}

 

Subclasses that want to implement their own functions will override these functions themselves so that they can be called

 

public class DirComponent extends FileSystemComponent{

@Override

public int getComponentSize() {

//return component size

}

 

@Override

public void addComponent(FileSystemComponent fc) {

//add component logic

}

 

@Override

public FileSystemComponent getComponent(int location) {

//return the component at the location

}

}

 

This approach has obvious advantages and disadvantages

 

Advantages Defect

Separation of responsibility for testing support of functions from client side The superclass should have all the default functions and the individual functions of each subclass

2. Iterator

General introduction

Iterator is a design pattern that allows a client to access the contents of a container sequentially without knowing the contents of the container. Container can be simply understood as a set of data or objects. Object here can also be a collection.

 

Internal vs External Iterator

Internal External

The method that accesses the object inside the collection will bind to the collection itself the iterator's functionality will be separate from the collection on an object called iterator .

Only 1 iterator with 1 collection at a time There can be an infinite number of iterators on a collection at a time

Collection must be self-sustaining and store the state of an iterator The state of the iterator will be separate from the collection

In Java default will support an interface Iterator<E>(E is generic type)

 

function prototype return type description

forEachRemaining(Consumer<? super E> action) void Perform action on all the remaining objects in the iterator until all objects are gone or the action returns an exception

hasNext() boolean Returns true if there are still objects in the iterator

next() AND Returns the next object in iterator

remove() void remove the last object returned from the set under consideration

Eg

The data in the examples we all use is a simple txt file below

 

1,test1,hello

2,test2,the quick fox jump over the lazy dog

3,test3,merry Christmas

 

Internal Iterator

 

public class User {

 

private String name;

private String description;

private String message;

 

public User(String name, String description, String message) {

this.name = name;

this.description = description;

this.message = message;

}

 

@Override

public String toString() {

return "User{" +

"name='" + name + '\'' +

", description='" + description + '\'' +

", message='" + message + '\'' +

'}';

}

}

 

This is the model class

 

public class AllUser {

private Vector data;

Enumeration ec;

User nextUser;

 

public AllUser() {

init();

ec = data.elements();

}

 

private void init() {

data = new Vector();

File f = new File("data.txt");

try {

InputStream is = new FileInputStream(f);

Scanner sc = new Scanner(is);

while(sc.hasNextLine()) {

String line = sc.nextLine();

StringTokenizer tokenizer = new StringTokenizer(line, ",");

data.add(new User(tokenizer.nextToken(), tokenizer.nextToken(), tokenizer.nextToken()));

}

} catch (FileNotFoundException e) {

e.printStackTrace();

}

 

}

 

public boolean hasNext(){

boolean match = false;

nextUser = null;

while(ec.hasMoreElements()){

User temp = (User) ec.nextElement();

nextUser = temp;

break;

}

return nextUser != null;

}

 

public Object next() {

if(nextUser != null) {

return nextUser;

} else {

throw new NoSuchElementException();

}

}

 

public void remove() {

 

}

}

 

The Iterator class both acts as a collection to store data and as an iterator that allows the client to access data.

 

public class TestInternalIterator {

public static void main(String[] args) {

AllUser allUser = new AllUser();

 

while(allUser.hasNext()) {

System.out.println(allUser.next().toString());

}

 

}

}

 

Test class to test the operation of iterator AllUser.

 

In the above example, it can be seen that the client (function test) simply initializes the iterator and its data, checks if there are any words left, and calls the next element. It doesn't matter what's inside like how the elements are stored, in what form and other details.

 

External Iterator

 

public class User {

 

private String name;

private String description;

private String message;

 

public User(String name, String description, String message) {

this.name = name;

this.description = description;

this.message = message;

}

 

@Override

public String toString() {

return "User{" +

"name='" + name + '\'' +

", description='" + description + '\'' +

", message='" + message + '\'' +

'}';

}

 

public String getName() {

return name;

}

 

public String getDescription() {

return description;

}

 

public String getMessage() {

return message;

}

}

 

model class

 

public class AllUser {

private Vector data;

Enumeration ec;

User nextUser;

 

public AllUser() {

init();

ec = data.elements();

}

 

private void init() {

data = new Vector();

File f = new File("data.txt");

try {

InputStream is = new FileInputStream(f);

Scanner sc = new Scanner(is);

while(sc.hasNextLine()) {

String line = sc.nextLine();

StringTokenizer tokenizer = new StringTokenizer(line, ",");

data.add(new User(tokenizer.nextToken(), tokenizer.nextToken(), tokenizer.nextToken()));

}

} catch (FileNotFoundException e) {

e.printStackTrace();

}

 

}

 

public Enumeration getAllUsers() {

return data.elements();

}

 

public Iterator getFilteredUser(boolean isEven) {

return new FilteredUser(this, isEven);

}

}

 

collection class

 

public class FilteredUser implements Iterator {

 

private Vector vector;

AllUser allUser;

boolean isEven;

Enumeration enumeration;

User nextUser;

 

public FilteredUser(AllUser allUser, boolean isEven) {

this.allUser = allUser;

this.isEven = isEven;

enumeration = allUser.getAllUsers();

}

 

@Override

public boolean hasNext() {

boolean matchFound = false;

while(enumeration.hasMoreElements()) {

User temp = (User) enumeration.nextElement();

if(Integer.parseInt(temp.getName()) % 2 == (isEven ? 0 : 1)) {

matchFound = true;

nextUser = temp;

break;

}

}

return matchFound;

}

 

@Override

public Object next() {

if(nextUser == null) {

throw new NoSuchElementException();

} else {

return nextUser;

}

}

 

@Override

public void remove() {

}

}

 

iterator class

 

public class TestInternalIterator {

public static void main(String[] args) {

AllUser allUser = new AllUser();

Iterator it = allUser.getFilteredUser(true);

while (it.hasNext()){

System.out.println(it.next());

}

}

}

 

test class

 

3. Flyweight

General introduction

The information of an object usually has one of two or both types of information:

 

intrinsic: Information that is independent of the object's context. That is, this information does not depend on the state of the object. This information is usually fixed and the same between objects. For example, with employee objects, information such as company name, company address, etc. will be the same between objects.

extrinsic: The information depends and changes according to the context of the object. This information of different objects will be different. For example name, date of birth, .. of employee.

For information of the intrinsic type , if all objects store this information, it will lead to unnecessary redundancy. Therefore, the Flyweight pattern was introduced with the idea that this information would be stored in other Flyweight objects and that other objects would share the same Flyweight object.

 

Flyweight requirements

Each flyweight has only one corresponding object and shares it with a group of appropriate objects

client object should not have permission to create flyweight objects directly

The client object should have some way to read the flyweight object's data if needed

How to build

Flyweight class should have only private constructor to prevent client object from being able to instantiate these objects

Since a flyweight class is recommended to have only one instance, it is often used with the Singleton pattern.

Eg

First, suppose we have a business card class as follows

 

public class VCard {

private String name;

private String title;

private String company;

private String address;

private String city;

private String state;

private String zip;

public void print() {

}

}

 

As can be seen, in the properties of this class, the intrinsic properties will include:

 

company

address

city

state

zip

Therefore, we can use the Flyweight pattern here.

 

First direction

 

In this way, we will display extrinsic information in the object and intrinsic information in a Flyweight object.

 

The first is an interface that Flyweight will implement

 

public interface FlyweightInterface {

public String getCompany();

public String getAddress();

public String getCity();

public String getState();

public String getZip();

}

 

Next, we will design a sigleton FlyweigthtFactoryto initialize the singleton Flyweight objects corresponding to each of the different parts.

 

Flyweight class will implement as inner class of FlyweightFactory

Flyweight class will only have a private constructor to prevent external objects from being able to instantiate its instance

FlyweightFactoryis the outer class of Flyweight, so it is still possible to initialize the instance ofFlyweight

Function of FlyweightFactory:

Maintain a HashMap of instances of each Flyweight class

Khi client request 1 flyweight:

If the instance is already in the HashMap, that instance is returned

If there is no corresponding instance, it will create a new instance, add it to HashMap and return this instance to the client

Because FlyweightFactoryof using Singleton pattern, our objects Flyweightwill also be unique

public class FlyweightFactory {

//store the instance of division flyweight that has been created

private HashMap<String, FlyweightInterface> listFlyweight;

//2 statements below implement singleton pattern to flyweightFactory

private static FlyweightFactory factory = new FlyweightFactory();

private FlyweightFactory() {

listFlyweight = new HashMap();

}

 

public synchronized FlyweightInterface getFlyweight(String divisionName) {

if(listFlyweight.get(divisionName) != null) {

return listFlyweight.get(divisionName);

} else {

FlyweightInterface fw = new Flyweight(divisionName);

listFlyweight.put(divisionName, fw);

return fw;

}

}

 

//inner singleton Flyweight class that only allow FlyweightFactory and itself create instance

class Flyweight implements FlyweightInterface {

 

private String company;

private String address;

private String city;

private String state;

private String zip;

 

private void setValue(String company, String address, String city, String state, String zip) {

this.company = company;

this.address = address;

this.city = city;

this.state = state;

this.zip = zip;

}

 

//predefine data for each division

private Flyweight(String division) {

switch (division.toLowerCase()) {

case "north":

setValue("CompanyX", "address 1", "city 1", "street 1", "10000");

break;

case "south":

setValue("CompanyX", "address 2", "city 2", "street 2", "20000");

break;

case "east":

setValue("CompanyX", "address 3", "city 3", "street 3", "30000");

break;

case "west":

setValue("CompanyX", "address 4", "city 4", "street 4", "40000");

break;

}

}

 

@Override

public String getCompany() {

return null;

}

 

@Override

public String getAddress() {

return null;

}

 

@Override

public String getCity() {

return null;

}

 

@Override

public String getState() {

return null;

}

 

@Override

public String getZip() {

return null;

}

}

}

 

Then, VCardthe class will only store extrinsic information and a Flyweightcorresponding object

 

public class VCard {

private String name;

private String title;

private FlyweightInterface companyDetail;

 

public VCard(String name, String title, FlyweightInterface companyDetail) {

this.name = name;

this.title = title;

this.companyDetail = companyDetail;

}

 

public void print() {

System.out.println("name: " + name);

System.out.println("title: " + title);

System.out.println("Company: " + companyDetail.getCompany());

System.out.println("City: " + companyDetail.getCity());

System.out.println("State: " + companyDetail.getState());

System.out.println("Address: " + companyDetail.getAddress());

System.out.println("Zip: " + companyDetail.getZip());

}

}

 

Direction 2

 

Similar to method 1, only, storing extrinsic properties will be in the calling function instead of being represented as an object.

 

Change function print()

convert function print()to Flyweightclass

Add parameters as extrinsic attributes

public void print(String name, String title)

 

Change the logic so that the print function prints the necessary information

With this approach, just initialize 4 Flyweightobjects. There is no need to instantiate client objects for extrinsic information as we can pass them directly through parameters. If the requirement is only to display information like for VCard and little private information, it is quite suitable. However, this way will limit customization for client objects if other operations are required, passing information as parameters instead of storing it in the object sometimes leads to inconvenience.

 

4. Visitor

General introduction

Visitor is a design pattern with the purpose of defining an operation on an object in a set of objects of different types in the same hierarchy without changing any of the classes of the objects in that collection. To do so, we will define independent operations on objects on a class Visitor . For each new operation, just define a new visitor without changing the object's classes.

 

Usually, one will define an Interface for other visitors to inherit and implement the logic.

 

public interface VisitorInterface {

void visit(ClassA classA);

void visit(ClassB classB);

}

 

The visitor classes will inherit from here and implement their logic for each class under consideration. To use visitors, there will usually be 2 directions

 

The client calls itself to the visit function and the corresponding object.

Objects in the hierarchy will have a function accept(VisitorInterface visitor)that allows a call to the visitor's visit function with the parameter being the class itself ( this ).

When adding a new operation on a set of objects, we simply add 1 visitor to inherit from VisitorInterface.

 

When a set of objects has a new type, VisitorInterfaceand the classes that inherit from it will have to define a visit function with the corresponding type.

 

Eg

Here I take an example with a set of Order:

 

OverseaOrder: total amount equal to price + shipping price

CaliforniaOrder: total amount = price + 10% tax

NonCaliforniaOrder: total amount = total price

public interface Order {

void accept(OrderVisitor visitor);

}

 

Order class

 

public class CaliforniaOrder implements Order{

private double totalAmount;

 

public CaliforniaOrder(double totalAmount) {

this.totalAmount = totalAmount;

}

 

@Override

public void accept(OrderVisitor visitor) {

visitor.visit(this);

}

 

public double getTotalAmount() {

return totalAmount;

}

 

public double getAdditionalTax() {

return totalAmount * 0.1;

}

}

 

CaliforniaOrder class

 

public class NonCaliforniaOrder implements Order{

 

private double totalAmount;

 

public NonCaliforniaOrder(double totalAmount) {

this.totalAmount = totalAmount;

}

 

@Override

public void accept(OrderVisitor visitor) {

visitor.visit(this);

}

 

public double getTotalAmount() {

return totalAmount;

}

 

}

 

NonCaliforniaOrder class

 

public class OverseaOrder implements Order{

 

private double totalAmount;

private int distance;

 

public OverseaOrder(double totalAmount, int distance) {

this.totalAmount = totalAmount;

this.distance = distance;

}

 

@Override

public void accept(OrderVisitor visitor) {

visitor.visit(this);

}

 

public double getTotalAmount() {

return totalAmount;

}

 

public double getAdditionalSH() {

return distance * 0.05;

}

}

 

OverseaOrder

 

Now to define operations on the set of orders, I will define an interface for the vistor

 

public interface OrderVisitor {

void visit(OverseaOrder overseaOrder);

void visit(NonCaliforniaOrder nonCaliforniaOrder);

void visit(CaliforniaOrder californiaOrder);

}

 

When you need to do something on the set of orders, just define a visitor that inherits from this interface. For example, I want to calculate the total value of orders in the set, I will define 1 visitor to calculate the total as follows

 

public class SumAmountVisitor implements OrderVisitor{

private double totalAmount = 0;

 

public SumAmountVisitor() {

}

 

@Override

public void visit(OverseaOrder overseaOrder) {

System.out.println("calculate on " + overseaOrder.getClass().getSimpleName().toString());

totalAmount += overseaOrder.getTotalAmount() + overseaOrder.getAdditionalSH();

}

 

@Override

public void visit(NonCaliforniaOrder nonCaliforniaOrder) {

System.out.println("calculate on " + nonCaliforniaOrder.getClass().getSimpleName().toString());

totalAmount += nonCaliforniaOrder.getTotalAmount();

}

 

@Override

public void visit(CaliforniaOrder californiaOrder) {

System.out.println("calculate on " + californiaOrder.getClass().getSimpleName().toString());

totalAmount += californiaOrder.getTotalAmount() + californiaOrder.getAdditionalTax();

}

 

public double getTotalAmount() {

return totalAmount;

}

}

 

It is possible to test the operation of this class with a simple program

 

public class OrderManager {

public static void main(String[] args) {

List<Order> orderList = new ArrayList<>();

orderList.add(new CaliforniaOrder(100));

orderList.add(new CaliforniaOrder(200));

orderList.add(new OverseaOrder(50, 2000));

orderList.add(new NonCaliforniaOrder(350));

 

SumAmountVisitor sumAmountVisitor = new SumAmountVisitor();

for(Order order: orderList) {

order.accept(sumAmountVisitor);

}

System.out.println(sumAmountVisitor.getTotalAmount());

}

}

 

When you want to add another operation, just declare another visitor without making any changes to the objects in the given set.

 

public class MaxAmountVisitor implements OrderVisitor{

private double maxAmount = 0;

 

@Override

public void visit(OverseaOrder overseaOrder) {

maxAmount = Math.max(maxAmount, overseaOrder.getTotalAmount()+ overseaOrder.getAdditionalSH());

}

 

@Override

public void visit(NonCaliforniaOrder nonCaliforniaOrder) {

maxAmount = Math.max(maxAmount, nonCaliforniaOrder.getTotalAmount());

}

 

Catalog: