Паттерны проектирования

Соколов Артём Сергеевич

Паттерны - это решения в дизайне или архитектуре программного обеспечения, которые зарекомендовали себя на практике при решении часто встречающихся проблем.

Введение - Определение

  • Делают решение задачи более структурированным

  • Помогают избегать подводных камней с которыми встречается разработчик

  • Проверено временем в разных ситуациях весьма успешное

Введение - Зачем?

Какие бывают:

  • Объектно-ориентированного программирования

  • Функционального программирования

  • Архитектурные/системные (в ИС)

  • Специфические области (бд, сеть)

 

Введение

Немного истории

Gang of Four

  • Первые кто представил паттерны в программировании:
  • Design Patterns: Elements of Reusable Object-Oriented Software 1st Edition
  • 1994 г.
  • На основе C++ и Smaltalk
  • Некоторые вещи не совсем актуальны
  • Классическая книга по паттернам

 

Немного истории

Gang of Four

  • Старые паттерны меняются в реализации (например из-за новых возможностей языков)

  • Новые подходы в программировании   🡆  Новые паттерны

Немного истории

Развитие

  1.  Название

  2.  Проблема которую решает

  3.  Пример реализации

  4.  Что даст в итоге

Основные составляющие части

  • Идентифицирует паттерн

1. Название

  • Используется: 

      - в названиях классов

      - в комментариях к коду

      - в общении

  • Новый функционал или изменения старого -> возникают сложности
  • Паттерн решает проблему
  • Это важно и является самым главным в паттерне

2. Проблема

  • Детализованное решение на ЯП или псевдокодом
  • Помогает понять как применить паттерн
  • Конкретная реализация может сильно отличаться
  • Пример не идеален

 

3. Пример реализации

  • Плюсы и минусы 
  • Тонкости
  • Использование в разных ситуациях
  • Возможные проблемы в будущем (ограничения)

 

4. Что дает

  • Язык общения и способ обмена идеями
  • Программисты лучше понимают друг друга
  • Не тратится время на тривиальные вещи

 

Паттерны – зачем?

Общение

Проще работать с кодом:

  • Легче вносить изменения
  • Проще читать и разбираться в своем коде

Паттерны – зачем?

Качество кода

  • В библиотеках (фреймворках) и в соседних проектах
  • Позволяют быстро в них ориентироваться

Паттерны – зачем?

Повсеместны

  • Не решение всех проблем в любых ситуациях

 

Паттерны -

не серебряная пуля

  • Возможные проблемы:
    - Не правильное наименование
    - Не та проблема (не правильно выбран)
    - Слишком много паттернов
  • Сложно теоретически освоить
  • Нужна практика и опыт
  • Не бойтесь и используйте
  • Помните про возможные проблемы

Путь к мастерству

  • Не правильное наименование
  • Не та проблема (не правильно выбран паттерн)
  • Слишком много паттернов

 

Возможные проблемы

  • Порождающие (Creational) – создание объектов
  • Структурные (Structural) – структура отношений между классами
  • Поведенческие (Behavioral) - эффективное и безопасное взаимодействие, поведение кода

Классификация - классическая

  • порождающий паттерн проектирования, который гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа

 

Singleton (Одиночка)

  • Операционная система устройства с 1 экраном
  • Класс ответственный за экран – Display
  • Подпрограммы отображают на экран уведомления – их классам нужен доступ до экземпляра кл. Display
  • Серьезная ошибка когда подпрограмма не использовала текущий экземпляр экрана

 

Singleton – Ситуация

  • Нужно гарантировать наличие единственного экземпляра класса. 
  • Глобальная точка доступа до этого экземляра.

Singleton – Решаемая проблема

public class Display {

   
public void draw(Object obj){ /* отображаем */ }
}

 

 

public class Configuration {
   
   
public static Display DISPLAY = new Display();
}

Singleton – Реализация (не используя)

public class GoodSubProgram {

   
public void doSomething(){
       
Display display = Configuration.DISPLAY;
       
display.draw("Hello world");
    }
}

public class BadSubProgram {

   
public void doSomething(){
       
Display displayDuplicate = new Display();
       
displayDuplicate.draw("Hello world");
    }
}

Singleton – не используя

public class Display {

   
private Display(){ /* инициализация*/ }

   
public static final Display INSTANCE = new Display();

   
public void draw(Object obj){ /* отображаем */ }
}

Singleton – Пример реализации

public class GoodSubProgram {

   
public void doSomething(){
       
Display displaySingleton = Display.INSTANCE;

        display.draw("Hello world");
    }
}

public class BadSubProgram {

   
public void doSomething(){

  // ! не скомпилируется
        Display displayDuplicate = new Display();
       
displayDuplicate.draw("Hello world");
    }
}

  • это порождающий паттерн проектирования, который позволяет создавать сложные объекты пошагово

 

