1

My service needs to put a message on a PubSub based on the Enum of Protocol in the message.

These are the PubSub Config

public class NotificationPublisherConfiguration {

    @Bean(name="websocketPublisher")
    public Publisher websocketPublisher(@Value("${gcp.projectId}") String gcpProjectId, @Value("${gcp.pubsub.notificationWebsocket}") String topicId) throws Exception {

        return Publisher.newBuilder(
                ProjectTopicName.newBuilder()
                        .setProject(gcpProjectId)
                        .setTopic(topicId)
                        .build()
        ).build();
    }

    @Bean(name="grpcPublisher")
    public Publisher grpcPublisher(@Value("${gcp.projectId}") String gcpProjectId, @Value("${gcp.pubsub.notificationGrpc}") String topicId) throws Exception {
        
        return Publisher.newBuilder(
                ProjectTopicName.newBuilder()
                        .setProject(gcpProjectId)
                        .setTopic(topicId)
                        .build()
        ).build();
    }
}

Now in my service class, I have set it up below.

public class NotificationService {


private final Publisher websocketPublisher;
private final Publisher grpcPublisher;



public void post(Map<SubscriptionType, Set<String>, String eventBody> subscriptionIdsByProtocol) throws Exception {

    for (Map.Entry<SubscriptionType, Set<String>> entry : subscriptionIdsByProtocol.entrySet()) {

        if (entry.getKey().equals(SubscriptionType.WEBSOCKET)) {
            publishMessage (eventBody, websocketPublisher, entry.getKey());
        } else if (entry.getKey().equals(SubscriptionType.GRPC)) {
            publishMessage(eventBody, grpcPublisher, entry.getKey());
        }
    }
}


private void publishMessage(String eventBody, Publisher publisher, SubscriptionType subscriptionType) {


    PubsubMessage pubsubMessage = PubsubMessage.newBuilder()
            .setData(eventBody)
            .build();

    ApiFuture<String> publish;

    try {
        publish = publisher.publish(pubsubMessage);
        log.debug("Message published: {}, on {}", pubsubMessage, subscriptionType.toString());
    } catch (Exception e) {}
  }
}

I am pretty sure there is a better way to do this so that I don't need to change a lot of code when a new protocol is introduced, and we need to put the message on a new PubSub as well. Can someone suggest what design pattern I can use here?

Thanks

BrownTownCoder
  • 1,312
  • 5
  • 19
  • 25
  • can you construct the `Publisher` inside the `SubscriptionType` enum? If this is acceptable for you I can provide a solution. I mean without using `@Value` unluckily – Pp88 Aug 17 '22 at 19:23

2 Answers2

2

You could do it without declaring the @Bean inside NotificationPublisherConfiguration, but be aware that this makes NotificationService quite impossible to test because you can't mock the injected beans, you can't use @Value properties and also each call to publishMessage will create a new Publisher but I think this can be solved somehow for example using a static method in a bean as stated here(if this is feasible you can also use injection and @Value and you will also be able to test NotificationService Mocking the static methods that will return the beans but I’ve not tryied) or maybe better with a sort of factory that produces singletons (also here you can mock the static methods an so you will be able to test)

