Всем привет! В данной статье рассмотрим паттерн проектирования Composite ("Компоновщик").

Начнем немного с теории.

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

Давайте рассмотрим данный паттерн на двух примерах.

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

Вначале нам понадобится основной элемент — абстрактный класс ContentsComponent. В нем мы напишем несколько методов.

public void add(ContentsComponent contentsComponent) — в данном методе в классах наследниках мы переопределим логику добавления элементов нашего содержания.

public String getName() — этот метод мы будем использовать для получения названия главы или параграфа.

public String getDescription() — этот метод мы будем использовать для получения номера страницы.

public void print() — переопределим в классах наследника непосредственно вывод на экран глав и параграфов.

public abstract class ContentsComponent {
   
	public void add(ContentsComponent contentsComponent) {
		throw new UnsupportedOperationException();
	}
	public String getName() {
		throw new UnsupportedOperationException();
	}
	public String getDescription() {
		throw new UnsupportedOperationException();
	}
	public void print() {
		throw new UnsupportedOperationException();
	}
}

Далее напишем класс Contents и унаследуем его от ContentsComponent. Этот класс будет выполнять роль наших глав, соответственно он должен содержать список уже более мелких элементов (параграфов) — поэтому добавляем ArrayList <СontentsComponents> contentsComponents. Также добавляем поля name и description, которые будут отвечать за название главы и номера страницы.

Переопределим метод public void print() — он будет выводить название главы и страницы, а также перебирать все элементы из contentsComponents и вызывать у них свой метод print().

public class Contents extends ContentsComponent {
	ArrayList<ContentsComponent> contentsComponents = new ArrayList<ContentsComponent>();
	String name;
	String description;

	public Contents(String name, String description) {
		this.name = name;
		this.description = description;
	}
 
	public void add(ContentsComponent contentsComponent) {
		contentsComponents.add(contentsComponent);
	}

	public String getName() {
		return name;
	}
 
	public String getDescription() {
		return description;
	}
 
	public void print() {
		System.out.print("\n" + getName());
		for (int i = 0; i < 70-getName().length(); i++) {
			System.out.print(".");
		}
		System.out.println(getDescription());
  
		Iterator<ContentsComponent> iterator = contentsComponents.iterator();
		while (iterator.hasNext()) {
			ContentsComponent contentsComponent =
				(ContentsComponent)iterator.next();
			contentsComponent.print();
		}
	}
}

Пришло время написать еще один класс — ContentsItem. Этот класс также унаследуем от ContentsComponent и он будет отвечать за наши параграфы. В данном классе также есть поля name и description (название параграфа и номер страницы), но уже нету списка новых элементов, так как он будет у нас как лист на дереве — не будет содержать в себе больше элементов.

Также для него переопределим метод public void print() — где выведем название параграфа и номер страницы.

public class ContentsItem extends ContentsComponent {
	String name;
	String description;
	public ContentsItem(String name,
                        String description)
	{ 
		this.name = name;
		this.description = description;
	}
  
	public String getName() {
		return name;
	}
  
	public String getDescription() {
		return description;
	}

  
	public void print() {
		System.out.print("  " + getName());
		for (int i = 0; i < 68-getName().length(); i++) {
			System.out.print(".");
		}
		System.out.println(getDescription());
	}
}

Напишем еще один класс Book, который будет содержать ContentsComponent contents, то есть наше содержание и напишем метод public void printContents(), который будет выводить содержание на печать.

public class Book {
	ContentsComponent contents;

	public Book(ContentsComponent contents) {
		this.contents = contents;
	}
 
	public void printContents() {
		contents.print();
	}
}

Осталось протестировать весь наш код, напишем класс TestDrive, в котором создадим содержание для книги.

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