Builder (Строитель)

  • Есть сложный объект создание которого через конструктор (или другие порождающие паттерны) - неудобно, не читаемо, не очевидно.

 

Builder (Строитель) - Проблема

public class SubProgram {

   
public SubProgram(boolean withGui,
                     
int port,
                     
KeyProcessor keyProcessor,
                     
Object someParameter1,
                     
Object someParameter2,
                     
Object someParameter3,
                     
Object someParameter4
                      ){}

}

 

Builder - Проблема

SubProgram someSubProgram1_1 = new SubProgram(true, 9000, new KeyProcessor(Arrays.asList("w", "s"),

    Collections.emptyList()), null, null, null, null);

Builder - Проблема

SubProgram someSubProgram2_1 = new SubProgram(
       
false,
       
8000,
       
new KeyProcessor(Collections.singletonList("e"), Collections.emptyList()),
       
null,
       
null,
       
null,
       
null
);

Builder - Пример

SubProgramBuilder builder = new SubProgramBuilder();

SubProgram someSubProgram1_2 = builder
        .withGui(true)
        .
port(9000)
        .
responseToKey("w")
        .
responseToKey("a")
        .
build();


SubProgram someSubProgram2_2 = new SubProgramBuilder()
        .
port(8000)
        .
responseToKey("e")
        .
build();

Builder - Пример

public class SubProgramBuilder {
   
private static int defaultPort = 1234;
   
private boolean withGui = false;
   
private int port = defaultPort;
   
private Object someParameter1 = new Object();
   
private Object someParameter2 = new Object();
   
private Object someParameter3 = new Object();
   
private Object someParameter4 = new Object();

   
private List<String> keyBindings = new ArrayList<>();
   
private List<String> ignoredKeys = new ArrayList<>();

Builder - Пример

SubProgramBuilder builder = new SubProgramBuilder();

SubProgram someSubProgram1_2 = builder
        .withGui(true)
        .
port(9000)
        .
responseToKey("w")
        .
responseToKey("a")
        .
build();


SubProgram someSubProgram2_2 = new SubProgramBuilder()
        .
port(8000)
        .
responseToKey("e")
        .
build();

Builder - Пример

public SubProgramBuilder withGui(boolean isWithGui) {
   
withGui = isWithGui;
   
return this;
}


public SubProgramBuilder port(int specifiedPort) {
   
port = specifiedPort;
   
return this;
}


public SubProgramBuilder responseToKey(String key) {
   
keyBindings.add(key);
   
return this;
}

Builder - Пример

public SubProgram build() {
   
return new SubProgram(
           
withGui,
           
port,
           
new KeyProcessor(keyBindings, ignoredKeys),
           
someParameter1,
           
someParameter2,
           
someParameter3,
           
someParameter4
   
);
}

  • это структурный паттерн проектирования, который предоставляет простой интерфейс к сложной системе классов, библиотеке или фреймворку.

Facade (Фасад)

  • Есть сложный и гибкий функционал который реализуется множеством классов
  • Вы хотите использовать лишь часть этого функционала определенным образом – вам не нужна мощь и гибкость, а нужна простота

Facade (Фасад) - Проблема

// нужно прочитать строки из файла
String encoding = "UTF-8";

try {
   
String filePath = args[0];

    URI
fileUri = ClassLoader.getSystemClassLoader().getResource(filePath).toURI();

   
File fileFromUri = new File(fileUri);

   
Scanner input = new Scanner(fileFromUri, encoding);

   
List<String> result = new ArrayList<>();
   
while (input.hasNext()) {
       
String nextLine = input.nextLine();
       
result.add(nextLine);
    }

   
input.close();
}
catch (Exception e){
   
// ...
}

Facade - Пример 

  • Можно выделить в статический метод в каком-то классе 
  • Но что если мы хотим как-то оптимизировать работу между вызовами – например кэшировать результаты или переиспользовать ресурсы

 

Facade – Пример 

public class FileReadingFacade {

   
// тут что-то что нужно для работы
   
Object someHeavyImportantStaff = null;

   
public FileReadingFacade(String someHeavyImportantStuff) {
       
this.someHeavyImportantStaff = someHeavyImportantStuff;
    }

   
public List<String> readFileForLines(String pathToFile){
       
try {
            URI
fileUri = getClass().getResource(pathToFile).toURI();
           
File fileFromUri = new File(fileUri);
           
Scanner input = new Scanner(fileFromUri);
           
List<String> result = new ArrayList<>();
           
while (input.hasNext()) {
               
String nextLine = input.nextLine();
               
result.add(nextLine);
            }
           
input.close();
           
return result;
        }
catch (Exception e){
           
throw new RuntimeException(e);
        }
    }
}

FileReadingFacade fileReadingFacade =

