# Developer Guide

# Build requirements

# How to build

We use Gradle (opens new window) to build Keva. The following command will compile Keva, run tests and generate JARs:

$ ./gradlew --parallel build

# Setting up your IDE

You can import Keva into your IDE (IntelliJ IDEA (opens new window) or Eclipse (opens new window)) as a Gradle project.

IntelliJ IDEA is our primary IDE for developing Keva.

# Always make the build pass

Make sure your change does not break the build.

  • Run ./gradlew --parallel build locally.
  • It is likely that you'll encounter some Checkstyle or Javadoc errors. Please fix them because otherwise the build will be broken.

# Avoid redundancy

Avoid using redundant keywords. To list a few:

  • final method modifier in a final class
  • static or public modifier in an interface
  • public method modifier in a package-local or private class
  • private constructor modifier in an enum
  • field access prefixed with this. where unnecessary

# Avoid using lombok.val and lombok.var

Avoid using lombok.val and lombok.var in your code.

# Use public only when necessary

The classes, methods and fields that are not meant to be used by a user should not be public. Use the most restrictive modifier wherever possible, such as private, package-local and protected, so that static analysis tools can find dead code easily.

# Organize

Organize class members carefully for readability, using top-down approach. Although there's no absolute rule of thumb, it's usually like:

  • static fields
  • static methods
  • member fields
  • constructors
  • member methods
  • utility methods (both static and member)
  • inner classes

# Check null

Do explicit null-check on the parameters of user-facing public methods. Always use @lombok.NonNull to do a null-check.

import lombok.NonNull

@Override
public int process(@NonNull String text) {
    // ...
}

# Use @Nullable

Use @Nullable annotation for nullable parameters and return types.

# Avoid redundant null checks

Avoid unnecessary null-checks, including the hidden checks in Objects.hashCode() and Objects.equals().

public final class MyClass {
    private final String name;

    public MyClass(String name) {
        // We are sure 'name' is always non-null.
        this.name = requireNonNull(name, "name");
    }

    @Override
    public int hashCode() {
        // OK
        return name.hashCode();
        // Not OK
        return Objects.hash(name);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        ... usual type check ...
        // OK
        return name.equals(((MyClass) obj).name);
        // Not OK
        return Objects.equals(name, ((MyClass) obj).name);
    }
}

# Use meaningful exception messages

When raising an exception, specify meaningful message which gives an explicit clue about what went wrong.

switch (fileType) {
    case TXT: ... break;
    case XML: ... break;
    default:
        // Note that the exception message contains the offending value
        // as well as the expected values.
        throw new IllegalStateException(
                "unsupported file type: " + fileType +
                 " (expected: " + FileType.TXT + " or " + FileType.XML + ')');
}

# Validate

Do explicit validation on the parameters of user-facing public methods. When raising an exception, always specify the detailed message in the following format:

public void setValue(int value) {
    if (value < 0) {
        // Note that the exception message contains the offending value
        // as well as the expected value.
        throw new IllegalArgumentException("value: " + value + " (expected: >= 0)");
    }
}

# Prefer JDK API

Prefer using plain JDK API when the same behavior can be achieved with the same amount of code.

// Prefer A (JDK) - less indirection
Map<String, String> map = new HashMap<>();   // A (JDK)
Map<String, String> map = Maps.newHashMap(); // B (Guava)

// Prefer B (Guava) - simpler yet more efficient
List<String> list = Collections.unmodifiableList(  // A (JDK)
        otherList.stream().filter(...).collect(Collectors.toList()));
List<String> list = otherList.stream().filter(...) // B (Guava)
        .collect(toImmutableList());

# Prefer early-return style

Prefer 'early return' code style for readability.

// Great
public void doSomething(String value) {
    if (value == null) {
        return;
    }

    // Do the actual job
}

// Not great
public void doSomething(String value) {
    if (value != null) {
        // Do the actual job
    }
}

However, when the 'normal' execution path is very simple, this may also look beautiful:

public void doSomething(String value) {
    if (value != null) {
        return value.trim();
    } else {
        return null;
    }
}

# Prefer MoreObjects.toStringHelper()

Prefer MoreObjects.toStringHelper() to hand-written toString() implementation. However, consider writing hand-written or caching toString() implementation in performance-sensitive places.

# Think aesthetics

Do not insert an empty line that hurts code aesthetics.

// OK
if (...) {
    doSomething();
}

// Not OK
if (...) {
    doSomething();
                        // <-- Remove this extra line.
}

Similarly, do not use two or more consecutive empty lines.

// OK
public void a() { ... }

public void b() { ... }

// Not OK
public void a() { ... }

                        // <-- Remove this extra line.
public void b() { ... }

# Git flow

Follow the Git flow (opens new window)

For feature PR (to develop) use squash merge, for release PR (to master) use rebase merge, commit message merged to master/develop branch should be lowercase first word (e.g. "fix: some bug").

All pull requests must be passed to the code review and passed all the tests.