public class TestDrive {
    public static void main(String[] args) {
        ContentsComponent contents = new Contents("Contents ", "1");
      
        ContentsComponent introduction = new Contents("1. Introduction ", "1");
        ContentsComponent creatingAndDestroyingObject = new Contents("2. Creating and destroying object ", "5");
        ContentsComponent methodsCommonToAllObject = new Contents("3. Methods common to all object ", "37");
        ContentsComponent classesAndInterfaces = new Contents("4. Classes and interfaces ", "73");

        contents.add(introduction);
        contents.add(creatingAndDestroyingObject);
        contents.add(methodsCommonToAllObject);
        contents.add(classesAndInterfaces);

        creatingAndDestroyingObject.add(new ContentsItem(
                "2.1 Consider static factory methods instead of constructors ",
                "5"
        ));
        creatingAndDestroyingObject.add(new ContentsItem(
                "2.2 Consider a builder when faced with many constructor parameters ",
                "10"
        ));
        creatingAndDestroyingObject.add(new ContentsItem(
                "2.3 Enforce the singleton property with a private constructor ",
                "17"
        ));

        methodsCommonToAllObject.add(new ContentsItem(
                "3.1 Obey the general contract when overriding equals ",
                "37"
        ));

        methodsCommonToAllObject.add(new ContentsItem(
                "3.2 Always override hashCode when you override equals ",
                "50"
        ));

        classesAndInterfaces.add(new ContentsItem(
                "4.1 Minimize the accessibility of classes and members ",
                "73"
        ));
        classesAndInterfaces.add(new ContentsItem(
                "4.2 in public classes, use accessor methods, not public fields ",
                "78"
        ));
        classesAndInterfaces.add(new ContentsItem(
                "4.3 Minimize mutability ",
                "80"
        ));

        System.out.println("\"Effective Java\" Joshua Bloch, 2018");

        Book book = new Book(contents);

        book.printContents();

    }

Вот такой должен получиться вывод на экран.

Пример второй. Допустим у нас есть такая задача как распечатать текст, но печатать можно только символы (сами буквы и знаки препинания). То есть предварительно надо текст поделить на абзацы, потом абзацы — на предложения, предложения на — слова, и слова — на символы и уже символы выводить на экран.

Давайте приступим. Вначале, как и в предыдущем примере, создадим основной абстрактный класс TextComponent. Он будет содержать три метода:

public void add(TextComponent textComponent) — где мы в классах наследниках переопределим добавление новых элементов, строя тем самым древовидную структуру.

public String getText() — получение текста.

public void print() — вывод на экран.

public abstract class TextComponent {
   
	public void add(TextComponent textComponent) {
		throw new UnsupportedOperationException();
	}
	public String getText() {
		throw new UnsupportedOperationException();
	}
  
	public void print() {
		throw new UnsupportedOperationException();
	}
}

Начнем с класса Text — самого главного класса. В данном классе мы создадим список абзацев ArrayList <TextComponent>paragraphs, потому что текст состоит из абзацев. Напишем метод public void setItemComponents() — в нем текст будем делить на абзацы и добавлять в список абзацев. Также переопределим метод public void print() — здесь тоже все просто вызываем метод setItemComponents() — делим текст на абзацы и потом для каждого элемента (то есть абзаца) вызываем метод print().

public class Text extends TextComponent{
    ArrayList<TextComponent> paragraphs = new ArrayList<TextComponent>();
    String text;

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

    public void add(TextComponent paragraph) {
        paragraphs.add(paragraph);
    }

    public String getText() {
        return text;
    }

    public void setItemComponents() {
        String[] result = text.split("\n");
        for (int i = 0; i < result.length; i++) {
            paragraphs.add(new Paragraph(result[i]));
        }
    }

    public void print() {
        setItemComponents();
        Iterator<TextComponent> iterator = paragraphs.iterator();
        while (iterator.hasNext()) {
            TextComponent textComponent =
                    (TextComponent) iterator.next();
            textComponent.print();
        }
    }

}

Следующий класс Paragraph — абзац. Он очень похож на предыдущий класс. Здесь у нас уже есть список предложений ArrayList<TextComponent>sentences, а также другая логика разделения каждого абзаца на предложения (метод public void setItemComponents()). Также как и в предыдущем классе метод public void print() делит текст на предложения и потом для каждого элемента (то есть предложения) вызываем метод print().

public class Paragraph extends TextComponent{
    ArrayList<TextComponent> sentences = new ArrayList<TextComponent>();
    String paragraph;

    public Paragraph(String paragraph) {
        this.paragraph = paragraph;
    }

    public void add(TextComponent paragraph) {
        sentences.add(paragraph);
    }

    public String getText() {
        return paragraph;
    }

    public void setItemComponents() {
        String[] result = paragraph.split("\\.\\\\!\\?");
        for (int i = 0; i < result.length; i++) {
            sentences.add(new Sentence(result[i]));
        }
    }