   new FileReadingFacade(getSomeHeavyStuff());

fileReadingFacade.readFileForLines(args[0]);

// между вызовами может сохранять кэш

// или переиспользовать ресурсы
fileReadingFacade.readFileForLines(args[2]);
fileReadingFacade.readFileForLines(args[4]);

Facade – Пример 

  • это структурный паттерн проектирования, который позволяет гибко добавлять объектам новую функциональность, оборачивая их в полезные «обёртки».

 

Decorator/Wrapper (Декоратор/Обертка)

  • Хотим добавлять разную функциональность для подпрограмм 
 
  • Например: 
    - проверять права, 
    - логгировать начало и конец выполнения, 
    - отправлять уведомления​

Decorator/Wrapper (Декоратор/Обертка)

public interface SubProgram {
   
void run();
}

 

public class SubProgramA implements SubProgram {
   
@Override
    public void run() {
       
// делает что-то полезное
   
}
}

Decorator - Пример

public class SubProgramAWithAuthorization extends SubProgramA {

   
@Override
    public void run() {
       
checkRights();
       
super.run();
    }

   
private void checkRights(){};
}

Decorator - без паттерна (наследование)

public class SubProgramAWithLogging extends SubProgramA {

   
@Override
    public void run() {
       
logStart();
       
super.run();
       
logEnd();
    }

   
private void logStart(){}
   
private void logEnd(){}
}

Decorator - без паттерна

public class SubProgramAWithAuthorizationWithLogging

  extends SubProgramAWithAuthorization {

   
@Override
    public void run() {
       
logStart();
       
super.run();
       
logEnd();
    }

   
private void logStart(){}
   
private void logEnd(){}
}

Decorator - без паттерна

public class AuthorizationWrapper implements SubProgram {

   
SubProgram wrappedSubProgram;

   
public AuthorizationWrapper(SubProgram object) {
       
this.wrappedSubProgram = object;
    }

   
@Override
    public void run() {
       
checkRights();
       
wrappedSubProgram.run();
    }

   
private void checkRights(){};
}

Decorator - Пример

public class LoggingWrapper implements SubProgram {
   
SubProgram wrappedSubProgram;
   
public LoggingWrapper(SubProgram object) {
       
this.wrappedSubProgram = object;
    }

   
@Override
    public void run() {
       
logStart();
       
wrappedSubProgram.run();
       
logEnd();
    }

   
private void logStart() {}
   
private void logEnd() {}
}

Decorator - Пример

public class NotificationWrapper implements SubProgram {

   
SubProgram wrappedSubProgram;

   
public NotificationWrapper(SubProgram object) {
       
this.wrappedSubProgram = object;
    }

   
@Override
    public void run() {
       
wrappedSubProgram.run();
       
sendNotification();
    }

   
private void sendNotification(){};
}

Decorator - Пример

SubProgram subProgram = new SubProgramA();

SubProgram wrappedSubprogram1 = new LoggingWrapper(
       
new NotificationWrapper(
               
new AuthorizationWrapper(
                       
subProgram
                )
        )
);

Decorator - Пример

boolean isAuthorizationNeeded = false;
boolean isLoggingEnabled = true;


SubProgram subProgramDynamic = new SubProgramA();

// динамически
if(isAuthorizationNeeded){
   
subProgramDynamic = new AuthorizationWrapper(subProgramDynamic);
}

if(isLoggingEnabled){
   
subProgramDynamic = new LoggingWrapper(subProgramDynamic);
}


subProgramDynamic.run();

  • это поведенческий паттерн проектирования, который даёт возможность последовательно обходить элементы некоторого множества объектов, не раскрывая внутреннего устройства этих множеств

     

Iterator (Итератор)

  • Множества объектов могут по разному реализовываться
  • Список, дерево, хеш-таблица, строки в базе данных
  • Алгоритмы обхода у всех разные
  • Хотим иметь простой интерфейс для этого​

Iterator (Итератор)

public interface Iterator<E> {
   
boolean hasNext();

   
E next();
}

 

void printAllElements(Collection<String> coll) {
   

    Iterator<String> i = coll.iterator();


    while (i.hasNext()) {
       
String s = i.next();
       
System.out.println(s);
    }
}

Iterator

void printAllElements(MyTree<String> tree) {
   
// алгоритм обход дерева и на каждом элементе вызываем 1 раз нужный нам функционал
   
System.out.println(element);

}


void printAllElements(MyList<String> list) {
   
// проходим по списку и вызываем на нем нужный функционал
   
System.out.println(element);
}

Iterator – без паттерна

void printAllElements(Collection<String> coll) {
   

    Iterator<String> i = coll.iterator();


    while (i.hasNext()) {
       
String s = i.next();
       
System.out.println(s);
    }
}

 

void printAllElements(Collection<String> coll) {
   
for (String s : coll) {
       
System.out.println(s);
    }
}

Iterator

