Хранение и загрузка конфигурации для REST-сервера без глобального состояния (т. е. синглтон, контекст или внедрение зависимостей).

Я разрабатываю архитектуру на Java с использованием tomcat, и я столкнулся с ситуацией, которая, по моему мнению, является очень общей, и все же, прочитав несколько вопросов/ответов в StackOverflow, я не смог найти окончательного ответа. В моей архитектуре есть REST API (работающий на tomcat), который получает один или несколько файлов и связанные с ними метаданные и записывает их в хранилище. Конфигурация уровня хранилища имеет отношение 1-1 к серверу REST API, и по этой причине интуитивно понятный подход заключается в написании синглтона для хранения этой конфигурации.

Очевидно, я знаю, что синглтоны приносят проблемы с тестируемостью из-за глобального состояния и трудностей с насмешками над синглтонами. Я также думал об использовании шаблона контекста, но я не уверен, что шаблон контекста применим в этом случае, и я беспокоюсь, что вместо этого я закончу кодирование с использованием «контекстного анти-шаблона».

Позвольте мне дать вам еще немного предыстории того, что я пишу. Архитектура состоит из следующих компонентов:

  • Клиенты, которые отправляют запросы к REST API, загружая или извлекая «объекты сохранения», или, проще говоря, PO (файлы + метаданные) в формате JSON или XML.

  • Высокоуровневый REST API, который получает запросы от клиентов и сохраняет данные на уровне хранилища.

  • Уровень хранения, который может содержать комбинацию контейнеров OpenStack Swift, ленточных библиотек и файловых систем. Каждый из этих «контейнеров хранения» (для простоты я называю контейнерами файловых систем) в моей архитектуре называется конечной точкой. Очевидно, что уровень хранилища не находится на том же сервере, где находится REST API.

Конфигурация конечных точек выполняется через REST API (например, POST /configEndpoint), поэтому пользователь с правами администратора может регистрировать новые конечные точки, редактировать или удалять существующие конечные точки с помощью HTTP-вызовов. Хотя я реализовал архитектуру только с использованием конечной точки OpenStack Swift, я ожидаю, что информация для каждой конечной точки будет содержать как минимум IP-адрес, какую-либо информацию для аутентификации и имя драйвера, например. «драйвер Swift», «драйвер LTFS» и т. д. (чтобы, когда появятся новые технологии хранения, их можно было легко интегрировать в мою архитектуру, если кто-то напишет для нее драйвер).

Моя проблема: как хранить и загружать конфигурацию тестируемым, повторно используемым и элегантным способом? Я даже не буду рассматривать передачу объекта конфигурации всем различным методам, реализующим вызовы REST API.

Несколько примеров вызовов REST API и где в игру вступает конфигурация:

// Retrieve a preservation object metadata (PO)
@GET
@Path("container/{containername}/{po}")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public PreservationObjectInformation getPOMetadata(@PathParam("containername") String containerName, @PathParam("po") String poUUID) {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName);
    // Configuration.getInstance(containerName);
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - RETRIEVE THE METADATA FROM THE STORAGE
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)
    // Pass poUUID as parameter

    // STEP 3 - CONVERT JSON/XML TO OBJECT
    // Unmarshall the file in JSON format
    PreservationObjectInformation poi = unmarshall(data);

    return poi;
}


// Delete a PO
@DELETE
@Path("container/{containername}/{po}")
public Response deletePO(@PathParam("containername") String containerName, @PathParam("po") String poName) throws IOException, URISyntaxException {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName); // Context
    // Configuration.getInstance(containerName); // Singleton
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - CONNECT TO THE STORAGE ENDPOINT
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)

    // STEP 3 - DELETE THE FILE

    return Response.ok().build();
}


// Submit a PO and its metadata
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Path("container/{containername}/{po}")
public Response submitPO(@PathParam("containername") String container, @PathParam("po") String poName, @FormDataParam("objectName") String objectName,
        @FormDataParam("inputstream") InputStream inputStream) throws IOException, URISyntaxException {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName);
    // Configuration.getInstance(containerName);
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - WRITE THE DATA AND METADATA TO STORAGE
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)

    return Response.created(new URI("container/" + container + "/" + poName))
            .build();
}

** ОБНОВЛЕНИЕ № 1. Моя реализация основана на комментарии @mawalker **

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

public interface StorageContainerStrategy {
    public void write();
    public void read();

    // other methods here
}

public class Context {
    public StorageContainerStrategy strategy;

    // other context information here...
}

public class StrategyFactory {
    public static StorageContainerStrategy createStorageContainerStrategy(Container c) {
        if(c.getEndpoint().isSwift())
            return new SwiftStrategy();
        else if(c.getEndpoint().isLtfs())
            return new LtfsStrategy();
        // etc.
        return null;
    }
}

public class SwiftStrategy implements StorageContainerStrategy {
    @Override
    public void write() {
        // OpenStack Swift specific code
    }

