Date / SimpleDateFormatare mutable and not thread-safe.List.of(...).contains(null)throws NPE.Collections.unmodifiableListis only a view - changes to the original list still show through.Integer.parseInt(null)throwsNumberFormatExceptioninstead ofNullPointerException.Arrays.sort(a, null)silently falls back to natural order.
CSRD - Composable Single Responsibility Domains
- Unambiguity - API directly correlates to responsibility.
- One domain & strict domain rules.
- Refraining from taking on others' responsibilities or assumptions.
- Composability - the final effect should be achievable with composition instead of being imprinted into each function.
- The contract cannot be abandoned - parts of the contract cannot be omitted just because they are hard to implement or rare.
- Minimal expression - do not introduce an additional domain when the current one is self-explanatory.
- Doing no harm is merely the absolute minimum; the goal is to provide help.
- Don't make things up.
- Limitation lifting - when a domain lifts a limitation, it should be reflected in the implementation (direct to static) (it's unreasonable to push the limitation all the way up).
- Domain hierarchy has to end somewhere - an enclosing domain may explicitly limit which domains it supports (e.g. a demo, a single security framework, a single type of DB).
- Changing a condition vs changing structure.
Unambiguity - API directly correlates to responsibility.
There are infinite ways to make developers suffer by making decisions that are not ours to make - even worse when we do it silently. A list whose size can only grow. A converter that removes spaces. A getter that exposes everything inside. A method that invokes autoboxing when types change.public static <T extends Comparable<? super T>> T[] sort(T[] a) {}
public static <T extends Comparable<? super T>> T[] sort(T[] a, int fromIndex, int toIndexExclusive) {}
public static <T> T[] sort(T[] a, int fromIndex, int toIndexExclusive, Comparator<? super T> c) {}T[] a- must not assume mutability
- must not assume unlimited memory
- must not assume a nullability policy
<T extends Comparable<? super T>>- uses Comparable contract
int fromIndex, int toIndexExclusive- contract is applied to the given range
- must not assume external traversal logic
Comparator<? super T> c- must not switch to natural order
- must not assume a comparator can always be provided
- must not assume mutability - the possibility of sorting does not indicate whether the input may be modified - so, just in case, a defensive copy needs to be made. The cost of copying is intentional: compromises on safety never end, so the default must be safe, and speed must be requested explicitly.
- must not assume unlimited memory - we can expect cloning, so 2x array memory is reasonable, but 100x+ is not something we are allowed to do if it can be avoided. For this reason, sorting an infinite number of empty arrays should not allocate memory.
- must not assume a nullability policy - null, empty, or both is an external decision, not an internal one, so this information needs to be preserved.
static(modifier) - is both reserver word and a contract that allows null handling (full explanation later). - must not assume external traversal (view) logic - an empty range contains no elements, so there is nothing to sort and nothing out of bounds to touch. Such a range can come from a loop stepping by -3 that ends at from = to = -2, or from an index that is valid in the original array but falls outside the part we were given.
For clarification: Not empty ranges are still validated. Callers who treat an out-of-bounds empty range as a bug can state it explicitly:sort(ensure.indexRange(array, from, to), from, to). - must not assume a comparator can always be provided - a Comparator may be derived from the data itself, which is impossible for a null or empty array.
Another example is deriving it from the differences between elements, which needs at least 2 elements.
Forcing the caller to pass a placeholder (e.g.
(x, y) -> 0) instead risks polluting the data domain, because that fake comparator can be stored, passed on and reused as if it were real. Sorting fewer than 2 elements needs no comparison, so anullcomparator has exactly same ussage as real one.
For clarification: For 2 or more elements it still fails, with no fallback to natural order. Requiring one need to be stated explicitly :sort(array, from, to, Objects.requireNonNull(c)).
- copying is safer,
- mutating is faster,
- null-friendly is more practical,
- null-rejecting is cleaner,
- fallback is convenient,
- strict failure is more correct.
Refraining from taking on others' responsibilities or assumptions.
- Preparing data for output:
- generating XML manually
- trimming strings
- Bypassing the API layer and effectively welding the modules together.
- Injecting garbage into the project by importing libraries without:
- taking into account overall quality
- checking dependencies
- considering writing this 'one' method manually
- Polluting memory and forcing other layers to clean it up.
- Using given elements as operation cache.
Composability
Requirements can be added by composition:
- Basic sort, no preconditions:
array = sort(array, from, to); - Require array to be non-null:
array = sort(Objects.requireNonNull(array), from, to); - Validate index range:
array = sort(ensureIndexRange(array, from, to), from, to); - Require comparator to be non-null:
array = sort(array, from, to, Objects.requireNonNull(comparator));
- Default:
java.util.Arrays.sort(array, from, to); - With null support:
if (array != null) { java.util.Arrays.sort(array, from, to); } - With range guard - multiple solutions exist:
// Skip if empty if (array != null && from != to) { java.util.Arrays.sort(array, from, to); }// Reset if empty if (array != null) { if (from == to) { from = to = array.length; } java.util.Arrays.sort(array, from, to); }
The contract cannot be abandoned.
To make that happen, lowercase static classes seem to be the best way to express a space for CSRD requirements.
- They look and behave like packages, but we have more freedom in using them.
- They are easy to filter out from imports, since importing them would violate CSRD.
public class Arrays {
public static class mutable {
public static class sorted {
public static <C extends Comparable<? super C>> C[] distinct(C... array);
}
public static <T> T[] shuffle(T[] array);
}
public static class sorted {
public static <C extends Comparable<? super C>> C[] distinct(C... array);
}
public static <T> T[] shuffle(T[] array);
public static <T> T[] shuffleDerangement(T[] array);
}
mutable- the main argument and consumable ones can change state - including, but not limited to, changes after returning them.sorted- we need to execute the algorithm according to the given order - {"a", "B", "a"} will give a different result than {"a", "a", "B"}.ensure- pass through and report errors.elements- limits the domain to elements in arrays/collections/...onWords- shrinking would not cut words.static(modifier) - a domain carried by the keyword itself: a static method can be called without an object, so it can receivenullwhere an instance method could not. To not waste that, the method acceptsnulland returns what its domain defines for it:nullwhen there is nothing to produce (sort(null)), or a real answer when the domain has one (e.g.equals(null, null) → true). That answer must follow from the domain itself, not from a chosen default: treatingnullas empty is a policy, not an answer. Like other defaults (e.g. immutability), it can be overridden by an explicit domain, such asObjects.requireNonNull, whose responsibility is rejection.
Arrays.shuffle(array);- will make a defensive copy of the array, shuffle it, and return it.Arrays.mutable.shuffle(array);- will shuffle the given array and return it.Arrays.sorted.distinct(array);- will return unique elements by comparing neighbours.Arrays.mutable.sorted.distinct(array);- will return unique elements by comparing neighbours, but you can expect to get back the given array if possible as well.
shuffle- elements will be shuffled.shuffleDerangement- no element stays in its original position, which is possible for size > 1.
Minimal expression.
- If a problematic condition can be expressed through composition, the implementation should adopt the widest behaviour.
For example,
T randomElement(T[] array)is not required to throw whenarray.length == 1(selecting from it is not really random), nor when the array is empty - it returnsnull, because there is nothing to produce. Stricter contracts are added through composition:randomElement(Arrays.ensure.minLength(array, 2)). If the array may containnullelements,Arrays.ensure.minLength(array, 1)also removes the ambiguity between "empty" and "pickednull". - Domains that can be directly derived from the API do not need to be materialized as separate spaces. Sorting part of an array does not need partial space, as from and to already express the range. Immutability - which is the default for not destroying data, does not need to be stated.
Doing no harm vs providing help
- Empty arrays should always be reused, as they are immutable by themselves, so we do not need to worry about memory pollution if a utility uses one object every time.
- If possible, we should limit the number of executed methods to a minimum, because we cannot predict the final cost.
- Do not retain caller data longer than needed.
- Do not silently replace caller intent with (internal) defaults or fallback behavior.
Don't make things up!
Limitation lifting
static method that accepts null handles it inside,
instead of every caller adding if (x != null).
Pushing the limitation up would repeat the same guard in every layer above, and the domain would claim a lift it does not deliver.
Domain hierarchy has to end somewhere
- Universal type - domains expected to be used everywhere; they require the strictest and most universal rules.
- Limited type - domains for projects focused on a single problem (e.g. a demo or a single project); they may allow weaker assumptions between methods, as long as they are not exposed as a public API.
- Manual type - domains for supervised processes; they may omit some automated error handling, because the process itself provides oversight.
Changing a condition vs changing structure.
- Optional - the structure changes: fields, getters, constructors, mappers, generics, and tests that use
Optionalmust all be rewritten. - CSRD - a condition changes: composed guards (
Objects.requireNonNull,ensure) are added or removed at call sites.
-1 as "not found" and language-level nullability (T?) remain popular and widely used: the policy can change while the data shape stays the same. Type-level approaches catch errors earlier; composed guards are cheaper to change; each scope chooses exactly the strictness it needs.
// CSRD: array required
result = sort(Objects.requireNonNull(array), from, to);// CSRD: policy change = remove array null guard:
result = sort(array, from, to);// classic
T[] field;
java.util.Arrays.sort(field, from, to);// Optional: policy change = structure change, spreading to every signature
Optional<T[]> field;
T[] result = field.map(a -> { java.util.Arrays.sort(a, from, to); return a; }).orElse(null);
// + every getter, constructor, mapper, test...
Real life example - analysis.
java.util.Objects:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
| a | b | Who decides | NPE for a.equals(null) | NPE for b.equals(null) |
|---|---|---|---|---|
| null | null | Objects decides: true | irrelevant | irrelevant |
| null | non-null | Objects decides: false | irrelevant | not called |
| non-null | null | handed to a.equals(null) | thrown | irrelevant |
| non-null | non-null | handed to a.equals(b) | irrelevant | irrelevant |
Implementation with proper contract would be one of:
- take over null check:
if (a == b) return true; if (a == null || b == null) return false; return a.equals(b); - always delegate it:
if (a == b) return true; // (null, null): no object to ask, so identity is the only answer return a != null ? a.equals(b) : b.equals(null);
Throwing from
equals - on null or on foreign types - violates the
Object.equals contract, but in a limited domain it can serve as a strict guard that prevents
security-sensitive objects from being silently compared with, or mixed into, other object spaces,
as long as every framework in use tolerates it and the class is clearly marked, directly or by its domain (Such a class must never reach a universal domain).
0 comments / komentarz(y):
Post a Comment