Паттерны проектирования. Часть 2. Структурные шаблоны

Паттерны проектирования, Шаблоны проектирования, Порождающие паттерны проектирования, паттерны проектирования java, паттерны проектирования классов, Структурные паттерны, Структурные шаблоны

Вторая статья из цикла статей, посвящённого паттернам проектирования. В этой статье мы рассмотрим различные структурные паттерны (шаблоны) проектирования и их практическое применение. Мы изучим, как они помогают улучшить архитектуру программных систем, сократить издержки и обеспечить легкость поддержки и расширения.

О том что такое порождающие и поведенческие паттерны (шаблоны) проектирования мы рассматривали в статьях Паттерны проектирования. Часть 1. Порождающие шаблоны и Паттерны проектирования. Часть 3. Поведенческие шаблоны

Что такое структурные паттерны проектирования?

Структурные паттерны проектирования (Structural Patterns) — это еще одна группа шаблонов проектирования, используемых в программировании. Шаблоны этой группы фокусируются на том, как классы и объекты могут быть объединены в более крупные структуры, обеспечивая гибкость и удобство обслуживания кода. Структурные шаблоны обращаются к вопросам компоновки объектов и их взаимосвязи.

Суть структурных шаблонов

Структурные шаблоны проектирования — это категория шаблонов, которые сосредотачиваются на том, как классы и объекты могут быть объединены в более крупные структуры. Они определяют отношения между объектами, классами и интерфейсами, что позволяет создавать более сложные системы.

Важное понятие в структурных шаблонах — это композиция. Это процесс создания более крупных объектов и структур из более мелких. Композиция позволяет создавать сложные системы, разбивая их на более простые и переиспользуемые части.

Основная цель

Основная цель структурных шаблонов проектирования — улучшить структуру программных систем. Это достигается путем разделения ответственностей между компонентами системы, облегчая поддержку и изменение кода. Важно создавать системы, которые легко поддерживать, модифицировать и расширять, и структурные шаблоны помогают достичь этой цели.

Зачем они нужны?

Структурные шаблоны имеют несколько важных преимуществ:

  1. Разделение ответственностей: Они позволяют разделить ответственности между различными компонентами системы. Это улучшает структуру и делает систему более модульной.
  2. Совместное использование кода: Структурные шаблоны позволяют совместно использовать код между различными классами и объектами. Это устраняет дублирование кода и обеспечивает согласованный интерфейс для взаимодействия.
  3. Улучшение расширяемости: Они обеспечивают способы динамического добавления нового функционала и изменения структуры системы без необходимости изменения существующего кода.
  4. Уменьшение связности: Структурные шаблоны уменьшают связность между компонентами системы, что делает систему более гибкой и управляемой.

Паттерн «Адаптер» (Adapter)

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

Суть паттерна

Суть паттерна «Адаптер» заключается в создании промежуточного класса или объекта (адаптера), который преобразует интерфейс одного класса в интерфейс, ожидаемый другим классом. Таким образом, объекты с несовместимыми интерфейсами могут взаимодействовать друг с другом.

Пример

Давайте рассмотрим пример, где у нас есть два класса с несовместимыми интерфейсами: OldSystem и NewSystem. OldSystem имеет метод doWorkOld(), а NewSystem имеет метод doWorkOld(). Нам нужно интегрировать OldSystem в NewSystem.

Сначала создадим интерфейс SystemInterface, который будет ожидаться новой системой:


public interface SystemInterface {
    void doWork();
}

Теперь создадим адаптер OldSystemAdapter, который реализует интерфейс SystemInterface и использует объект OldSystem:


public class OldSystemAdapter implements SystemInterface {
private OldSystem oldSystem;

public OldSystemAdapter(OldSystem oldSystem) {
        this.oldSystem = oldSystem;
    }

    @Override
    public void doWork() {
        oldSystem.doWorkOld();
    }
}

Далее мы можем использовать OldSystem в NewSystem, используя адаптер:

public class NewSystem {
    private SystemInterface system;

    public NewSystem(SystemInterface system) {
        this.system = system;
    }

    public void performWork() {
        system.doWork();
    }

