При прохождении собеседования почти всегда задается вопрос — Какие паттерны проектирования вы знаете?
На моей работе коллега регулярно проводит собеседования, но он никогда не спрашивает знание паттернов. Наверное сам не знает? Пора переходить на новый качественный уровень!
Использование паттернов проектирования вещь нужная. Поэтому я решил разобраться с ними на примере проектирования Зоопарка.
AnimalFactory factory = new AnimalFactory()
IAnimal elefant = new factory.getAnimal(AnimalFactory.ID_ELEFANT_TYPE)
Всего есть три типа паттернов:
- Creational design patterns — Создает обьекты
- Structural design patterns — Используется для компоновки объектов. Определяет способы составления объектов для получения новых функциональных возможностей
- Behavioral design patterns — Используется наследование, чтобы определить поведение для различных классов
Возьмем зверей и применим к ним Фабрику методов. Это простой Паттерн (Creational pattern) который позволяет получить объект (Class) по Идентификатору, Типу,… (Id, type)
«Define an interface for creating an object, but let subclasses decide which class to instantiate. The Factory method lets a class defer instantiation it uses to subclasses.»
Как определяются используемые классы
// Interface
interface IAnimal{
String getName();
}
// Class 1
public class Elefant implements IAnimal{
@Override
String getName(){
return "Elefant";
}
}
// Class 2
public class Monky implements IAnimal{
@Override
String getName(){
return "Monky";
}
}
// Class N
...
Как реализуется сама Фабрика методов и ее использование
// Factory method
public class AnimalFactory{
public static final int ID_ELEFANT_TYPE = 2;
public static final int ID_MONKEY_TYPE = 1;
public IAnimal getAnimal(int type){
if (type == ID_ELEFANT_TYPE){
return new Elefant()
} else if (type == ID_MONKEY_TYPE){
return new Monky()
}
...
}
}
AnimalFactory factory = new AnimalFactory()
IAnimal elefant = new factory.getAnimal(AnimalFactory.ID_ELEFANT_TYPE)
Log.d(TAG, elefant.getName())
У нас есть данные с которыми можно работать дальше. Если нам понадобится расширить наших животных до Царства, мы можем применить Абстрактную Фабрику (Abstract factory pattern). Это на один уровень абстракции «выше». Т.е. мы будем иметь несколько фабрик. Сначала мы получим фабрику, а потом сам класс
Наш зоопарк будет состоять из рабочих. Которые будут ухаживать за животными, лечить, кормить.
Здесь для рабочих можно применить такую же фабрику методов. Для простоты рассмотрения возьмем простые классы и посмотрим как можно применить поведенческий шаблон. Например Strategy pattern
Описание действий над животными
public interface IAction {
void doAction(Animal animal);
}
// Feeding
public class ActionFeeding implements IAction{
@Override
public void doAction(Animal animal) {
System.out.println("doFeed : " + animal.getName());
}
}
// Vet
public class ActionVet implements IAction{
@Override
public void doAction(Animal animal) {
System.out.println("doVet : " + animal.getName());
}
}
// Strategy
public class TreatContext {
private IAction context;
public TreatContext(IAction context) {
this.context = strategy;
}
// You can even use setter for more flexibility
// public setContext(IAction context) {
// this.context = strategy;
// }
public String executeStrategy(Animal animal) {
return context.doAction(animal.getName());
}
}
Патерн стратегия позволяет изменять поведение класса во время исполнения кода
The Strategy pattern is employed in situations where algorithms or behavior of class should be dynamic. This means that both the behavior and the algorithms can be changed at runtime, based on the input of the client.
Например содержимое класса TreatContext инициализируется действием ActionFeeding. Этот context может динамически принимать наших разных Animal в executeStrategy
Как мы это вызываем и используем
AnimalFactory factory = new AnimalFactory()
IAnimal elefant = new factory.getAnimal(AnimalFactory.ID_ELEFANT_TYPE)
IAnimal monkey = new factory.getAnimal(AnimalFactory.ID_MONKEY_TYPE)
TreatContext contextFeed = new TreatContext(new ActionFeeding())
Log.d(TAG, "Treat Feed : " + contextFeed.executeStrategy(elefant))
Log.d(TAG, "Treat Feed : " + contextFeed.executeStrategy(monkey))
TreatContext contextVet = new TreatContext(new ActionVet())
Log.d(TAG, "Treat Vet : " + contextVet.executeStrategy(elefant))
Log.d(TAG, "Treat Vet : " + contextVet.executeStrategy(monkey))
Как мы можем предположить над всеми животными должны выполняться одни и те же действия. Например если собрать Animals в коллекцию можно обработать в цикле всех животных
List<IAnimal> animals = new ArrayList<IAnimal>();
animals.add(elefant);
animals.add(elefant);
TreatContext contextFeed = new TreatContext(new ActionFeeding())
TreatContext contextVet = new TreatContext(new ActionVet())
for (IAnimal animal: animals){
contextFeed.executeStrategy(animal)
contextVet.executeStrategy(animal)
}
Также, мы можем использовать другой паттерн — Command pattern
Этот паттерн сложнее. Класс, над которым выполняется команда, не связан с выполнением команды и не знает как выполняется эта команда. Позволяет легко организовать историю операций. Есть некое сходство с Фабрикой методов, но в случае с Command pattern идентификатору (Vet_Monkey, Feed_Monkey,...) соответствует некое действие. Fabric of methods is creating objects. Command pattern is behavioral pattern
This enables one to configure a class with a command object that is used to perform a request. The class is no longer coupled to a particular request and has no knowledge (is independent) of how the request is carried out
// Vet
public class CommandVet implements CommandFactory.Command {
private IAnimal animal;
CommandVet(IAnimal animal){
this.animal = animal;
}
@Override
public void execute() {
System.out.println("CommandVet : " + animal.getName());
}
}
// Feed
public class CommandFeeding implements CommandFactory.Command {
private IAnimal animal;
CommandFeeding(IAnimal animal){
this.animal = animal;
}
@Override
public void execute() {
System.out.println("CommandFeeding : " + animal.getName());
}
}
// CommandFactory
public class CommandFactory {
public interface Command {
void execute();
}
private final HashMap<String, Command> commands = new HashMap<>();
private final ArrayList<String> history = new ArrayList<>();
public void add(String commandName, Command command) {
commands.put(commandName, command);
}
public void storeAndExecute(String commandName) {
Optional.ofNullable(commands.get(commandName)).ifPresent(command -> {
command.execute();
history.add(commandName); // optional
});
}
public void printHistory() {
System.out.println(history);
}
}
Как мы это вызываем и используем. Запрос ассоциируется с командой. Vet_Monkey, Feed_Monkey, Vet_Elefant, Feed_Elefant
public static void main(String[] args) {
AnimalFactory animalFactory = new AnimalFactory()
IAnimal elefant = new animalFactory.getAnimal(AnimalFactory.ID_ELEFANT_TYPE)
IAnimal monkey = new animalFactory.getAnimal(AnimalFactory.ID_MONKEY_TYPE)
CommandFactory commandFactory = new CommandFactory();
commandFactory.add("Vet_Monkey", new CommandVet(monkey));
commandFactory.add("Feed_Monkey", new CommandFeeding(monkey));
commandFactory.add("Vet_Elefant", new CommandVet(elefant));
commandFactory.add("Feed_Elefant", new CommandFeeding(elefant));
commandFactory.storeAndExecute("Feed_Monkey");
commandFactory.storeAndExecute("Vet_Monkey");
commandFactory.storeAndExecute("Feed_Elefant");
commandFactory.storeAndExecute("Vet_Elefant");
commandFactory.printHistory();
}
После выполнения мы получим исполнение команд и распечатку истории команд
CommandFeeding: Monkey
CommandVet: Monkey
CommandFeeding: Elefant
CommandVet: Elefant
[Feed_Monkey, Vet_Monkey, Feed_Elefant, Vet_Elefant]
Cледующее что нам надо рассмотреть это посетителей Зоопарка. Применим очень схожий по названию паттерн Visitor. Он подходит если нужно сделать какие либо действия над рядом не связанных объектов или связанных в узлы объектов. Нет необходимости следить за типизацией этих объектов
When new operations are needed frequently and the object structure consists of many unrelated classes, it's inflexible to add new subclasses each time a new operation is required because "[..] distributing all these operations across the various node classes leads to a system that's hard to understand, maintain, and change."
Например у нас есть посетители Взрослые, Студенты, Дети. А также может быть тип посетителей как Семья. Этот тип можно условно считать узлом, включающим в себя группу из ранее перечисленных посетителей. Семья может иметь скидки при посещении. Как и каждый из посетителей в отдельности может иметь скидку тоже
// Base interface
public interface People {
void accept(ZooVisitor visitor);
}
// Visitor interface
public interface ZooVisitor {
void visit(Adult adult);
void visit(Family family);
void visit(Student student);
void visit(Children children);
}
// Adult
public class Adult implements People {
boolean memberOfFamily;
public Adult(boolean memberOfFamily) {
this.memberOfFamily = memberOfFamily;
}
@Override
public void accept(ZooVisitor visitor) {
visitor.visit(this);
}
}
// Student
public class Student implements People {
boolean memberOfFamily;
public Student(boolean memberOfFamily) {
this.memberOfFamily = memberOfFamily;
}
@Override
public void accept(ZooVisitor visitor) {
visitor.visit(this);
}
}
// Family node
public class Family implements People {
private List<People> peoples = new ArrayList<>();
public List<People> getPeoples() {
return peoples;
}
Family addAdult() {
peoples.add(new Adult(true));
return this;
}
Family addChildren() {
peoples.add(new Children(true));
return this;
}
Family addStudent(){
peoples.add(new Student(true));
return this;
}
String getFamilyMembers(){
StringBuffer stringBuffer = new StringBuffer();
for (People people: getPeoples()){
stringBuffer.append("\n " + people.getClass().getSimpleName());
}
return stringBuffer.toString();
}
@Override
public void accept(ZooVisitor visitor) {
for (People people: peoples){
people.accept(visitor);
}
visitor.visit(this);
}
}
// Concrete Visitor implementation
public class ZooDoVisitor implements ZooVisitor {
@Override
public void visit(Adult adult) {
if (adult.memberOfFamily)
System.out.println("Adult Family discount. Family : " + adult.memberOfFamily);
else
System.out.println("Adult full price");
}
@Override
public void visit(Family family) {
System.out.println("Family discount for "
+ family.getPeoples().size() + " person: "
+ family.getFamilyMembers());
}
@Override
public void visit(Student student) {
if (student.memberOfFamily)
System.out.println("Student Family discount. Family : " + student.memberOfFamily);
else
System.out.println("Student 50% discount");
}
@Override
public void visit(Children children) {
System.out.println("Children are free to charge. Family : " + children.memberOfFamily);
}
}
Для определения Ноды(family) или одиночного Посетителя зоопарка используется флаг memberOfFamily. В дальнейшем для подсчета выручки надо ввести серийный номер (Id) билета. Поскольку серийный номер будет привязан к конкретному посетителю а также к группе (Семейный билет), этот флаг можно будет не использовать. Фильтруя по Id мы будем знать какой группе соответствует конкретный Посетитель. Однако для простого обхода и подсчета общего количества Нод он может быть также полезен
Как это используется
public static void main(String[] args) {
ZooVisitor visitor = new ZooDoVisitor();
People adult = new Adult(false);
People student = new Student(false);
People family = new Family().addAdult().addAdult().addStudent().addChildren();
List<People> peopleList = new ArrayList<>();
peopleList.add(adult);
peopleList.add(student);
peopleList.add(family);
for (People people : peopleList){
people.accept(visitor);
}
}
Adult full price
Student 50% discount
Adult Family discount. Family: true
Adult Family discount. Family: true
Student Family discount. Family: true
Children are free to charge. Family: true
Family discount for 4 person:
Adult
Adult
Student
Children
Обратите внимание как мы заполняем Ноду (Family). Это применение другого полезного шаблона — Builder Pattern. Обычно на интервью спрашивают в чем разница между Конструктором и Билдером. Они оба выполняют одни и те же действия — Инициализацию объекта. Но в случае конструктора, если надо добавить один параметр придется переписывать все места где создается этот объект. Или создавать новый конструктор с новым параметром. В случае Билдера это решается более элегантным способом.
People family = new Family().addAdult().addAdult().addStudent().addChildren();
Для изменения действий с посетителями можно переписать класс ZooDoVisitor и мы получим новую функциональность
Зоопарк также включает Административный корпус для ведения расходов и выручки, Клетки для содержания, поступление и передачу животных в другие зоопарки или ветеринарные клиники
To be continued
Links
Design Patterns in Java Tutorial
Software design pattern
2019