🔔 This article was originally posted on my site, MihaiBojin.com. 🔔
One of my goals for this project was to create a library that is elegant and easy to use while feeling Java idiomatic.
Let's talk SOLID. They're a set of five principles aimed at making object-oriented design implementations flexible and maintainable. I am designing a library that I hope will be a joy to use and will make developers want to adopt it, for which reason I interpreted these principles in the best possible form I could think of.
My initial thought was to offer a base Prop
interface, abstracting away lower-level implementation details that are not relevant to users of this class.
However, I settled on using a few abstract classes, for several reasons, inspired by a few of the SOLID principles:
- all
Prop
objects need a few common traits - a class should have a single responsibility; since I was building multiple themes, sticking them all into a single class didn't feel elegant
- it should be easy to build on top of each layer
- not all methods need to be exposed in the final public contract; unfortunately, Java interfaces do not support non-public methods
Let's break it down; here is the high-level end-result class design:
@FunctionalInterface
public interface Subscribable<T> {
void subscribe(Consumer<T> onUpdate, Consumer<Throwable> onError);
}
public abstract class SubscribableProp<T> implements Subscribable<T> {
/* Processes a value update event. */
protected void onValueUpdate(@Nullable T value, long epoch) {}
/* Processes an exception encountered during an update. */
protected void onUpdateError(Throwable error, long epoch) {}
}
public abstract class Prop<T> extends SubscribableProp<T> implements Supplier<T> {
/* Identifies the Prop */
public abstract String key();
/* Returns the Prop's value */
public abstract T get();
}
public abstract class BoundableProp<T> extends Prop<T> {
/* Allows the Registry to update a Prop's value */
protected abstract boolean setValue(@Nullable String value);
}
public class Registry {
/* Binds a Prop object to the Registry object, allowing it to process update events and set the Prop's value */
public <T, PropT extends BoundableProp<T>> PropT bind(PropT prop) {}
}
Subscribable
denotes that a Prop
can be subscribed to. The result of a prop update is either success or an error.
SubscribableProp
is a partial implementation that hosts the logic necessary to process updates/errors and notify clients safely.
Prop
is the absolute minimum public contract that a consumer/client should care about. It defines an identifier (key
) and a way to get the prop's value
.
Finally, BoundableProp
encompasses all of the above and also includes a mechanism that allows the Registry
to update prop values when the underlying sources are updated.
However, in practice, relying on a key and value alone, is not enough of a reason to adopt this library.
For that reason, the CustomProp
class provides an almost complete implementation, par the corresponding Converter.decode()
method, which requires a knowledge of the Prop's type.
public abstract class CustomProp<T> extends BoundableProp<T> implements Converter<T> {
/* Identifies the Prop */
public String key() {};
/* Returns the Prop's value */
public String get() {};
/* Describes the prop */
public String description() {};
/* true, if the prop is required */
public boolean isRequired() {};
/* true, if the prop is a secret */
public boolean isSecret() {};
}
@FunctionalInterface
public interface Converter<T> {
/* Decodes a String into the desired type; must be implemented */
T decode(@Nullable String value);
/* Encodes the value into a String, defaulting to using Object.toString() */
default String encode(@Nullable T value) {
return value == null ? null : value.toString();
}
}
One way to extend CustomProp
is to provide an implementation for Converter.decode
, thus completing the class, e.g.:
public class LongProp extends CustomProp<Long> {
public Long decode(String value) {
Number number = safeParseNumber(value);
try {
return NumberFormat.getInstance().parse(value).longValue();
} catch (ParseException e) {
log.log(SEVERE, e);
return null;
}
}
}
However, we can do a bit better. Since one can assume that most props will be of common Java datatypes, I have provided a series of default converters that can be composed into a final implementation. The above code can be rewritten as follows:
public class LongProp extends CustomProp<Long> implements LongConverter {
}
public interface LongConverter extends Converter<Long> {
@Override
public Long decode(String value) {
Number number = safeParseNumber(value);
try {
return NumberFormat.getInstance().parse(value).longValue();
} catch (ParseException e) {
log.log(SEVERE, e);
return null;
}
}
}
How would we use this in production? Here's a small complete excerpt:
Source source = new PropertyFile(PATH_TO_PROP_FILE);
Registry registry = new RegistryBuilder(source).build();
Prop prop = registry.bind(new LongProp("a.key"));
prop.get(); // will return the value corresponding to a.key
prop.subscribe(updatedValue -> {/* process updates */},
error -> {/* process any errors */});
Hopefully, this article serves as a good high-level introduction to the contract one can expect from the props library.
In future series I'd like to explore the props library's API a bit more and show a few real-world examples of how it could be used to simplify application settings/property management in Java projects.
As always, any feedback is welcome; feel free to ping me on Twitter.
Thanks!
If you liked this article and want to read more like it, please subscribe to my newsletter; I send one out every few weeks!
Top comments (0)