The anatomy of high quality code that supports longevity, cross-team usage, and correctness
Some limitations might apply
Trust me, I am a developer too.
What do you think is good code?
There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies.
C.A.R. Hoare, The 1980 ACM Turing Award Lecture
An exercise
Let's continue the last ideas
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
Martin Fowler
If you are proud on the fact, that nobody gets your code despite the code being correct, you are working for the wrong company.
René Schwietzke
And there is more...
Just data from four popular projects
https://erikbern.com/2016/12/05/the-half-life-of-code.html
Just data from four popular projects
https://erikbern.com/2016/12/05/the-half-life-of-code.html
Just to Get into the Mood
Simplicity is prerequisite for reliability.
Edsger W. Dijkstra
https://developerzen.com/how-do-you-define-good-code-c8a383c207a4
I invented the term ‘Object-Oriented’, and I can tell you I did not have C++ in mind.
Alan Kay
The final touch - Balance
http://geekandpoke.typepad.com/geekandpoke/2012/04/sometimes-its-that-simple.html
Let's talk about writing code
Really?
We won't care about the precise format here...
Just some ideas for Java code based on personal preferences
/**
* The Intershop and Demandware Style
*
* Lookup the key in the map, returns the value if found, null otherwise.
* Will throw an IllegalArgumentException when null is passed as key.
*
* @param k the key to look up in the map
*
* @return the value found or null otherwise
*/
@Override
public V get(final K key) throws IllegalArgumentException
{
// prevent null from being used
if (key == null)
{
throw new IllegalArgumentException("Get doesn't support null keys");
}
// where is our entry located
final int pos = calculatePosition(key);
// get the entry
final Entry<K, V> entry = getEntry(key, pos);
if (entry == null)
{
// does not exist
return null;
}
else
{
// we know it, return value
return entry.value;
}
}
The JavaScript guys just follow the common linter, there is less arguing about style in the JS world.
Good code doesn't need comments, it explains itself...?!
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.
Rick Osborne
[1] https://www.sandimetz.com/blog/2017/6/1/the-half-life-of-code
Best practices
We will return to the API topic
/**
* Hash table based implementation of the <tt>Map</tt> interface. This
* implementation provides all of the optional map operations, and permits
* <tt>null</tt> values and the <tt>null</tt> key. (The <tt>HashMap</tt>
* class is roughly equivalent to <tt>Hashtable</tt>, except that it is
* unsynchronized and permits nulls.) This class makes no guarantees as to
* the order of the map; in particular, it does not guarantee that the order
* will remain constant over time.
*
* <p>This implementation provides constant-time performance for the basic
* operations (<tt>get</tt> and <tt>put</tt>), assuming the hash function
* disperses the elements properly among the buckets. Iteration over
* collection views requires time proportional to the "capacity" of the
* <tt>HashMap</tt> instance (the number of buckets) plus its size (the number
* of key-value mappings). Thus, it's very important not to set the initial
* capacity too high (or the load factor too low) if iteration performance is
* important.
*
* <p>An instance of <tt>HashMap</tt> has two parameters that affect its
* performance: <i>initial capacity</i> and <i>load factor</i>. The
* <i>capacity</i> is the number of buckets in the hash table, and the initial
* capacity is simply the capacity at the time the hash table is created. The
* <i>load factor</i> is a measure of how full the hash table is allowed to
* get before its capacity is automatically increased. When the number of
* entries in the hash table exceeds the product of the load factor and the
* current capacity, the hash table is <i>rehashed</i> (that is, internal data
* structures are rebuilt) so that the hash table has approximately twice the
* number of buckets.
* <p>As a general rule, the default load factor (.75) offers a good
* tradeoff between time and space costs. Higher values decrease the
* space overhead but increase the lookup cost (reflected in most of
* the operations of the <tt>HashMap</tt> class, including
* <tt>get</tt> and <tt>put</tt>). The expected number of entries in
* the map and its load factor should be taken into account when
* setting its initial capacity, so as to minimize the number of
* rehash operations. If the initial capacity is greater than the
* maximum number of entries divided by the load factor, no rehash
* operations will ever occur.
*
* <p>If many mappings are to be stored in a <tt>HashMap</tt>
* instance, creating it with a sufficiently large capacity will allow
* the mappings to be stored more efficiently than letting it perform
* automatic rehashing as needed to grow the table. Note that using
* many keys with the same {@code hashCode()} is a sure way to slow
* down performance of any hash table. To ameliorate impact, when keys
* are {@link Comparable}, this class may use comparison order among
* keys to help break ties. ...
/*
* Implementation notes.
*
* This map usually acts as a binned (bucketed) hash table, but
* when bins get too large, they are transformed into bins of
* TreeNodes, each structured similarly to those in
* java.util.TreeMap. Most methods try to use normal bins, but
* relay to TreeNode methods when applicable (simply by checking
* instanceof a node). Bins of TreeNodes may be traversed and
* used like any others, but additionally support faster lookup
* when overpopulated. However, since the vast majority of bins in
* normal use are not overpopulated, checking for existence of
* tree bins may be delayed in the course of table methods.
*
* Tree bins (i.e., bins whose elements are all TreeNodes) are
* ordered primarily by hashCode, but in the case of ties, if two
* elements are of the same "class C implements Comparable",
* type then their compareTo method is used for ordering. (We
* conservatively check generic types via reflection to validate
* this -- see method comparableClassFor). The added complexity
* of tree bins is worthwhile in providing worst-case O(log n)
* operations when keys either have distinct hashes or are
* orderable, Thus, performance degrades gracefully under
* accidental or malicious usages in which hashCode() methods
* return values that are poorly distributed, as well as those in
* which many keys share a hashCode, so long as they are also
* Comparable. (If neither of these apply, we may waste about a
* factor of two in time and space compared to taking no
* precautions. But the only known cases stem from poor user
* programming practices that are already so slow that this makes
* little difference.)
*
* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
/**
* Implements Map.remove and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
An example
/**
* Processes an uri and replace an encoded space (+) by %20.
* There is no error handling defined, make sure you don't pass
* null.
*
* This method is thread-safe and stateless.
*
* @param uri
* the uri as String to process, null is not permitted
* @return a new uri as String with all spaces being encoded as %20
* instead of +
*/
public static String fixUrlSpaceEncoding(final String uri)
{
// determine ?
final int pos = uri.indexOf("?");
if (pos < 0)
{
// nothing to do
return uri;
}
// get the data before and after the ? pos, keep the ?
final String path = uri.substring(0, pos);
final String query = uri.substring(pos);
// we assume that some replacements have to be made and hence ask
// for a little more space (+16) already to avoid that the buffer grows
// that is ok for our cases right now, because we mostly have only
// 1-4 spaces in it, if you encounter more, you can start with more overhead
// to keep it overall efficient
final StringBuilder sb = new StringBuilder(uri.length() + 16).append(path);
for (int i = 0; i < query.length(); i++)
{
final char c = query.charAt(i);
// replace + with %20
if (c == '+')
{
sb.append("%20");
}
else
{
sb.append(c);
}
}
return sb.toString();
}
Very simple example
Don't write what you don't need
Object-oriented programming is an exceptionally bad idea which could only have originated in California.
Edsger W. Dijkstra
That is opinionated
You owe it to yourself
https://www.toptal.com/api-developers/5-golden-rules-for-designing-a-great-web-api
Logic should be readable easily
or
and and
!
can be overlooked, consider foo == false
insteadpublic boolean doIt(String s)
{
// empty s cannot be processed, sort things out early
if (s == null || s.length() == 0)
{
return false;
}
// That is BAD code here
if (s.length() == 3 &&
!"foo".equals(s) &&
!("bar".equals(s) || "rab".equals(s)))
{
return false;
}
// final result (too obscure)
return s.split(",").length() > 3 ?
return true :
s.split(";").length() < 3;
}
Be clear, don't negate what you want to express
# Uncomment to disable the processing, it is enabled by default
# myapplication.processing.disable = true
/**
* Horrible example
*/
public void test(final boolean disabled)
{
if (!disabled)
{
// do stuff here
}
if (isEnabled())
{
setEnabled(disabled);
}
}
By consistent and compare to what others do
/**
* Remove all entries from the list
*
* But why purge, all of Java says clear()?!
*/
public void purge()
{
final boolean globalState = getGlobalState();
final String organisationName = getName();
for (final char c : organisationName.toCharArray())
{
// more here
}
}
// parameters and overloading
public void removeCarsFromStreet(final Street street)
public void removeCarsFromStreet(
final Street street, final Car carType)
public void removeCarsFromStreetByType(
final Street street, final Car carType)
public V getFromMap(final K key); // JDK says get()
public void put(List<V> list); // JDK says putAll or addAll!
public final static int NUMBER = 42; // I am a constant
public final static String DEFAULT_NAME = "Maria"; // So do I
Think before you name public APIs, there is no way back.
Retire or remove?
/**
* Demo
*/
public boolean demo(final String s)
{
// ...
// Comment and replaced with enhanced for, because it is
// more compact code (FOO-987654)
// for (int i = 0; i < s.length(); i++)
// {
// final char c = s.charAt(i);
// if (c != ' ')
// {
// result.add(c);
// }
// }
for (final char c : s.toCharArray())
{
if (c != ' ')
{
result.add(c);
}
}
// ...
}
Get the rare cases right
public void test()
{
try(InputStream is = new FileInputStream(new File("f.txt")))
{
int i = is.read();
}
catch (final FileNotFoundException e)
{
// do your stuff here
// try to avoid other exceptions
}
catch (final IOException e)
{
// do your stuff here
// try to avoid other exceptions
}
finally
{
// in case something is left to do after all that
// trouble
}
}
We will see some of that in High Performance Java
Explain why is something not there
/**
* Fill the battery if needed
*/
public void fillUpOfNeeded()
{
if (getGlobal() < 42)
{
recharge();
}
else
{
// we don't need to recharge, we still have
// enough juice
}
}
That is also highly subjective.
Small methods are good methods
Arguments are implicit documentation
public void test()
public void test(boolean a, String b)
public void test(boolean a, String b, String c)
public void test(String b, String c, String d, boolean a)
public void split(
String line,
char delimiter,
boolean quotesPermitted)
Contain data to the required scope
public void limitedScope(final String s)
{
// dont't do that
char c;
// make things final if possible
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++)
{
c = s.charAt(i);
sb.append(c);
}
...
}
State is hard to control and error-prone
public class DiceCup
{
// list of dice to use
private final List<Dice> dice = new ArrayList<>();
/**
* Constructor, takes a list of dice to be used
*/
public DiceCup(final List<Dice> dice)
{
this.dice.addAll(dice);
}
/**
* Rolls all dice
*/
public List<Dice> roll()
{
for (final Dice d : dice)
{
d.roll();
}
// keep a copy for returning
final List<Dice> result = new ArrayList<>(dice);
// empty out cup
dice.clear();
// return result
return result;
}
}
Everything that has to be returned
public void test()
{
try(
InputStream is = new FileInputStream(new File("foo.txt"));
OutputStream os = new OutputStream(new File("bar.txt"));
)
{
// your I/O code here
}
catch (final FileNotFoundException e)
{
// do special stuff for the exception case
}
// This is the virtual finally block.
// Java will auto-close "os" first
// "is" last
}
Always error handle completely
Make the right move at the right time
public void test()
{
final List<Person> persons = persons.stream().filter(p -> {
if (p.getGender() == Gender.MALE)
{
return true;
}
LocalDate now = LocalDate.now();
Duration age = Duration.between(p.getBirthDate(), now);
Duration adult = Duration.of(18, ChronoUnit.YEARS);
if (age.compareTo(adult) > 0)
{
return true;
}
return false;
}).map(p -> p.getFirstName() + " " + p.getLastName())
.collect(Collectors.toList());
}
Log properly and carefully
State and plan
/**
* This method sorts incoming data and is using up the max
* number of available CPU cores to be quick. The threads will
* be automatically started and stopped.
*
* Tests have shown that is scales well up to 10 cpus and any
* further CPU core up to 20 contribute about 80% more of the
* theoretical limit. Beyond that, more cpus don't contribute
* anything.
* No tests with more than 40 cpus have been performed.
*/
public SomeStream sort(SomeStream inputData)
{
}
Nothing can irritate you
Good code is efficient code
Let's talk about testing code
Just a quick rinse
Full topic was covered on Test Cases/Test Plans and Unit Testing.
Make sure your test is complete
Make sure your test is documented
/**
* Tests for the alternative implementation of ParseLong
* that is supposed to faster but does not handle all edge-case
* instead falls back to JDK based parsing.
*
* This class is holding all tests for com.foo.bar.ParseNumbers
*/
public class ParseNumbersTest
{
/**
* Verify constructior and that all constructors are
* private only.
*/
@Test
public void constructorsArePrivate();
/**
* Verify some standard good cases that are covered by
* this implementation. We are not measuring the performance
* difference, just verifiying correctness.
*/
@Test
public void parseLongs_StandardGoodCases();
/**
* Verify the fallback to JDK long parsing for
* negative numbers, because this is not covered
* by this alternative implementation, because we don't
* expect this in the input data.
*/
@Test
public void parseLongs_Fallback();
/**
* Everything that is not parseable also not as fallback
* should report a NumberFormatExcpeption
*/
@Test(expected = NumberFormatException.class)
public void parseLongs_ExceptionForUnparseable_NoData();
@Test(expected = NumberFormatException.class)
public void parseLongs_ExceptionForUnparseable_Null();
@Test(expected = NumberFormatException.class)
public void parseLongs_ExceptionForUnparseable_Letters();
}
Let's review what someone has written
What is a code review?
Code review is systematic examination of computer source code.
https://en.wikipedia.org/wiki/Code_review
Image by smitty42 under CC-BY-ND-2.0
Why and how
Quick reminder what to check