    public void print() {
        setItemComponents();
        Iterator<TextComponent> iterator = sentences.iterator();
        while (iterator.hasNext()) {
            TextComponent textComponent =
                    (TextComponent) iterator.next();
            textComponent.print();
        }
        System.out.print("\n");
    }

}

Следующие два класса Sentence и World комментировать не буду — они аналогичны предыдущим.

public class Sentence extends TextComponent{
    ArrayList<TextComponent> worlds = new ArrayList<TextComponent>();
    String sentence;

    public Sentence(String sentence) {
        this.sentence = sentence;
    }

    public void add(TextComponent paragraph) {
        worlds.add(paragraph);
    }

    public String getText() {
        return sentence;
    }

    public void setItemComponents() {
        String[] result = sentence.split(" ");
        for (int i = 0; i < result.length; i++) {
            worlds.add(new World(result[i]));
        }
    }

    public void print() {
        setItemComponents();
        Iterator<TextComponent> iterator = worlds.iterator();
        while (iterator.hasNext()) {
            TextComponent textComponent =
                    (TextComponent) iterator.next();
            textComponent.print();

        }
    }
}
public class World extends TextComponent{
    ArrayList<TextComponent> symbols = new ArrayList<TextComponent>();
    String symbol;

    public World(String symbol) {
        this.symbol = symbol;
    }

    public void add(TextComponent paragraph) {
        symbols.add(paragraph);
    }

    public String getText() {
        return symbol;
    }

    public void setItemComponents() {
        String[] result = symbol.split("");
        for (int i = 0; i < result.length; i++) {
            symbols.add(new Symbol(result[i]));
        }
    }

    public void print() {
        setItemComponents();
        Iterator<TextComponent> iterator = symbols.iterator();
        while (iterator.hasNext()) {
            TextComponent textComponent =
                    (TextComponent) iterator.next();
            textComponent.print();
        }
        System.out.print(" ");
    }
}

И наконец последний класс Symbol (символ) — самый маленький элемент. В нем в методе public void print() мы наконец выводим на экран символ.

public class Symbol extends TextComponent{
    String item;

    public Symbol(String item) {
        this.item = item;
    }
    public String getText() {
        return item;
    }

    public void print() {
        System.out.print(getText()) ;
    }
}

Протестируем наш весь код. Напишем класс TestDriveSecond, в котором создадим рандомный текст и вызовем метод print() у объекта класса Text.

public class TestDriveSecond {
    public static void main(String[] args) {
        final String textForPrint =
                        "Occaecat irure commodo labore ullamco est minim aute laborum sint officia consequat cupidatat dolor reprehenderit. Nulla magna amet aliqua incididunt laborum esse tempor aliqua ex esse veniam. Proident tempor voluptate voluptate velit magna et eu aute sint excepteur quis labore in. Sint consectetur sint aliquip minim elit quis elit mollit voluptate sit aute cupidatat.\n" +
                                "Anim eu Lorem aliqua elit enim amet dolore tempor irure ullamco dolore ad fugiat. Deserunt commodo aliquip cillum exercitation exercitation ex velit voluptate mollit sit aute. Fugiat in officia amet est dolore sunt.\n" +
                                "Pariatur amet qui esse fugiat veniam occaecat Lorem exercitation consectetur qui. Magna tempor exercitation laborum sunt minim magna eu mollit laborum ut do sit. Excepteur sunt ullamco nisi officia non labore est magna aute excepteur cillum. Fugiat quis reprehenderit quis quis aliquip consequat ut cillum culpa. Magna pariatur incididunt nisi nostrud esse amet proident. Reprehenderit ad ullamco deserunt amet amet laboris.\n" +
                                "Mollit adipisicing laboris laboris tempor in irure aliquip ullamco laborum cillum sit. Magna id proident commodo culpa magna dolor reprehenderit ad minim. Enim ad est eu fugiat id. Aute id sit aliquip sunt ullamco in magna est consequat culpa magna.";
        Text text = new Text(textForPrint);
        text.print();
    }
}

В результате должны получить обычный вывод на экран.

Вот мы и рассмотрели на двух примерах паттерн проектирования Composite.

Спасибо всем, кто дочитал до конца. Всем пока!

Комментарии (7)