Я разрабатываю архитектуру на 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
}
}