    public static void main(String[] args) {
        OldSystem oldSystem = new OldSystem();
        OldSystemAdapter adapter = new OldSystemAdapter(oldSystem);

        NewSystem newSystem = new NewSystem(adapter);
        newSystem.performWork();
    }
}

Теперь OldSystem может быть использован в NewSystem с помощью адаптера, не изменяя его исходного кода.

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

Паттерн «Мост» (Bridge)

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

Суть паттерна

Суть паттерна «Мост» заключается в разделении абстракции (интерфейса) и реализации (конкретной реализации этого интерфейса). Паттерн представляет собой двойную иерархию классов: одна иерархия — это абстракция, а другая — реализация. Мост позволяет изменять и расширять обе иерархии независимо друг от друга.

Пример

Давайте рассмотрим пример с графической библиотекой, где у нас есть разные типы фигур (круг, квадрат) и разные типы рендерера (рисование на экране, печать). Мы хотим иметь возможность комбинировать фигуры и рендереры в любых сочетаниях.
Сначала создадим интерфейс «Рендерер» (Renderer), который определяет метод для рисования фигуры:

public interface Renderer {
    void renderCircle();
    void renderSquare();
}

Затем создадим две конкретные реализации этого интерфейса: «ЭкранРендерер» (ScreenRenderer) и «ПринтерРендерер» (PrinterRenderer):

public class ScreenRenderer implements Renderer {
    @Override
    public void renderCircle() {
        System.out.println("Отрисовка круга на экране");
    }

    @Override
    public void renderSquare() {
        System.out.println("Отрисовка квадрата на экране");
    }
}

public class PrinterRenderer implements Renderer {
    @Override
    public void renderCircle() {
        System.out.println("Печать круга");
    }

    @Override
    public void renderSquare() {
        System.out.println("Печать квадрата");
    }
}

Теперь создадим абстрактный класс «Фигура» (Shape), который будет представлять фигуры и иметь ссылку на объект «Рендерер»:

public abstract class Shape {
    protected Renderer renderer;

    public Shape(Renderer renderer) {
        this.renderer = renderer;
    }

    public abstract void draw();
}

Создадим две конкретные реализации этого класса: «Круг» (Circle) и «Квадрат» (Square), которые будут использовать реализацию рисования из объекта «Рендерер»:

public class Circle extends Shape {
    public Circle(Renderer renderer) {
        super(renderer);
    }

    @Override
    public void draw() {
        renderer.renderCircle();
    }
}

public class Square extends Shape {
    public Square(Renderer renderer) {
        super(renderer);
    }

    @Override
    public void draw() {
        renderer.renderSquare();
    }
}

Теперь мы можем создавать любые комбинации фигур и рендереров:

public class Main {
    public static void main(String[] args) {
        Renderer screenRenderer = new ScreenRenderer();
        Renderer printerRenderer = new PrinterRenderer();

        Shape circleOnScreen = new Circle(screenRenderer);
        Shape squareOnPrinter = new Square(printerRenderer);

        circleOnScreen.draw();
        squareOnPrinter.draw();
    }
}

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

Паттерн «Компоновщик» (Composite)

Паттерн «Компоновщик» — это структурный паттерн проектирования, который позволяет собирать объекты в древовидные структуры для представления часть-целое. Он позволяет клиентам одинаково работать с отдельными объектами и их композициями.

Суть паттерна

Суть паттерна «Компоновщик» заключается в создании общего интерфейса для всех компонентов (или «листьев» и «контейнеров») и предоставлении клиенту возможности работать с ними через этот интерфейс. Внутри композитов может содержаться любое количество других компонентов, включая другие композиты.

Паттерн «Компоновщик» позволяет строить древовидные структуры, где каждый узел может быть как отдельным объектом, так и композицией других объектов. Это упрощает работу с древовидными данными и обеспечивает единообразный способ доступа к элементам.

Пример

Давайте рассмотрим пример с построением иерархии графических фигур. У нас есть два типа фигур: «Круг» и «Квадрат». Мы хотим создать иерархию фигур, которая позволяет группировать их в составные фигуры.