    @Override
    public void read() {
        // OpenStack Swift specific code
    }
}

public class LtfsStrategy implements StorageContainerStrategy {
    @Override
    public void write() {
        // LTFS specific code
    }

    @Override
    public void read() {
        // LTFS specific code
    }
}

person MisterStrickland    schedule 23.12.2015    source источник
comment
Внедрите фабрику конфигурации в свои классы API. Фабрика вернет соответствующую конфигурацию по имени контейнера. Для тестирования вы можете внедрить фиктивную фабрику, которая возвращает известную конфигурацию.   -  person dbugger    schedule 23.12.2015
comment
Спасибо за комментарий. Я думаю, что завод - это путь. Однако ответ @mawalker предвосхитил что-то - я ожидаю, что появится все больше и больше слоев. Поэтому я собираюсь использовать фабрику, как вы сказали, но фабрику стратегий, делая каждую стратегию специфичной для реализации хранилища. Смотрите мое обновление выше для решения.   -  person MisterStrickland    schedule 28.12.2015


Ответы (1)


Вот статья, написанная Дугом Шмидтом (откровенно говоря, моим текущим консультантом по докторской диссертации) о шаблоне объекта контекста.

https://www.dre.vanderbilt.edu/~schmidt/PDF/Context-Object-Pattern.pdf

Как заявил dbugger, создание фабрики в ваших классах API, которая возвращает соответствующий объект «конфигурации», является довольно чистым способом сделать это. Но если вы знаете «контекст» (да, перегруженное использование) обсуждаемой статьи, это в основном для использования в промежуточном программном обеспечении. Там, где есть несколько уровней изменения контекста. Обратите внимание, что в разделе «Реализация» рекомендуется использовать Шаблон стратегии для добавления каждого «контекстная информация» слоя в «контекстный объект».

Я бы порекомендовал аналогичный подход. С каждым «контейнером хранения» связана своя стратегия. Таким образом, у каждого «драйвера» есть собственная стратегия реализации. сорт. Эта стратегия будет получена на заводе, а затем использована по мере необходимости. (Как спроектировать свои Strats... лучший способ (я предполагаю) состоял бы в том, чтобы сделать вашу «страту драйвера» общей для каждого типа драйвера, а затем настроить ее соответствующим образом по мере появления новых ресурсов/назначения объекта страта)

Но, насколько я могу судить прямо сейчас (если я не ошибаюсь в вашем вопросе), у него будет только 2 «слоя», о которых будет знать «контекстный объект», «остальные серверы» и « конечные точки хранения». Если я ошибаюсь, пусть будет так... но только с двумя слоями, вы можете просто использовать «шаблон стратегии» так же, как вы думали о «шаблоне контекста», и избежать проблемы с синглтонами/контекстом «анти-шаблон». '. (Вы «могли бы» иметь объект контекста, который содержит стратегию использования драйвера, а затем «конфигурацию» для этого драйвера... это не было бы безумием и могло бы хорошо соответствовать вашей динамической конфигурации HTTP.)

Фабричный класс стратегии (ов) не обязательно должен быть одноэлементным/иметь статические фабричные методы. Раньше я делал фабрики, которые были объектами, даже с D.I. для тестирования. Всегда есть компромиссы между различными подходами, но я обнаружил, что лучшее тестирование стоит того почти во всех случаях, с которыми я сталкивался.

person mawalker    schedule 24.12.2015
comment
Спасибо за такой уточняющий ответ! Я ожидаю, что в будущем будет больше слоев, поэтому я решил попробовать. Я прочитал статью и написал решение (см. Обновление 1 выше). Я создал фабрику стратегий, используя статический метод (для простоты — это может измениться в будущем). StorageContainerStrategy определяет контракт, которому должны соответствовать все водители. В зависимости от типа конечной точки фабрика вернет другую реализацию StorageContainerStrategy. Каждый объект контекста будет содержать объект strat и другую контекстную информацию. Это то, что вы имели в виду? - person MisterStrickland; 28.12.2015
comment
Стратегическая партия выглядит прекрасно. «Ключом» будет класс «контекст», и как именно вы с этим справитесь. Существует МНОГО способов справиться с этим. И во многом будет зависеть от остальной части вашей арки. но да, это вообще способ приблизиться к этому. Тем не менее, я бы создал ваш код с нуля, чтобы поддерживать D.I. таким образом, вам не нужно возвращаться и модифицировать его, чтобы сделать это. - person mawalker; 28.12.2015
comment
Я бы, вероятно, добавил новый интерфейс/объекты конфигурации, которые затем D.I. вставлен в стратегию импл. Это позволяет «стратегической реализации» оставаться статичной и просто передавать в нее отдельные «конфигурации». Это поможет с динамической конфигурацией (и, возможно, тестированием) - person mawalker; 28.12.2015