public enum SubscriptionType {
    WEBSOCKET {
        @Override
        protected Publisher getPublisher() throws Exception {
            return Publisher.newBuilder(
                ProjectTopicName.newBuilder()
                        .setProject("you must hardcode the string")
                        .setTopic("you must hardcode the string")
                        .build()
             ).build();
        }
    },
    GRPC {
        @Override
        protected Publisher getPublisher() throws Exception {
            return Publisher.newBuilder(
                ProjectTopicName.newBuilder()
                        .setProject("you must hardcode the string")
                        .setTopic("you must hardcode the string")
                        .build()
         ).build();
    };
    
    protected abstract Publisher getPublisher() throws Exception;

    public void publishMessage(String eventBody) {
        PubsubMessage pubsubMessage = PubsubMessage.newBuilder()
            .setData(eventBody)
            .build();

        ApiFuture<String> publish;

        try {
            publish = getPublisher().publish(pubsubMessage);
            log.debug("Message published: {}, on {}", pubsubMessage, name());
        } catch (Exception e) {}
    }
    
}

and your NotificationService becomes:

public class NotificationService {

public void post(Map<SubscriptionType, Set<String>, String eventBody> subscriptionIdsByProtocol) throws Exception {

    for (Map.Entry<SubscriptionType, Set<String>> entry : subscriptionIdsByProtocol.entrySet()) {
        entry.getKey().publishMessage(eventBody);
    }
}

You can also write the enum like this it solves the singleton problem but makes the code untestable:

public enum SubscriptionType {
    WEBSOCKET(Publisher.newBuilder(
                ProjectTopicName.newBuilder()
                        .setProject("you must hardcode the string")
                        .setTopic("you must hardcode the string")
                        .build()),
    GRPC(Publisher.newBuilder(
                ProjectTopicName.newBuilder()
                        .setProject("you must hardcode the string")
                        .setTopic("you must hardcode the string")
                        .build());
    
    private Publisher publisher;

    private SubscriptionType(Publisher publisher) {
        this.publisher = publisher;
    }

    public void publishMessage(String eventBody) {
        PubsubMessage pubsubMessage = PubsubMessage.newBuilder()
            .setData(eventBody)
            .build();

        ApiFuture<String> publish;

        try {
            publish = publisher.publish(pubsubMessage);
            log.debug("Message published: {}, on {}", pubsubMessage, name());
        } catch (Exception e) {}
    }
    
}

I still prefer a factory, that makes the code testble.

Otherwise you can try to mock Publisher.newBuilder and this will solve all the testing problems but you still can’t use @Value

Edit: I've had the time to play a little bit with it this is the solution i come with:

  1. you can have the configuration class:
@Configuration
public class NotificationPublisherConfiguration {

    @Bean(name="websocketPublisher")
    public Publisher websocketPublisher(@Value("${gcp.projectId}") String gcpProjectId, @Value("${gcp.pubsub.notificationWebsocket}") String topicId) throws Exception {

        return Publisher.newBuilder(
                ProjectTopicName.newBuilder()
                        .setProject(gcpProjectId)
                        .setTopic(topicId)
                        .build()
        ).build();
    }

    @Bean(name="grpcPublisher")
    public Publisher grpcPublisher(@Value("${gcp.projectId}") String gcpProjectId, @Value("${gcp.pubsub.notificationGrpc}") String topicId) throws Exception {
        
        return Publisher.newBuilder(
                ProjectTopicName.newBuilder()
                        .setProject(gcpProjectId)
                        .setTopic(topicId)
                        .build()
        ).build();
    }
}
  1. We will use a component as factory:
@Component
public class PublisherFactory implements InitializingBean {
    
    private static final HashMap<SubscriptionType, Publisher> map = new HashMap<>();
    
    @Autowired
    @Qualifier("websocketPublisher")
    private Publisher webSocketPublisher;
    
    @Autowired
    @Qualifier("grpcPublisher")
    private Publisher grpcPublisher;
    
    @Override
    public void afterPropertiesSet() throws Exception {
        map.map(SubscriptionType.WEBSOCKET, webSocketPublisher);
        map.put(SubscriptionType.GRPC, grpcPublisher);
    }
    
    public static Publisher getPublisher(SubscriptionType protocol) {
        return map.get(protocol);
    }

}
  1. the enum will look like this:
public enum SubscriptionType {
    WEBSOCKET {
        @Override
        protected String getPublisher() {
            return PublisherFactory.getPublisher(this);
        }
    },
    GRPC {
        @Override
        protected String getPublisher() {
            return PublisherFactory.getPublisher(this);
        }
    };
    
    protected abstract String getPublisher();

    public void publishMessage(String eventBody) {
        PubsubMessage pubsubMessage = PubsubMessage.newBuilder()
            .setData(eventBody)
            .build();

        ApiFuture<String> publish;

        try {
            publish = getPublisher().publish(pubsubMessage);
            log.debug("Message published: {}, on {}", pubsubMessage, name());
        } catch (Exception e) {}
    }
}
  1. The notification service:
public class NotificationService {

public void post(Map<SubscriptionType, Set<String>, String eventBody> subscriptionIdsByProtocol) throws Exception {

    for (Map.Entry<SubscriptionType, Set<String>> entry : subscriptionIdsByProtocol.entrySet()) {
        entry.getKey().publishMessage(eventBody);
    }
}

Whit this you can test and use injection and @Value

Pp88
  • 830
  • 6
  • 19
1

You can use Strategy pattern to select a desired algorithm at runtime. However, it is necessary to store these algorithms somewhere. This is a place where Factory pattern can be used.

So let me show an example. I am sorry, I do not know Java, but let me show an example via C#. Code does not use special features of language, so you can apply it to Java.

So let's create an abstract class that will define common behaviour for all Publishers:

public abstract class Publisher
{ 
    public abstract void Publish();
}

And its concrete implementations WebSocketPublisher and GrpcPublisher :

public class WebSocketPublisher : Publisher
{
    public override void Publish()
    {
        Console.WriteLine("Message published through WebSocket.");
    }
}

public class GrpcPublisher : Publisher
{
    public override void Publish()
    {
        Console.WriteLine($"Message published through Grpc.");
    }
}

Then we need a place to store these strategies and take it when it is necessary. This is a place where Factory pattern can be used:

public enum SubscriptionType 
{
    WebSocket, Grpc
}


public class PublisherFactory
{
    private Dictionary<SubscriptionType, Publisher> _pubisherByType = 
        new Dictionary<SubscriptionType, Publisher>()
    {
        { SubscriptionType.WebSocket, new  WebSocketPublisher () },
        { SubscriptionType.Grpc, new GrpcPublisher () },
    };

    public Publisher GetInstanceByType(SubscriptionType courtType) => 
        _pubisherByType[courtType];
}

And then you can call the above code like this:

PublisherFactory courtFactory = new PublisherFactory();
Publisher publisher = courtFactory.GetInstanceByType(SubscriptionType.WebSocket);
publisher.Publish(); // OUTPUT: "Message published through WebSocket."

So, here we've applied open closed principle. I mean you will add new functionality by adding new classes that will be derived from Court class.

StepUp
  • 36,391
  • 15
  • 88
  • 148