Imagine that you made a bit of code that outputs JSON.
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
Json json) {
...
}
}
By default your output contains no extra whitespace, but you want to provide an option to the user to print that JSON with some indentation.
[{"name":"joe","age":35}]
[
{
"name":"joe",
"age":35
}
]
Any toggles you add to your api are toggles you might need to support now and forever more. Depending on the code you are writing and who its consumers are, it might make more sense to provide a more restricted api.
With only a single option you want configurable, you can just expose a method with a different name.
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
Json json) {
...
}
public static void writeJsonWithIndentation(
Appendable out,
Json json) {
...
}
}
writeJson(out, json);
writeJsonWithIndentation(out, json);
A single option is either on or off. True or false. That is often the domain of a boolean.
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json jsonboolean indent
) {
if (indent) {
...
}
else {
...
}
}
}
writeJson(out, json, true);
writeJson(out, json, false);
Booleans are great, but for understandability at the call-site you might want to provide an enum with two possible values instead.
public enum Indentation {
,
INDENT
DO_NOT_INDENT}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json json
Indentation indent) {
switch (indent) {
case INDENT -> ...
case DO_NOT_INDENT -> ...
}
}
}
writeJson(out, json, Indentation.INDENT);
writeJson(out, json, Indentation.DO_NOT_INDENT);
Say now you get some feedback that while the indentation style is great for objects, it is sometimes not great for JSON with long arrays.
[{"numbers":[1,2,3]}]
[
{
"numbers": [
1,
2,
3
]
}
]
[{
"numbers": [1, 2, 3]
}]
[
{"numbers": [
1,
2,
3
]}
]
Your users just want a way to turn off indentation for arrays. Give it to them.
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
Json json) {
...
}
public static void writeJsonIndentObjects(
Appendable out,
Json json) {
...
}
public static void writeJsonIndentEverything(
Appendable out,
Json json) {
...
}
}
writeJson(out, json);
writeJsonIndentObjects(out, json);
writeJsonIndentEverything(out, json);
There are four logical settings that come out of two different flags, so you can certainly provide all four options as methods. Could save you time later.
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
Json json) {
...
}
public static void writeJsonIndentObjects(
Appendable out,
Json json) {
...
}
public static void writeJsonIndentArrays(
Appendable out,
Json json) {
...
}
public static void writeJsonIndentEverything(
Appendable out,
Json json) {
...
}
}
writeJson(out, json);
writeJsonIndentObjects(out, json);
writeJsonIndentArrays(out, json);
writeJsonIndentEverything(out, json);
Two logically independent things to configure, you can always take a boolean for each.
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json jsonboolean indentObjects,
boolean indentArrays
) {
if (indentObjects) {
if (indentArrays) {
...
}
else {
...
}
}
else {
...
}
}
}
writeJson(out, json, true, true);
writeJson(out, json, true, false);
writeJson(out, json, false, true);
writeJson(out, json, false, false);
Booleans describe everything, but enums are still more explicit.
public enum Indentation {
,
INDENT
DO_NOT_INDENT}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json json,
Indentation indentObjects
Indentation indentArrays) {
switch (indentObjects) {
case INDENT -> switch (indentArrays) {
case INDENT -> ...
case DO_NOT_INDENT -> ...
}
case DO_NOT_INDENT -> ...
}
}
}
writeJson(out, json, Indentation.INDENT, Indentation.INDENT);
writeJson(out, json, Indentation.INDENT, Indentation.DO_NOT_INDENT);
writeJson(out, json, Indentation.DO_NOT_INDENT, Indentation.INDENT);
writeJson(
,
out,
json.DO_NOT_INDENT,
Indentation.DO_NOT_INDENT
Indentation);
It's an old school solution and maybe a bit too clever, but you are feeling old school and clever.
public final class Indentation {
public static final int NO_INDENTATION = 0b00;
public static final int INDENT_OBJECTS = 0b01;
public static final int INDENT_ARRAYS = 0b10;
private Indentation() {}
}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json jsonint indentation
) {
if ((indentation & Indentation.INDENT_OBJECTS) != 0) {
if ((indentation & Indentation.INDENT_ARRAYS) != 0) {
...
}
else {
...
}
}
else {
...
}
}
}
writeJson(
out,
json,
Indentation.INDENT_OBJECTS | Indentation.INDENT_ARRAYS,
);
writeJson(out, json, Indentation.INDENT_OBJECTS);
writeJson(out, json, Indentation.INDENT_ARRAYS);
writeJson(out, json, Indentation.NO_INDENTATION);
Rather than waste a parameter on each flag, explicitly take the set of behaviors they want to enable.
public enum Indent {
,
OBJECTS
ARRAYS}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json jsonEnumSet<Indent> indent
) {
if (indent.contains(Indent.OBJECTS)) {
if (indent.contains(Indent.ARRAYS)) {
...
}
else {
...
}
}
else {
...
}
}
}
writeJson(out, json, EnumSet.of(Indent.OBJECTS, Indent.ARRAYS));
writeJson(out, json, EnumSet.of(Indent.OBJECTS));
writeJson(out, json, EnumSet.of(Indent.ARRAYS));
writeJson(out, json, EnumSet.noneOf(Indent.class));
Similar to just taking two booleans, but putting them in an object means you can refer to a set of options as a concrete "thing". This could help you keep the most common usages terse.
Options(boolean indentObjects, boolean indentArrays) {
record public static final Options INDENT_EVERYTHING =
new Options(true, true);
public static final Options NO_INDENT =
new Options(false, false);
}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json json
Options options) {
if (options.indentObjects()) {
if (options.indentArrays()) {
...
}
else {
...
}
}
else {
...
}
}
}
writeJson(out, json, Options.INDENT_EVERYTHING);
writeJson(out, json, new Options(true, false));
writeJson(out, json, new Options(false, true));
writeJson(out, json, Options.NO_INDENT);
Maybe you want to give your api some extra wiggle room to grow. Maybe you just like how the usage of an opaque object made from a builder looks.
With this approach you can choose to internally represent things as booleans, enums, an enum set, bit flags, or whatever other evil lies within the hearts of mankind.
public final class Options {
private final boolean indentObjects;
private final boolean indentArrays;
private Options(Builder builder) {
this.indentArrays = builder.indentArrays;
this.indentObjects = builder.indentObjects;
}
public boolean indentArrays() {
return this.indentArrays;
}
public boolean indentObjects() {
return this.indentObjects;
}
public static Options standard() {
return builder().build();
}
public static Builder builder() {
return new Builder();
}
public final class Builder {
private boolean indentObjects;
private boolean indentArrays;
private Builder() {
this.indentObjects = false;
this.indentArrays = false;
}
public Builder indentObjects() {
this.indentObjects = true;
return this;
}
public Builder indentArrays() {
this.indentArrays = true;
return this;
}
public Options build() {
return new Options(this);
}
}
}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json json
Options options) {
if (options.indentObjects()) {
if (options.indentArrays()) {
...
}
else {
...
}
}
else {
...
}
}
}
writeJson(
out,
json,
Options.builder()
.indentObjects()
.indentArrays()
.build()
);
writeJson(
out,
json,
Options.builder()
.indentObjects()
.build()
);
writeJson(
out,
json,
Options.builder()
.indentArrays()
.build()
);
writeJson(out, json, Options.standard());
🚑 Weewoo Weewoo 🚑
Its legal! Your apis are great and all, but when you send data to external clients we would really like to include an explicit statement of copyright. That copyright message might change depending on your contract with the client and also we shouldn't send it internally.
Good luck, legal out.
[
{
"name": "joe",
"age": 35
}
]
{
"copyright": "(c) 2022 Inc.",
"data": [
{
"name":"joe",
"age":35
}
]
}
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
Json json) {
...
}
public static void writeJsonIndentObjects(
Appendable out,
Json json) {
...
}
public static void writeJsonIndentArrays(
Appendable out,
Json json) {
...
}
public static void writeJsonIndentEverything(
Appendable out,
Json json) {
...
}
public static void writeJsonWithCopyright(
Appendable out,
,
Json jsonString copyright
) {
...
}
public static void writeJsonIndentObjectsWithCopyright(
Appendable out,
,
Json jsonString copyright
) {
...
}
public static void writeJsonIndentArraysWithCopyright(
Appendable out,
,
Json jsonString copyright
) {
...
}
public static void writeJsonIndentEverythingWithCopyright(
Appendable out,
,
Json jsonString copyright
) {
...
}
}
writeJson(out, json);
writeJsonIndentObjects(out, json);
writeJsonIndentArrays(out, json);
writeJsonIndentEverything(out, json);
writeJsonWithCopyright(out, json, "(c) 2022");
writeJsonIndentObjectsWithCopyright(out, json, "(c) 2022");
writeJsonIndentArraysWithCopyright(out, json, "(c) 2022");
writeJsonIndentEverythingWithCopyright(out, json, "(c) 2022");
If your boolean-like options only took up a single overload, you can get away with just adding a single new method to the list.
This will look different depending on whether you used booleans, enums, an EnumSet
, or bit flags.
public enum Indent {
,
OBJECTS
ARRAYS}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json jsonEnumSet<Indent> indent
) {
}
public static void writeJson(
Appendable out,
,
Json jsonEnumSet<Indent> indent,
String copyright
) {
}
}
writeJson(out, json, EnumSet.of(Indent.OBJECTS, Indent.ARRAYS));
writeJson(out, json, EnumSet.of(Indent.OBJECTS));
writeJson(out, json, EnumSet.of(Indent.ARRAYS));
writeJson(out, json, EnumSet.noneOf(Indent.class));
writeJson(
,
out,
jsonEnumSet.of(Indent.OBJECTS, Indent.ARRAYS),
"(c) 2022"
);
writeJson(
,
out,
jsonEnumSet.of(Indent.OBJECTS),
"(c) 2022"
);
writeJson(
,
out,
jsonEnumSet.of(Indent.ARRAYS),
"(c) 2022"
);
writeJson(
,
out,
jsonEnumSet.noneOf(Indent.class),
"(c) 2022"
);
If you don't want to add yet another overload, you can always just allow users to pass null
.
public enum Indent {
,
OBJECTS
ARRAYS}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json jsonEnumSet<Indent> indent,
String copyright
) {
if (indent.contains(Indent.OBJECTS)) {
if (indent.contains(Indent.ARRAYS)) {
if (copyright == null) {
...
}
else {
...
}
}
else {
...
}
}
else {
...
}
}
}
writeJson(out, json, EnumSet.of(Indent.OBJECTS, Indent.ARRAYS), null);
writeJson(out, json, EnumSet.of(Indent.OBJECTS), null);
writeJson(out, json, EnumSet.of(Indent.ARRAYS), null);
writeJson(out, json, EnumSet.noneOf(Indent.class), null);
writeJson(
,
out,
jsonEnumSet.of(Indent.OBJECTS, Indent.ARRAYS),
"(c) 2022"
);
writeJson(out, json, EnumSet.of(Indent.OBJECTS), "(c) 2022");
writeJson(out, json, EnumSet.of(Indent.ARRAYS), "(c) 2022");
writeJson(out, json, EnumSet.noneOf(Indent.class), "(c) 2022");
Optional
It's time to paint a bike shed. You can do this with java.util.Optional
or your own sealed type.
public enum Indent {
,
OBJECTS
ARRAYS}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json jsonEnumSet<Indent> indent,
<String> copyright
Optional) {
if (indent.contains(Indent.OBJECTS)) {
if (indent.contains(Indent.ARRAYS)) {
if (copyright.isPresent()) {
... copyright.orElseThrow() ...
} else {
...
}
} else {
...
}
} else {
...
}
}
}
writeJson(
,
out,
jsonEnumSet.of(Indent.OBJECTS, Indent.ARRAYS),
.empty()
Optional);
writeJson(
,
out,
jsonEnumSet.of(Indent.OBJECTS),
.empty()
Optional);
writeJson(
,
out,
jsonEnumSet.of(Indent.ARRAYS),
.empty()
Optional);
writeJson(
,
out,
jsonEnumSet.noneOf(Indent.class),
.empty()
Optional);
writeJson(
,
out,
jsonEnumSet.of(Indent.OBJECTS, Indent.ARRAYS),
.of("(c) 2022")
Optional);
writeJson(
,
out,
jsonEnumSet.of(Indent.OBJECTS),
.of("(c) 2022")
Optional);
writeJson(
,
out,
jsonEnumSet.of(Indent.ARRAYS),
.of("(c) 2022")
Optional);
writeJson(
,
out,
jsonEnumSet.noneOf(Indent.class),
.of("(c) 2022")
Optional);
Options(
record boolean indentObjects,
boolean indentArrays,
String copyright) {
public static final Options INDENT_EVERYTHING =
new Options(true, true, null);
public static final Options NO_INDENT =
new Options(false, false, null);
}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json json
Options options) {
if (options.indentObjects()) {
if (options.indentArrays()) {
if (options.copyright() == null) {
...
}
else {
...
}
}
else {
...
}
}
else {
...
}
}
}
Optional
property to a transparent config objectSame as above, but if you like to explicitly have the property be optional you can, but if you are using records as your transparent data carriers then you need to take an Optional
in your constructor.
Options(
record boolean indentObjects,
boolean indentArrays,
<String> copyright
Optional) {
public static final Options INDENT_EVERYTHING =
new Options(true, true, Optional.empty());
public static final Options NO_INDENT =
new Options(false, false, Optional.empty());
}
public final class Options {
private final boolean indentObjects;
private final boolean indentArrays;
private final String copyright;
private Options(Builder builder) {
this.indentArrays = builder.indentArrays;
this.indentObjects = builder.indentObjects;
this.copyright = builder.copyright;
}
public boolean indentArrays() {
return this.indentArrays;
}
public boolean indentObjects() {
return this.indentObjects;
}
public Optional<String> copyright() {
return Optional.ofNullable(this.copyright);
}
public static Options standard() {
return builder().build();
}
public static Builder builder() {
return new Builder();
}
public final class Builder {
private boolean indentObjects;
private boolean indentArrays;
private String copyright;
private Builder() {
this.indentObjects = false;
this.indentArrays = false;
this.copyright = null;
}
public Builder indentObjects() {
this.indentObjects = true;
return this;
}
public Builder indentArrays() {
this.indentArrays = true;
return this;
}
public Builder copyright(String copyright) {
this.copyright = copyright;
return this;
}
public Options build() {
return new Options(this);
}
}
}
writeJson(
,
out,
json.builder()
Options.indentObjects()
.indentArrays()
.build()
);
writeJson(
,
out,
json.builder()
Options.indentObjects()
.build()
);
writeJson(
,
out,
json.builder()
Options.indentArrays()
.build()
);
writeJson(out, json, Options.standard());
writeJson(
,
out,
json.builder()
Options.indentObjects()
.indentArrays()
.copyright("(c) 2022")
.build()
);
writeJson(
,
out,
json.builder()
Options.indentObjects()
.copyright("(c) 2022")
.build()
);
writeJson(
,
out,
json.builder()
Options.indentArrays()
.copyright("(c) 2022")
.build()
);
writeJson(
,
out,
json.builder()
Options.copyright("(c) 2022")
.build()
);
It would be a lot more efficient if you started sending your JSON in a binary format like MessagePack. It has the same data model as JSON, so it should work out.
Also, when sending in that binary format there is a choice between "Little Endian" and "Big Endian".
Problem is, there really isn't a meaning to indentation in a binary format or to endianness in a text one.
For a split as fundamental as this, it might make sense to start to make an entirely separate API for the new JSON-like format.
public enum Indent {
,
OBJECTS
ARRAYS}
...
enum Endianness {
,
BIG_ENDIAN
LITTLE_ENDIAN}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json jsonEnumSet<Indent> indent,
<String> copyright
Optional) {
...
}
public static void writeMessagePack(
Appendable out,
,
Json json,
Endianness endianness<String> copyright
Optional) {
...
}
}
writeJson(
,
out,
jsonEnumSet.of(Indent.OBJECTS),
.of("(c) 2022")
Optional);
writeMessagePack(
,
out,
json.BIG_ENDIAN,
Endianness.of("(c) 2022")
Optional);
You were surprised you didn't think of this first. Dynamic dispatch is some classic Java stylings.
public interface JsonWriter {
void write(Appendable out, JSON json);
}
...
public final class TextJsonWriter implements JsonWriter {
private final boolean indentObjects;
private final boolean indentArrays;
private final String copyright;
public TextJsonWriter(
boolean indentObjects,
boolean indentArrays,
String copyright
) {}
@Override
public void write(Appendable out, JSON json) {
...
}
}
...
enum Endianness {
,
BIG_ENDIAN
LITTLE_ENDIAN}
...
public final class BinaryJsonWriter implements JsonWriter {
private final Endianness endianness;
private final String copyright;
public BinaryJsonWriter(
,
Endianness endiannessString copyright
) {}
@Override
public void write(Appendable out, JSON json) {
...
}
}
new BinaryJsonWriter(
Endianness.BIG_ENDIAN,
"(c) 2022"
).writeJson(out, json);
new TextJsonWriter(
true,
false,
"(c) 2022"
).writeJson(out, json);
You need to choose whether you silently ignore bad combinations of objects and what behaviors get preference, but there is a simplicity to just throwing it all into a record or opaque object and figuring it out from there.
enum Endianness {
,
BIG_ENDIAN
LITTLE_ENDIAN}
Options(
record Boolean indentObjects,
Boolean indentArrays,
String copyright,
boolean useBinary,
Endianness endianness) {
public static final Options INDENT_EVERYTHING =
new Options(
true,
true,
null,
false,
null
);
public static final Options NO_INDENT =
new Options(
false,
false,
null,
false,
null
);
public static final Options BINARY_LE =
new Options(
null,
null,
null,
true,
.LITTLE_ENDIAN
Endianness);
}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json json
Options options) {
if (options.useBinary() &&
(options.indentArrays() != null
|| options.indentObjects() != null)) {
// ignore or throw
...
}
else if (!options.useBinary() &&
.endianness() != null) {
options...
}
else {
...
}
}
}
writeJson(
,
out,
json.INDENT_EVERYTHING
Options);
writeJson(
,
out,
json.BINARY_LE
Options);
writeJson(
,
out,
jsonnew Options(
null,
null,
null,
true,
.BIG_ENDIAN
Endianness)
);
With a bit of restructuring, you can actually make an Options
object that will correctly handle having that disjoint set of options.
Maybe not what you would choose with 100 settings or more complicated legality restrictions, but for this case it all seems to work out.
enum Endianness {
,
BIG_ENDIAN
LITTLE_ENDIAN}
interface Options permits BinaryOptions, TextOptions {
sealed <String> copyright();
Optional}
TextOptions(
record @Override Optional<String> copyright,
boolean indentObjects,
boolean indentArrays
) implements Options {}
BinaryOptions(
record @Override Optional<String> copyright,
Endianness endianness) implements Options {}
...
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json json
Options options) {
switch (options) {
case TextOptions textOptions -> {
...
}
case BinaryOptions binaryOptions ->
switch (binaryOptions.endianness()) {
case BIG_ENDIAN -> ...
case LITTLE_ENDIAN -> ...
}
}
}
}
writeJson(
,
out,
jsonnew TextOptions(Optional.of("(c) 2022"), true, false)
);
writeJson(
,
out,
jsonnew BinaryOptions(Optional.empty(), Endianness.BIG_ENDIAN)
);
This was always an option. It works just as well here as it does in a dynamic language, it's just a tad more verbose and unsafe.
public final class JsonWriter {
private JsonWriter() {}
public static void writeJson(
Appendable out,
,
Json jsonMap<String, Object> options
) {
= options.get("copyright");
var copyright if (copyright == null) {
...
= options.get("binary");
var endianness ...
}
else if (copyright instanceof String copyrightString) {
...
}
else {
throw new IllegalArgumentException(...);
}
}
}
writeJson(
,
out,
jsonMap.of(
"indentObjects", true,
"copyright", "(c) 2022"
)
);
writeJson(
,
out,
jsonMap.of(
"binary", true,
"endianness", Endianness.BIG_ENDIAN
)
);
You've taken the mouse to the movies. People don't need all the configuration options you've provided and don't like using the API that has them. They want a simpler API.
Maybe you should have gone with option 1.
Exercise for the reader. Make a spreadsheet of all these options versus all the criteria you use to evaluate software and fill in the grid with smiley faces, frowny faces, and meh faces. Feel free to fill in some options I missed, like reading from environment variables, system properties, or more inheritance schemes.