Сначала создадим интерфейс «ГрафическаяФигура» (GraphicShape), который будет общим для всех фигур:

public interface GraphicShape {
    void draw();
}

Затем создадим конкретные реализации этого интерфейса: «Круг» (Circle) и «Квадрат» (Square):

public class Circle implements GraphicShape {
    @Override
    public void draw() {
        System.out.println("Рисуем круг");
    }
}

public class Square implements GraphicShape {
    @Override
    public void draw() {
        System.out.println("Рисуем квадрат");
    }
}

Теперь создадим композит «СоставнаяФигура» (CompositeShape), который может содержать другие фигуры, включая другие композиты:

import java.util.ArrayList;
import java.util.List;

public class CompositeShape implements GraphicShape {
    private List components = new ArrayList<>();

    public void addComponent(GraphicShape component) {
        components.add(component);
    }

    @Override
    public void draw() {
        System.out.println("Рисуем составную фигуру:");
        for (GraphicShape component : components) {
            component.draw();
        }
    }
}

Теперь мы можем создать иерархию фигур:

public class Main {
    public static void main(String[] args) {
        Circle circle = new Circle();
        Square square = new Square();

        CompositeShape composite = new CompositeShape();
        composite.addComponent(circle);
        composite.addComponent(square);

        CompositeShape biggerComposite = new CompositeShape();
        biggerComposite.addComponent(composite);
        biggerComposite.addComponent(circle);

        circle.draw();
        square.draw();
        composite.draw();
        biggerComposite.draw();
    }
}

Паттерн «Компоновщик» позволяет создавать иерархии объектов, где каждый объект может быть как отдельным элементом, так и составным объектом. Это упрощает управление сложными структурами данных и обеспечивает единообразный интерфейс для доступа к элементам, независимо от их типа.

Паттерн «Декоратор» (Decorator)

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

Суть паттерна

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

Пример

Давайте рассмотрим пример с созданием текстового редактора, где мы хотим добавить возможность форматирования текста. Например, устанавливать жирный текст, курсив и подчеркнутый текст, без изменения исходного текстового класса.

Сначала создадим интерфейс «Текст» (Text), который будет представлять базовый текст:

public interface Text {
String getContent();
}

Затем создадим конкретную реализацию этого интерфейса: «ПростойТекст» (PlainText):

public class PlainText implements Text {
private String content;

public PlainText(String content) {
this.content = content;
}

@Override
public String getContent() {
return content;
}
}

Теперь создадим абстрактный класс-декоратор «ТекстДекоратор» (TextDecorator), который также реализует интерфейс «Текст» и содержит ссылку на объект «Текст»:

public abstract class TextDecorator implements Text {
private Text text;

public TextDecorator(Text text) {
this.text = text;
}

@Override
public String getContent() {
return text.getContent();
}
}

Создадим конкретные декораторы, такие как «ЖирныйТекст» (BoldText), «КурсивТекст» (ItalicText) и «ПодчеркнутыйТекст» (UnderlineText):

public class BoldText extends TextDecorator {
public BoldText(Text text) {
super(text);
}

@Override
public String getContent() {
return "" + super.getContent() + "";
}
}

public class ItalicText extends TextDecorator {
public ItalicText(Text text) {
super(text);
}

@Override
public String getContent() {
return "" + super.getContent() + "";
}
}

public class UnderlineText extends TextDecorator {
public UnderlineText(Text text) {
super(text);
}

@Override
public String getContent() {
return "" + super.getContent() + "";
}
}

Теперь мы можем создавать и комбинировать текст с различными декораторами:

public class Main {
public static void main(String[] args) {
Text plainText = new PlainText("Простой текст");
Text boldText = new BoldText(plainText);
Text italicBoldText = new ItalicText(boldText);

System.out.println(plainText.getContent()); // Вывод: Простой текст
System.out.println(boldText.getContent()); // Вывод: Простой текст
System.out.println(italicBoldText.getContent()); // Вывод: Простой текст
}
}

Паттерн «Декоратор» позволяет добавлять и комбинировать функциональность объектов динамически, сохраняя общий интерфейс. Это удобно, когда у вас есть классы, которые должны быть расширены в разное время и разными способами без изменения их исходного кода.

