PullXorPush
From APIDesign
(→Push like Example) |
(→Pull like Approach) |
||
Line 20: | Line 20: | ||
== Pull like Approach == | == Pull like Approach == | ||
- | Why register everything at the beginning? It is not even clear the registered translation will really be needed. In such open-ended system, it is very likely better to | + | Why register everything at the beginning? It is not even clear the registered translation will really be needed. In such open-ended system, it is very likely better to apply a '''pull'''-like pattern: |
<source lang="java"> | <source lang="java"> | ||
public interface Translate { | public interface Translate { |
Revision as of 07:40, 19 June 2017
The Extreme Advice Considered Harmful chapter of TheAPIBook talks about symmetry. It describes how harmful seeking for an absolute symmetry at all costs can be. The chapter argues that there often isn't a need for both GettersAndSetters, that in many cases just getters in an API are enough. Similar example can be given for pull vs. push in APIs - it is unlikely you need both of them at once.
Push like Example
Imagine you have a translation service and you'd like to register dictionaries to it. The push version could be based on following API:
public final class Translator { private Translator() { } public static void registerTranslation(String fromLanguage, String toLanguage, Map<String,String> vocabulary) { // ... } }
in a system of such kind you require all translation modules (it probably makes sense to assume the translations are developed independently, right?) to eagerly - e.g. as early as possible - register themselves into the system and provide necessary details. From which language they translate words, to which language and what is the vocabulary they can handle. This is probably too premature - better to delay such query and only ask when really needed.
Pull like Approach
Why register everything at the beginning? It is not even clear the registered translation will really be needed. In such open-ended system, it is very likely better to apply a pull-like pattern:
public interface Translate { public boolean canFromLanguage(String language); public boolena canToLanguage(String language); public String translate(String word); public static void registerTranslation(Translate t) { // ... } }
This has some benefits (and it can even be improved to work without the registerTranslation method with Lookup based registration or even more with AnnotationProcessors) over the push version. One doesn't have to load languages until they are really needed. Also one can translate unknown words - if the word to translate is in plural, it is the language implementation can make additional adjustments and convert it to singular form, translate it and then (in the target language) translate it back to plural. Or one can translate even words that don't make sense - by looking up the closest alternative.
This is the value of the pull-approach - one gets a callback and can adjust to it when needed.
Don't Pull & Push at Once!
The thing to remember is that (unless there are threading concerns), it is always possible to simulate the push approach with the pull one. E.g. it makes no sense to design an API that supports both - prefer the pull approach as the more flexible one. One can always turn push into pull:
public final class PushLikeTranslate implements Translate { static { Translate.registerTranslation(new PushLikeTranslate()); } public boolean canFromLanguage(String language) { // use the data from pushTranslation } public boolean canToLanguage(String language) { // use the data from pushTranslation } public String translate(String word) { // use the data from pushTranslation } public static void pushTranslation(String fromLanguage, String toLanguage, Map<String,String> vocabulary) { // register the vocabulary for further use } }
It is easy to mimic push approach when you have a chance to to register for a pull. Just like avoiding both GettersAndSetters, never support both pull & push in the same core API.