Aiming for object-oriented design elegance

Aiming for object-oriented design elegance

ยท

4 min read

๐Ÿ”” 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 {

void subscribe(Consumer onUpdate, Consumer onError);

}

public abstract class SubscribableProp implements Subscribable {

/* 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 extends SubscribableProp implements Supplier {

/* Identifies the Prop */

public abstract String key();

/* Returns the Prop's value */

public abstract T get();

}

public abstract class BoundableProp extends Prop {

/* 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 > 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 extends BoundableProp implements Converter {

/* 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 {

/* 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.encode, thus completing the class, e.g.:


public class LongProp extends CustomProp {

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 implements LongConverter {

}

public interface LongConverter extends Converter {

@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!

ย