Паттерн «Фасад» (Facade)

Паттерн «Фасад» — это структурный паттерн проектирования, который предоставляет унифицированный интерфейс для набора интерфейсов в подсистеме. Он упрощает использование подсистемы путем создания высокоуровневого интерфейса, скрывающего сложность и детали работы с подсистемой.

Суть паттерна

Суть паттерна «Фасад» заключается в создании класса-фасада, который предоставляет клиентам простой интерфейс для взаимодействия с сложной подсистемой. Фасад скрывает детали взаимодействия с различными компонентами подсистемы и предоставляет клиентам только те методы, которые им действительно нужны.

Паттерн «Фасад» позволяет уменьшить зависимость клиентского кода от подсистемы и упростить её использование. Если изменения произойдут внутри подсистемы, это не повлияет на клиентский код, так как клиенты взаимодействуют только через фасад.

Пример

Давайте рассмотрим пример с использованием паттерна «Фасад» в контексте работы с мультимедийным плеером. Представьте, что у вас есть сложная мультимедийная подсистема, включающая видео- и аудиоплееры, а также библиотеку для работы с мультимедийными файлами. Вместо того, чтобы клиентам напрямую взаимодействовать с этими компонентами, мы создадим фасад для управления мультимедийным воспроизведением.

Сначала создадим интерфейсы и классы для видеоплеера, аудиоплеера и библиотеки:

// Видеоплеер
public interface VideoPlayer {
    void playVideo(String videoFile);
    void stopVideo();
}

// Аудиоплеер
public interface AudioPlayer {
    void playAudio(String audioFile);
    void stopAudio();
}

// Библиотека для работы с мультимедийными файлами
public class MultimediaLibrary {
    public void loadVideo(String videoFile) {
        System.out.println("Загрузка видео: " + videoFile);
    }

    public void loadAudio(String audioFile) {
        System.out.println("Загрузка аудио: " + audioFile);
    }
}

Теперь создадим фасад «МультимедийныйПлеер» (MultimediaPlayer), который будет предоставлять простой интерфейс для работы с мультимедийным воспроизведением:

public class MultimediaPlayer {
    private VideoPlayer videoPlayer;
    private AudioPlayer audioPlayer;
    private MultimediaLibrary library;

    public MultimediaPlayer() {
        this.videoPlayer = new VideoPlayerImpl();
        this.audioPlayer = new AudioPlayerImpl();
        this.library = new MultimediaLibrary();
    }

    public void playVideo(String videoFile) {
        library.loadVideo(videoFile);
        videoPlayer.playVideo(videoFile);
    }

    public void playAudio(String audioFile) {
        library.loadAudio(audioFile);
        audioPlayer.playAudio(audioFile);
    }

    public void stopVideo() {
        videoPlayer.stopVideo();
    }

    public void stopAudio() {
        audioPlayer.stopAudio();
    }
}

Теперь клиенты могут использовать фасад «МультимедийныйПлеер» для воспроизведения мультимедийных файлов без необходимости взаимодействовать напрямую с видео-, аудиоплеерами и библиотекой:

public class Main {
    public static void main(String[] args) {
        MultimediaPlayer player = new MultimediaPlayer();

        player.playVideo("video.mp4");
        player.playAudio("audio.mp3");

        player.stopVideo();
        player.stopAudio();
    }
}

Паттерн «Фасад» упрощает взаимодействие с сложной подсистемой и скрывает детали её работы от клиентского кода. Это позволяет клиентам использовать подсистему более удобно и без необходимости знать всех её деталей.

Паттерн «Приспособленец» (Flyweight)

Паттерн «Приспособленец» (Flyweight) — это структурный паттерн проектирования, который позволяет разделять общие части объектов между объектами. Он применяется для оптимизации работы с большим числом мелких объектов, разделяя их общие данные и вынося общую часть во внешнюю структуру.

Суть паттерна

