Groovy Developer Manifesto Explained

Vladimír Oraný

Test Facilitator @ Agorapulse

@musketyr

What kind of Groovy developer you are?

Groovy Hacker

class Whatever {

    def doSomeStuff(a, b, c) {
        def iDontCare = c
        if (iDontCare < a) {
            iDontCare = "I don't care"
        } else if (iDontCare > b) {
            iDontCare = "I don't care anyway"
        }
        return "$iDontCare about other developers".bytes
    }

}

Groovy Hacker

@Unroll
void 'given a=#a and b=#b we should get result'() {
    expect:
        new String(new Whatever().doSomeStuff(a, b, c)) == result
    where:
        a  | b  | c | result
        0  | 0  | 0 | "0 about other developers"
        1  | 2  | 0 | "I don't care about other developers"
        -1 | -2 | 0 | "I don't care anyway about other developers"
}

Groovy Craftsman

@CompileStatic
class Ranges {

    private Ranges() { }

    static boolean inRange(int min, int max, int number) {
        if (min > max) {
            throw new IllegalArgumentException(
                    "Min must be lower or equal to max! min=$min, max=$max"
            )
        }
        if (number < min || number > max) {
            return false
        }
        return true
    }

}

Groovy Craftsman

@Unroll
void '#number #expected in range (#min, #max)'() {
    expect:
        Ranges.inRange(min, max, number) == result
    where:
        min | max  | number | result
        0   | 0    | 0      | true
        1   | 2    | 0      | false
        -2  | -2   | 0      | false

        expected = result ? 'is' : 'is not'
}

Groovy Developer Manifesto

1. I will write code in Java unless writing in Groovy would add some value; removing semicolon does not add value

Enums

enum SessionType {

    REGULAR_TALK,
    LIGHTNING_TALK,
    WORKSHOP

}
public enum SessionType {

    REGULAR_TALK,
    LIGHTNING_TALK,
    WORKSHOP;

}

Interfaces

interface Attendee {

    void voteForSession(String sessionName, int points)
    void voteForSession(String sessionName, int points, String remarks)

}
public interface Attendee {

    default void voteForSession(String sessionName, int points) {
        voteForSession(sessionName, points, null);
    };

    void voteForSession(String sessionName, int points, String remarks);

}

Functional Programming

Iterable<Session> filterSession(Iterable<Session> all, SessionType type) {
    return all.findAll { Session session ->
        session.type == type
    }
}
Iterable<Session> filterSession(Iterable<Session> all, SessionType type) {
    return StreamSupport.stream(all.spliterator(), true)
            .filter(s -> type.equals(s.getType()))
            .collect(Collectors.toList());
}

Data Classes

@Canonical
@CompileStatic
class Session {

    final String name
    final SessionType type

    Session(String name, SessionType type) {
        this.name = name
        this.type = type
    }

}

Data Classes

public class Session {

    private final String name;
    private final SessionType type;

    public Session(String name, SessionType type) {
        this.name = name;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public SessionType getType() {
        return type;
    }

    @Override
    public boolean equals(Object o) { ... }

    @Override
    public int hashCode() { ... }

    @Override
    public String toString() { ... }
}

2. I will follow Effective Java rules even when I am writing Groovy code

Prefer Immutability

@ToString
@EqualsAndHashCode
class Session {

    final String name
    final String description

    Sesssion(String name, String description) {
        this.name = name
        this.description = description
    }

    // ...

}
@CompileStatic
class Conference implements Event {

    private static final Map<String, Conference> CONFERENCES = [:]

    static Conference create(String name) {
        CONFERENCES.computeIfAbsent(name) {
            new Conference(name)
        }
    }

    private final String  name;
    private final List<Session> sessions = []

    private Conference(String name) {
        this.name = name
    }

    @Override
    void addSession(Session session) {
        sessions.add(session)
    }

    @Override
    Iterable<Session> getSessions() {
        return sessions.asImmutable()
    }

