«1. Введение

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

Мы также обсудим некоторые из его преимуществ и недостатков.

2. Шаблон прототипа

Шаблон прототипа обычно используется, когда у нас есть экземпляр класса (прототип) и мы хотим создать новые объекты, просто скопировав прототип.

Давайте воспользуемся аналогией, чтобы лучше понять этот шаблон.

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

Итак, сначала мы создаем экземпляр дерева. Затем мы можем создать из этого экземпляра (прототипа) столько деревьев, сколько захотим, и обновить их позиции. Мы также можем изменить цвет деревьев для нового уровня в игре.

Шаблон Prototype очень похож. Вместо того, чтобы создавать новые объекты, нам просто нужно клонировать прототип.

3. Диаграмма UML

На диаграмме мы видим, что клиент говорит прототипу клонировать себя и создать объект. Prototype представляет собой интерфейс и объявляет метод клонирования самого себя. ConcretePrototype1 и ConcretePrototype2 реализуют операцию клонирования самих себя.

4. Реализация

Одним из способов реализации этого шаблона в Java является использование метода clone(). Для этого мы реализуем интерфейс Cloneable.

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

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

Если он содержит ссылки на изменяемые поля, мы должны сделать глубокую копию. Мы могли бы сделать это с помощью конструкторов копирования или сериализации и десериализации.

Давайте возьмем пример, который мы упоминали ранее, и перейдем к тому, как применить шаблон Prototype без использования интерфейса Cloneable. Для этого создадим абстрактный класс Tree с абстрактным методом «copy».

public abstract class Tree {
    
    // ...
    public abstract Tree copy();
    
}

Теперь предположим, что у нас есть две разные реализации Tree с именами PlasticTree и PineTree:

public class PlasticTree extends Tree {

    // ...

    @Override
    public Tree copy() {
        PlasticTree plasticTreeClone = new PlasticTree(this.getMass(), this.getHeight());
        plasticTreeClone.setPosition(this.getPosition());
        return plasticTreeClone;
    }

}
public class PineTree extends Tree {
    // ...

    @Override
    public Tree copy() {
        PineTree pineTreeClone = new PineTree(this.getMass(), this.getHeight());
        pineTreeClone.setPosition(this.getPosition());
        return pineTreeClone;
    }
}

Итак, здесь мы видим, что классы, которые расширяют Tree и реализуют метод копирования, могут действовать как прототипы для создание копии самого себя.

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

5. Тестирование

public class TreePrototypesUnitTest {

    @Test
    public void givenAPlasticTreePrototypeWhenClonedThenCreateA_Clone() {
        // ...

        PlasticTree plasticTree = new PlasticTree(mass, height);
        plasticTree.setPosition(position);
        PlasticTree anotherPlasticTree = (PlasticTree) plasticTree.copy();
        anotherPlasticTree.setPosition(otherPosition);

        assertEquals(position, plasticTree.getPosition());
        assertEquals(otherPosition, anotherPlasticTree.getPosition());
    }
}

Теперь давайте проверим это:

Мы видим, что дерево было клонировано из прототипа, и у нас есть два разных экземпляра PlasticTree. Мы только что обновили позицию в клоне и сохранили остальные значения.

@Test
public void givenA_ListOfTreesWhenClonedThenCreateListOfClones() {

    // create instances of PlasticTree and PineTree

    List<Tree> trees = Arrays.asList(plasticTree, pineTree);
    List<Tree> treeClones = trees.stream().map(Tree::copy).collect(toList());

    // ...

    assertEquals(height, plasticTreeClone.getHeight());
    assertEquals(position, plasticTreeClone.getPosition());
}

Теперь давайте клонируем список деревьев:

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

6. Преимущества и недостатки

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

Иногда мы можем встретить подклассы, которые отличаются только своим состоянием. Мы можем устранить эти подклассы, создав прототипы с начальным состоянием, а затем клонировав их.

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

7. Заключение

«В этом руководстве мы изучили ключевые концепции шаблона Prototype и увидели, как реализовать его на Java. Мы также обсудили некоторые из его плюсов и минусов.