В этой серии мы рассмотрим шаблоны структурного проектирования:

  1. Композитный
  2. Адаптер
  3. Мост
  4. Декоратор
  5. Фасад
  6. Наилегчайший вес
  7. Прокси

🤓 Что это?

Композитный – это структурный шаблон проектирования, который позволяет объединять объекты в древовидные структуры, а затем работать с этими структурами, как если бы они были отдельными объектами.

TL;DR: шаблон Composite позволяет клиентам одинаково обрабатывать отдельные объекты и композиции объектов.

🥰 Почему..

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

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

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

На примерах станет понятнее ❤️

🥰 Структура

Составной шаблон состоит из трех основных типов объектов:

  1. Компонент
    Компонент объявляет интерфейс для объектов в композиции и при необходимости реализует поведение по умолчанию для интерфейса, общего для всех классов. Класс базового компонента может объявить интерфейс для доступа к своим дочерним компонентам и управления ими.
  2. Лист
    Лист представляет конечные объекты композиции. Лист не имеет потомков; он определяет поведение примитивных объектов в композиции.
  3. Composite
    Composite определяет поведение компонентов, имеющих дочерние элементы; он хранит дочерние компоненты и реализует операции, связанные с дочерними компонентами, в интерфейсе компонентов.

😎 Примеры:

🚙 Пример 1

Давайте рассмотрим пример файловой системы, в которой у нас есть файлы и каталоги. Каталоги могут содержать файлы, а также другие каталоги. Мы можем реализовать Composite Design Pattern для представления этой иерархии файловой системы.

Здесь составной шаблон говорит о том, что мы можем работать с файлами и каталогами через общий интерфейс.

interface FileSystemComponent {
  getName(): string;
  getSize(): number;
  addChild(child: FileSystemComponent): void;
}

class File implements FileSystemComponent {
  private name: string;
  private size: number;
  constructor(name: string, size: number) {
    this.name = name;
    this.size = size;
  }
  getName() {
    return this.name;
  }
  getSize() {
    return this.size;
  }
  addChild(child: FileSystemComponent) {
    throw new Error('Cannot add child to a file');
  }
}
class Directory implements FileSystemComponent {
  private name: string;
  private children: FileSystemComponent[] = [];
  constructor(name: string) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
  getSize() {
    let size = 0;
    for (const child of this.children) {
      size += child.getSize();
    }
    return size;
  }
  addChild(child: FileSystemComponent) {
    this.children.push(child);
  }
}

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

function getTotalSize(component: FileSystemComponent): number {
  let size = component.getSize();
  if (component instanceof Directory) {
    for (const child of component.children) {
      size += getTotalSize(child);
    }
  }
  return size;
}


// Client
const root = new Directory('root');
const folder1 = new Directory('folder1');
const folder2 = new Directory('folder2');
root.addChild(folder1);
root.addChild(folder2);
folder1.addChild(new File('file1', 100));
folder1.addChild(new File('file2', 200));
folder2.addChild(new File('file3', 50));
console.log(getTotalSize(root));

👨🏻‍💻 Пример 2:

Представьте, что вы создаете интернет-магазин. Магазин продает товары и услуги. Каждый продукт или услуга может продаваться по отдельности или в составе пакета. Например, клиент может купить книгу, музыкальный компакт-диск и видео-DVD в комплекте. Покупатель также может приобрести каждый из этих предметов по отдельности.

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

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

// Component
public abstract class Product {
    public abstract double getPrice();
}
// Leaf
public class Book extends Product {
    private String title;
    private double price;
    public Book(String title, double price) {
        this.title = title;
        this.price = price;
    }
    public double getPrice() {
        return price;
    }
}
// Leaf
public class MusicCD extends Product {
    private String title;
    private double price;
    public MusicCD(String title, double price) {
        this.title = title;
        this.price = price;
    }
    public double getPrice() {
        return price;
    }
}
// Leaf
public class VideoDVD extends Product {
    private String title;
    private double price;
    public VideoDVD(String title, double price) {
        this.title = title;
        this.price = price;
    }
    public double getPrice() {
        return price;
    }
}
// Composite
public class Bundle extends Product {
    private String name;
    private List<Product> products = new ArrayList<>();
    public Bundle(String name, Product... products) {
        this.name = name;
        for (Product product : products) {
            this.products.add(product);
        }
    }
    public void addProduct(Product product) {
        products.add(product);
    }
    public void removeProduct(Product product) {
        products.remove(product);
    }
    public double getPrice() {
        double totalPrice = 0;
        for (Product product : products) {
            totalPrice += product.getPrice();
        }
        return totalPrice;
    }
}

Теперь мы для клиентского кода

// Client code
public class OnlineStore {
    public static void main(String[] args) {
        Book book = new Book("The Lord of the Rings", 20.0);
        MusicCD musicCD = new MusicCD("Abbey Road", 15.0);
        VideoDVD videoDVD = new VideoDVD("The Godfather", 25.0);
        Bundle bundle = new Bundle("Entertainment Bundle", book, musicCD, videoDVD);
        System.out.println("Total price: " + bundle.getPrice());
    }
}

🧐 Когда использовать?

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

✅ Преимущества

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

❌ Недостатки

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

🚀 Заключение

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

Дополнительные пояснения и примеры кода можно найти в моем репозитории:



Надеюсь, эта статья была для вас интересной и веселой ❤️