    @Override
    Iterator<Session> iterator() {
        return getSessions().iterator()
    }
    
}

3. I will use @CompileStatic annotation by default unless I really need dynamic language features

Static Compilation

@CompileStatic
Iterable<Session> filterSession(Iterable<Session> all, SessionType type) {
    return all.findAll { Session session ->
        session.type == type
    }
}

Dynamic Structures

@CompileStatic
class StaticVsDynamic {

    String readSomething(Map<String, Object> map) {
        map.something
    }

    @CompileDynamic
    String readSomethingElse(Node node) {
        node.something.else
    }

}

Domain Specific Languages

@CompileStatic
class DomainSpecificLanguages {

    void buildSpreadsheet(SpreadsheetBuilder builder) {
        builder.build {
            sheet('Sample') {
                row {
                    cell 'A'
                }
                row {
                    cell 1
                }
            }
        }
    }

}

4. I will never ever use def keyword

Groovy Hacker

class Whatever {

    def doSomeStuff(a, b, c) {
        def iDontCare = c
        if (iDontCare < a) {
            iDontCare = "I don't care"
        } else if (iDontCare > b) {
            iDontCare = "I don't care anyway"
        }
        return "$iDontCare about other developers".bytes
    }

}

No Guarantee

import groovy.transform.CompileStatic

@CompileStatic
static String aMethod() {
    def r = 'a string'
    System.out.println "result is now ${r.getClass()}"
    // imagine plenty of code
    r = [trash: 'bin']
    // imagine plenty of code
    System.out.println "result is now ${r.getClass()}"
    return r
}

println "r: ${aMethod()}"

5. I will never use Map as an excuse not to create meaningful class

6. I will use @NamedParam, @NamedDelegate and @NamedVariant instead of raw Map method parameters

Map instead of Object

Iterable<Session> filterSession(
    Iterable<Session> all, 
    SessionType type, 
    Map options
) {
    Collection<Session> result = all.findAll { Session session ->
        session.type == type
    }

    if (options.offset) {
        result = result.drop(options.offset as Integer)
    }

    if (options.limit) {
        result = result.take(options.limit as Integer)
    }

    return result
}
repository.filterSession(sessions, type, [offset: 10])

Object instead of Map

Iterable<Session> filterSession(
    Iterable<Session> all, 
    SessionType type, 
    PaginationOptions options
) {
    Collection result = all.findAll { Session session ->
        session.type == type
    }
    return result.drop(options.offset).take(options.limit)
}
repository.filterSession(sessions, type, withOffset(10))

Named Variant

@NamedVariant
Iterable<Session> filterSession2(
        Iterable<Session> all, 
        SessionType type, 
        @NamedDelegate PaginationOptions options
) {
    Collection result = all.findAll { Session session ->
        session.type == type
    }
    return result.drop(options.offset).take(options.limit)
}
repository.filterSession2(sessions, type, offset: 10)

7. I will never use raw Closure without a type parameter

8. I will use @ClosureParams for every Closure method parameter

9. I will use @DelegatesTo for every Closure method parameter with altered delegate

Closure instead of Map/Object

repository.filterSession(sessions, type) {
    offset 50
    limit 25
}

Closure instead of Map/Object

Iterable<Session> filterSession(
        Iterable<Session> all,
        SessionType type,
        @DelegatesTo(
                value = PaginationOptions.Builder, 
                strategy = Closure.DELEGATE_FIRST
        )
        @ClosureParams(
                value = SimpleType, 
                options = 'manifesto.groovy.PaginationOptions.Builder'
        )
                Closure<PaginationOptions.Builder> paginationOptions
) {
    Collection result = all.findAll { Session session ->
        session.type == type
    }
    PaginationOptions.Builder builder = new PaginationOptions.Builder()
    builder.with paginationOptions
    PaginationOptions options = builder.build()
    return result.drop(options.offset).take(options.limit)
}

10. CodeNarc is my best friend forever

Thank You

Groovy Developer Manifesto Explained

By musketyr

Groovy Developer Manifesto Explained

  • 1,814