Суть паттерна «Приспособленец» заключается в разделении объектов на две части: внутреннюю состояние (intrinsic state) и внешнее состояние (extrinsic state). Внутреннее состояние — это общие данные, которые можно разделять между объектами. Внешнее состояние — это данные, специфичные для каждого объекта.

Для хранения внутреннего состояния используется разделяемая структура данных, как, например, пул объектов или хеш-таблица. Внешнее состояние передается объекту извне.

Паттерн «Приспособленец» позволяет существенно уменьшить объем используемой памяти, так как общие данные хранятся в единственном экземпляре и множество объектов ссылается на них.

Пример

Давайте рассмотрим пример с текстовым редактором, где каждая буква текста представляется как объект, и мы хотим оптимизировать память, используемую для хранения одинаковых букв.

Сначала создадим интерфейс «Буква» (Letter), представляющий букву текста:

public interface Letter {
    void print();
}

Затем создадим конкретную реализацию этого интерфейса: «КонкретнаяБуква» (ConcreteLetter), которая будет представлять собой букву и хранить её внутреннее состояние:

public class ConcreteLetter implements Letter {
    private char character;

    public ConcreteLetter(char character) {
        this.character = character;
    }

    @Override
    public void print() {
        System.out.print(character);
    }
}

Теперь создадим фабрику «ПриспособленцаБукв» (LetterFlyweightFactory), которая будет создавать и управлять объектами «КонкретнаяБуква» и обеспечивать разделение общих букв:

import java.util.HashMap;
import java.util.Map;

public class LetterFlyweightFactory {
    private Map<Character, Letter> letterCache = new HashMap<>();

    public Letter getLetter(char character) {
        if (!letterCache.containsKey(character)) {
            Letter letter = new ConcreteLetter(character);
            letterCache.put(character, letter);
        }
        return letterCache.get(character);
    }
}

Теперь мы можем использовать «ПриспособленцаБукв» для оптимизации памяти при создании текста:

public class Main {
    public static void main(String[] args) {
        LetterFlyweightFactory factory = new LetterFlyweightFactory();

        String text = "Hello, world!";
        for (char character : text.toCharArray()) {
            Letter letter = factory.getLetter(character);
            letter.print();
        }
    }
}

Паттерн позволяет оптимизировать использование памяти при работе с множеством объектов, разделяя общие данные между ними. Это особенно полезно, когда имеется большое количество объектов с схожими характеристиками.

Паттерн «Заместитель» (Proxy)

Паттерн «Заместитель» (Proxy) — это структурный паттерн проектирования, который предоставляет заместителем объекта контроль доступа к нему. Он позволяет создать промежуточный объект, который может управлять доступом к другому объекту, без необходимости изменения самого объекта.

Суть паттерна

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

Пример

Давайте рассмотрим пример с использованием паттерна «Заместитель» в контексте загрузки изображений. Предположим, что у нас есть интерфейс «Изображение» (Image), который предоставляет метод для загрузки изображения:

public interface Image {
    void display();
}

И у нас есть реализация этого интерфейса — «РеальноеИзображение» (RealImage), которая выполняет загрузку и отображение изображения:

public class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("Загрузка изображения: " + filename);
    }

    @Override
    public void display() {
        System.out.println("Отображение изображения: " + filename);
    }
}

Для оптимизации времени загрузки изображения, мы можем создать заместитель «ЗаместительИзображения» (ProxyImage). Он будет загружать и отображать изображение только при вызове метода «display»:

public class ProxyImage implements Image {
    private RealImage realImage;
    private String filename;

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

Теперь мы можем использовать заместитель для загрузки и отображения изображения:

public class Main {
    public static void main(String[] args) {
        Image image1 = new ProxyImage("image1.jpg");
        Image image2 = new ProxyImage("image2.jpg");

        // Изображение будет загружено только при вызове display()
        image1.display();

        // Изображение будет загружено только при вызове display()
        image2.display();
    }
}

Паттерн «Заместитель» позволяет управлять доступом к объекту и выполнять дополнительные действия без изменения самого объекта. Это особенно полезно в ситуациях, где объекты могут быть дорогостоящими в создании или загрузке, и вы хотите отложить этот процесс до реальной необходимости.

Техноблог
Добавить комментарий