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
CodePudding user response:
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
CodePudding user response:
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.