Pull to refresh

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

Reading time 5 min
Views 43K
Почитать описание других паттернов.

Проблема


Предоставить клиенту единообразный доступ к листовым и составным элементам древовидной структуры.

Описание


Существует большое количество программных систем, в которых так или иначе применяются древовидные структуры объектов. В большинстве случаев, это всевозможные конструкторы/редакторы, которые позволяют собрать что-то большое (составное) из чего-то более мелкого (листового). При этом, клиент трактует и большое и мелкое как одно и тоже, а система должна различать составные и листовые объекты соответственно.

Не только программы-конструкторы используют данный паттерн (а я надеюсь, что используют). Ярким примером подобной древовидной структуры является пользовательский интерфейс (GUI). Действительно, типичное окно пользовательского интерфейса — контейнер для более простых виджетов — панелей, кнопок, полей воода и т.д., причем, панель, в свою очередь тоже являются контейнерными объектами и так вплоть до элементарных листовых объектов. Ярким примером использования данного паттерна в этом контексте является библиотека отрисовки интерфейса Swing (я имею ввиду класс javax.swing.JComponent).

Итак, вернемся к поставленной проблеме — предоставить клиенту единообразный интерфейс к листовым и составным элементам древовидной структуры. Очевидно, что для решения данной проблемы необходимо завести общий интерфейс, который будет описывать и элементарные и составные объекты. Более того, т.к. интерфейс описывает и составные объекты, он должен содержать контейнерные методы — add, remove, get для добавления, удаления и получения объекта из контейнера. Причем, данные методы должны быть параметризованы тем самым общим интерфейсом. Таким образом, автоматически появляется возможность добавлять в контейнер не только элементарные объекты но и другие контейнеры.

Все объекты древовидной структуры (листовые и составные) должны реализовывать этот единообразный интерфейс, причем составные объекты переопределяют операции add, remove, get а листовые их попросту игнорируют.

Практическая задача


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

Я специально не стал брать классические примеры с конструкторами или пользовательским интерфейсом, чтобы у читателя не осталось впечатлений об узкой направленности паттерна.

Диаграмма классов


Для начала несколько комментариев. В данном примере я несколько иначе трактую понятие контейнера. Скажем так, я спроецировал классическое понятие контейнера на предметную область — вычисление выражений. Мой «контейнер» ведет себя несколько иначе, чем классический. Вместо метода remove(), у SubExpression есть метод sub(), который на самом деле и делает удаление из контейнера, но только по-своему. Ввиду того, что это все-таки сумматор, метод sub() аналогично add() добавляет подвыражение в контейнер, но с противоположным знаком, тем самым реализуя вычитание.



Рассмотрим диаграмму. Интерфейс подвыражения (SubExpression) описывает единообразный интерфейс для всех объектов древовидной структуры, которых, к слову сказать, не много — целые числа (IntegetValue), вещественные числа (FloatValue) и выражения (Expression). Очевидно, что все числа — листовые объекты и контейнерных методов они не реализуют, а выражение как раз наоборот — контейнер.

Реализация на Java


В реализации нет кода класса FloatValue.
// Единообразный интерфейс доступа к листовым и контейнерным объектам
public interface SubExpression {
  
  public Number value();
  
  public void add(SubExpression expr);
  public void sub(SubExpression expr);
  public SubExpression getSubExpression(int index);
}

// Лист - целое число
public class IntegerValue implements SubExpression {
  
  private Integer value;
  
  public IntegerValue(Integer value) {
    this.value = value;
  }

  @Override
  public void add(SubExpression expr) {
    throw new UnsupportedOperationException();    
  }

  @Override
  public SubExpression getSubExpression(int index) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sub(SubExpression expr) {
    throw new UnsupportedOperationException();    
  }

  @Override
  public Number value() {
    return value;
  }
}

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

// Выражение - контейнер
public class Expression implements SubExpression {
  
  private List<SubExpression> exprs;
  
  public Expression(SubExpression ... exprs) {
    this.exprs = new ArrayList<SubExpression>();
    for (SubExpression expr: exprs) {
      this.exprs.add(expr);
    }
  }
  
  @Override
  public void add(SubExpression expr) {
    exprs.add(expr);    
  }
  
  @Override
  public void sub(SubExpression expr) {
    if (expr instanceof IntegerValue) {
      exprs.add(new IntegerValue(-1*expr.value().intValue()));
    } else {
      exprs.add(new FloatValue(-1*expr.value().floatValue()));
    }
    
  }

  @Override
  public SubExpression getSubExpression(int index) {
    return exprs.get(index);
  }

  @Override
  public Number value() {
    Number result = new Float(0);
    
    for (SubExpression expr: exprs) {
      result = result.floatValue() + expr.value().floatValue();
    }
    
    return result;
  }
}

// Использование
public class Main {

  public static void main(String[] args) {
    // Вычислим выражение - 20 - (5-2) - (11+6)
    // Приведем к следующему виду 20 - a - b
    SubExpression expr = new Expression();

    SubExpression a = new Expression(new IntegerValue(5), new IntegerValue(-2));
    SubExpression b = new Expression(new IntegerValue(11), new IntegerValue(6));
    
    expr.add(new IntegerValue(20));
    expr.sub(a);
    expr.sub(b);
    
    System.out.println(expr.value());
  }
}

* This source code was highlighted with Source Code Highlighter.


Надеюсь, у меня получилось донести до Вас идею паттерна.

Tags:
Hubs:
+27
Comments 32
Comments Comments 32

Articles