  • Listener – слушатель события
  • Publish-subscribe – Издатель-подписчик
  • Observer и observable - наблюдатель и наблюдаемый объект

 

Listener

Другие названия

  • это поведенческий паттерн проектирования, который создаёт механизм подписки, позволяющий одним объектам следить и реагировать на события, происходящие в других объектах
  • Позволяет достичь разделения логики создания события от его обработки.

 

Listener

  1.  Event – событие, имеет тип и доп. информацию
  2.  EventSource - создает события
  3.  EventBus/EventBroker - передает события на обработку
  4.  EventListener - обрабатывает события на которые подписался

 

Listener

Listener

public static class Editor {
   
private EventBroker eventBroker;
   
private File file;

   
public Editor(EventBroker eventBroker) {

        this.eventBroker = eventBroker;

    }

   
public void openFile(String filePath) {
       
this.file = new File(filePath);
       
eventBroker.newEvent("open", file);
    }

   
public void saveFile() throws Exception {
       
// сохранение
       
eventBroker.newEvent("save", file);
    }
}

Listener – Пример редактор файлов

public static class EventBroker {
   
Map<String, List<EventListener>> listeners = new HashMap<>();

   
public EventBroker(String... eventTypes) {
       
for (String operation : eventTypes) {
           
this.listeners.put(operation, new ArrayList<>());
        }
    }

   
public void subscribe(String eventType, EventListener listener) {
       
List<EventListener> users = listeners.get(eventType);
       
users.add(listener);
    }

   
public void unsubscribe(String eventType, EventListener listener) {
       
List<EventListener> users = listeners.get(eventType);
       
users.remove(users.indexOf(listener));
    }

Механизм уведомления (брокер)

    public void newEvent(String eventType, File file) {
       
List<EventListener> users = listeners.get(eventType);
       
for (EventListener listener : users) {
           
listener.handleEvent(eventType, file);
        }
    }
}

Механизм уведомления (брокер)

public interface EventListener {
   
void handleEvent(String eventType, File file);
}


public static class CriticalFilesChangedListener implements EventListener {
   
@Override
    public void handleEvent(String eventType, File file) {
       
if(isCritical(file)) { alert(); }
    }

   
private boolean isCritical(File file) { return true; }
   
private void alert() {}
}

Listener – сам слушатель

public static class LogFileOperationsListener implements EventListener {
   
@Override
    public void handleEvent(String eventType, File file) {
       
System.out.println("Someone has performed " + eventType

    + " operation with the following file: " + file.getName());
    }
}

Listener – сам слушатель

EventBroker eventBroker = new EventBroker("open", "save");
Editor editor = new Editor(eventBroker);
LogFileOperationsListener operationLogingListener = new LogFileOperationsListener();

eventBroker.subscribe("open", operationLogingListener);
eventBroker.subscribe("save", operationLogingListener);

eventBroker.subscribe("save", new CriticalFilesChangedListener());

try {
   
editor.openFile("test.txt");
   
editor.saveFile();
}
catch (Exception e) {
   
e.printStackTrace();
}

Listener – применение

Три самых распространенных методов для обработки коллекций данных.

  • filter - оставляет в коллекции только те элементы, которые удовлетворяют условию
  • map - преобразует каждый элемент коллекции в другой элемент по определенному правилу
  • reduce - преобразует коллекцию к какому-то одному элементу

Filter/Map/Reduce

int processData(List<String> list) {

   return list.stream()

       .filter(s -> s.endsWith("!"))

       .map(s -> countWords(s))

       .reduce(0, (s1, s2) -> s1 + s2);

}

Filter/Map/Reduce

  • Пришло из функционального программирования и сейчас есть везде.
  • Так же это базовые методы для параллельной и распределенной обработки данных.
  • Примеры: Apache Hadoop, Apache Spark

Filter/Map/Reduce

  • Позволяет отделить шаблонную повторяющуюся логику от специфичной логики для данного конкретного места

Template Method (Шаблонный метод)

public abstract class AbstractTest {


    abstract protected void prepare();
   
abstract protected boolean run();
   
protected void releaseResources(){};


    public void runTest() {
       
prepare();
       
long start = System.currentTimeMillis();
       
boolean isSuccessful = run();
        TestHelper.logTime(start);

        releaseResources();
    }
}

Template Method

public class ConcreteTest extends AbstractTest {

   
@Override
    protected void prepare() {
       
// какие-то специальные действия по подготовке теста к запуску
   
}

   
@Override
    protected boolean run() {
       
// логика самого теста
   
}

   
@Override
    protected void releaseResources() {
       
// логика освобождения ресурсов если надо
   
}
}

Несколько ссылок