Привет, Хабр!

Сегодня в статье рассмотрим такие паттерны как Object Mothet и Object Pool, двух мощных инструментов в Java. Эти паттерны упрощают управление объектами и повышают эффективность работы приложений.

Object Mother

Основная идея заключается в том, чтобы вместо того чтобы в каждом тесте вручную создавать сложные объекты, Object Mother предлагает классы или методы, которые инкапсулируют логику создания этих объектов. Так можно повторно использовать код, тем самым упрощая тестирование.

Например, нужно тестить систему управления юзерами, можно иметь класс UserMother, который предоставляет различные версии объектов User для тестов:

public class UserMother {
    public static User createDefaultUser() {
        return new User("ivan", "ivan@example.com", "qwerty");
    }

    public static User createUserWithCustomDetails(String name, String email, String password) {
        return new User(name, email, password);
    }
}

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

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

Часто Object Mothet комбинируют с паттерном Builder. Так можно создавать тестовые данные с высокой гибкостью. Подход хорош для создания разнообразных конфигов объектов в тестах, поддерживая при этом принцип единственной ответственности.

Допустим, есть базовый класс Invoice, который хочется использовать в различных тестах с разными конфигурациями:

public class Invoice {
    private final String customerName;
    private final BigDecimal amount;
    private final LocalDate date;

    private Invoice(String customerName, BigDecimal amount, LocalDate date) {
        this.customerName = customerName;
        this.amount = amount;
        this.date = date;
    }

    public static class Builder {
        private String customerName;
        private BigDecimal amount;
        private LocalDate date;

        public Builder withCustomerName(String customerName) {
            this.customerName = customerName;
            return this;
        }

        public Builder withAmount(BigDecimal amount) {
            this.amount = amount;
            return this;
        }

        public Builder withDate(LocalDate date) {
            this.date = date;
            return this;
        }

        public Invoice build() {
            return new Invoice(customerName, amount, date);
        }
    }
}

Теперь с паттерном Object Mother создаем предварительно сконфигурированные версии объектов Invoice для использования в тестах:

public class InvoiceMother {
    public static Invoice.Builder standardInvoice() {
        return new Invoice.Builder()
            .withCustomerName("Standard Co.")
            .withAmount(new BigDecimal("100.00"))
            .withDate(LocalDate.now());
    }

    public static Invoice.Builder discountInvoice() {
        return standardInvoice()
            .withAmount(new BigDecimal("50.00")); // применяем скидку
    }
}

Можно использовать "матери" для создания инвойсов с разными параметрами, добавляя специфичные детали при необходимости:

public class InvoiceTest {
    @Test
    public void testInvoiceWithCustomDate() {
        Invoice invoice = InvoiceMother.standardInvoice()
            .withDate(LocalDate.of(2022, 1, 1))
            .build();
        // выполнить проверки
    }
}

Object Pool

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

Для создания потокобезопасного пула объектов можно использовать java.util.concurrent, который предлагает необходимые классы и интерфейсы. Один из подходов — использование интерфейса BlockingQueue для управления пулом объектов:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ObjectPool<T> {
    private BlockingQueue<T> pool;

    public ObjectPool(int size, ObjectFactory<T> factory) {
        pool = new LinkedBlockingQueue<>(size);
        for (int i = 0; i < size; i++) {
            pool.offer(factory.createObject());
        }
    }

    public T acquire() throws InterruptedException {
        return pool.take();
    }

    public void release(T object) {
        pool.offer(object);
    }
}

ObjectPool использует LinkedBlockingQueue для хранения пула объектов. Методы acquire() и release() используются для получения и возврата объектов пула.

Когда объекты в пуле должны удовлетворять определённым условиям (например, время жизни объекта или проверка его состояния), можно создать абстрактный класс с доп. методами:

import java.util.Enumeration;
import java.util.Hashtable;

public abstract class ReusablePool<T> {
    private Hashtable<T, Long> locked, unlocked;

    protected abstract T createObject();

    public synchronized T acquire() {
        long now = System.currentTimeMillis();
        if (!unlocked.isEmpty()) {
            Enumeration<T> e = unlocked.keys();
            while (e.hasMoreElements()) {
                T t = e.nextElement();
                if ((now - unlocked.get(t)) > expirationTime) {
                    unlocked.remove(t);
                    expired(t);
                    t = createObject();
                }
                if (validate(t)) {
                    unlocked.remove(t);
                    locked.put(t, now);
                    return t;
                }
            }
        }
        T t = createObject();
        locked.put(t, now);
        return t;
    }

    public synchronized void release(T t) {
        locked.remove(t);
        unlocked.put(t, System.currentTimeMillis());
    }

    protected abstract boolean validate(T o);
    protected abstract void expired(T o);
}

В классе юзаются методы validate() для проверки состояния объекта и expired() для обработки истекших объектов.

Иногда эти паттерны можно объединять

Представим, что есть система управления счетами, и нам нужно тестировать разные состояния объектов Invoice. Object Mother будем юзать для создания предварительно настроенных счетов и Object Pool для управления их повторным использованием.

Object Mother:

public class InvoiceMother {
    public static Invoice.Builder standardInvoice() {
        return new Invoice.Builder()
            .withNumber("INV-001")
            .withCustomer("Ivan")
            .withAmount(new BigDecimal("100.00"))
            .withDate(LocalDate.now());
    }
}

Object Pool:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ObjectPool<T> {
    private final BlockingQueue<T> pool;

    public ObjectPool(int size, Supplier<T> creator) {
        pool = new LinkedBlockingQueue<>(size);
        for (int i = 0; i < size; i++) {
            pool.offer(creator.get());
        }
    }

    public T acquire() throws InterruptedException {
        return pool.take();
    }

    public void release(T obj) {
        pool.offer(obj);
    }
}

Objecct Mother + Object Pool:

public class InvoiceTest {
    private static final ObjectPool<Invoice> invoicePool = new ObjectPool<>(10, 
        () -> InvoiceMother.standardInvoice().build());

    @Test
    public void testInvoiceProcessing() throws InterruptedException {
        Invoice invoice = invoicePool.acquire();
        try {
            // тест с использованием объекта invoice
            assertEquals("INV-001", invoice.getNumber());
            assertEquals(new BigDecimal("100.00"), invoice.getAmount());
        } finally {
            invoicePool.release(invoice);
        }
    }
}

InvoiceMother используется для создания стандартных счетов, а ObjectPool управляет их повторным использованием.


Записывайтесь на открытые уроки в рамках курса Java для начинающих:

  • 5 июня. Введение в Stream API: посмотрим, как изменилась Java, начиная с 8-й версии. На практике создадим программы на языке Java и интерпретируем базовый вариант решения задач, но уже с применением Stream API. Записаться

  • 18 июня. Сборка приложения на Java: рассмотрим, как запустить собрать исполняемый jar-файл, добавить ресурсы в него и запустить java-приложение. Записаться

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


  1. MadL1me
    02.06.2024 14:03
    +1

    Жаль, что у меня не хватает кармы минусовать статьи.


  1. verls
    02.06.2024 14:03
    +2

    Потрачено. Человек не в курсе про Design Patterns банды четырех. Прежде чем писать статьи просьба проводить хоть минимальное исследование, что теме уже 20 лет в обед и куча книг и докладов еще десятки лет назад были доступны. RTFM короче :)