https://github.com/jeffgbutler/practical-functional-java
https://jeffgbutler.github.io/practical-functional-java/
Functional programming imposes discipline on assignment.
Uncle Bob in "Clean Architecture", page 23
In other words...immutability is the key concept in functional programming.
If you can only remember one thing from this workshop, then remember that writing pure functions will radically change, and improve, the way you write code.
public final class SimpleImmutableObject {
private final Integer id;
private final String description;
public SimpleImmutableObject(Integer id, String description) {
this.id = Objects.requireNonNull(id); // check for nulls!
this.description = Objects.requireNonNull(description);
}
public Integer getId() {
return id;
}
public String getDescription() {
return description;
}
}
SimpleImmutableObject myObject = new SimpleImmutableObject(3, "A Number");
public final class SimpleImmutableObjectWithBuilder {
private final Integer id;
private final String description;
private SimpleImmutableObjectWithBuilder(Builder builder) {
this.id = Objects.requireNonNull(builder.id);
this.description = Objects.requireNonNull(builder.description);
}
// getters...
public static class Builder {
private Integer id;
private String description;
public Builder withId(Integer id) {
this.id = id;
return this;
}
public Builder withDescription(String description) {
this.description = description;
return this;
}
public SimpleImmutableObjectWithBuilder build() {
return new SimpleImmutableObjectWithBuilder(this);
}
}
SimpleImmutableObjectWithBuilder myObject = new SimpleImmutableObjectWithBuilder.Builder()
.withId(3)
.withDescription("A Number")
.build();
The with* methods can be called in any order, or multiple times, or not at all. The constructor should do null checking for required attributes.
public final class SimpleImmutableObjectWithBuilder {
...
public static SimpleImmutableObjectWithBuilder of(Integer id, String description) {
return new Builder()
.withId(id)
.withDescription(description)
.build();
}
}
SimpleImmutableObjectWithBuilder myObject = SimpleImmutableObjectWithBuilder.of(3, "A Number");
These "of" convenience methods can make immutable objects easier to use. You could write several methods like this to handle common cases.
public class ImmutablePerson {
private String middleName;
...
private ImmutablePerson(Builder builder) {
....
middleName = builder.middleName; // no null check
}
...
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
public static class Builder {
private String middleName;
...
public Builder withMiddleName(String middleName) {
this.middleName = middleName;
return this;
}
...
}
}
public class ImmutablePerson {
private List<String> nickNames;
...
private SimpleImmutableObjectWithBuilder(Builder builder) {
....
nickNames = Objects.requireNonNull(builder.nickNames);
}
...
public Stream<String> nickNames() {
return nickNames.stream();
}
public static class Builder {
private List<String> nickNames = new ArrayList<>();
...
public Builder withNickName(String nickName) {
nickNames.add(nickName);
return this;
}
public Builder withNickNames(List<String> nickNames) {
this.nickNames.addAll(nickNames);
return this;
}
...
}
}
public class ImmutablePerson {
...
// copy constructor - for the with*** methods
private ImmutablePerson(ImmutablePerson other) {
firstName = other.firstName;
middleName = other.middleName;
lastName = other.lastName;
nickNames = new ArrayList<>(other.nickNames);
}
public ImmutablePerson withMiddleName(String middleName) {
ImmutablePerson copy = new ImmutablePerson(this);
copy.middleName = middleName;
return copy;
}
}
ImmutablePerson person = new ImmutablePerson.Builder()
.withFirstName("Fred")
.withLastName("Flintstone")
.build();
person = person.withMiddleName("Farnsworth");
The with* methods create new instances. So the old person object still exists and is unchanged.
@Test
public void testImpureFunction() {
List<ImmutablePerson> allPeople = new ArrayList<>();
addTheFlintstones(allPeople);
addTheRubbles(allPeople);
assertThat(allPeople.size()).isEqualTo(4);
assertThat(allPeople.get(1).getFirstName()).isEqualTo("Wilma");
assertThat(allPeople.get(3).getFirstName()).isEqualTo("Betty");
}
private void addTheFlintstones(List<ImmutablePerson> people) {
people.add(ImmutablePerson.of("Fred", "Flintstone"));
people.add(ImmutablePerson.of("Wilma", "Flintstone"));
}
private void addTheRubbles(List<ImmutablePerson> people) {
people.add(ImmutablePerson.of("Barney", "Rubble"));
people.add(ImmutablePerson.of("Betty", "Rubble"));
}
@Test
public void testPureFunction() {
List<ImmutablePerson> allPeople = new ArrayList<>();
allPeople.addAll(getTheFlintstones());
allPeople.addAll(getTheRubbles());
assertThat(allPeople.size()).isEqualTo(4);
}
private List<ImmutablePerson> getTheFlintstones() {
List<ImmutablePerson> flintstones = new ArrayList<>();
flintstones.add(ImmutablePerson.of("Fred", "Flintstone"));
flintstones.add(ImmutablePerson.of("Wilma", "Flintstone"));
return flintstones;
}
private List<ImmutablePerson> getTheRubbles() {
List<ImmutablePerson> rubbles = new ArrayList<>();
rubbles.add(ImmutablePerson.of("Barney", "Rubble"));
rubbles.add(ImmutablePerson.of("Betty", "Rubble"));
return rubbles;
}
@Test
public void testPureFunction() {
List<ImmutablePerson> allPeople = Stream.of(getTheFlintstones(), getTheRubbles())
.flatMap(Function.identity())
.collect(Collectors.toList());
assertThat(allPeople.size()).isEqualTo(4);
assertThat(allPeople.get(1).getFirstName()).isEqualTo("Wilma");
assertThat(allPeople.get(3).getFirstName()).isEqualTo("Betty");
}
private Stream<ImmutablePerson> getTheFlintstones() {
return Stream.of(ImmutablePerson.of("Fred", "Flintstone"),
ImmutablePerson.of("Wilma", "Flintstone"));
}
private Stream<ImmutablePerson> getTheRubbles() {
return Stream.of(ImmutablePerson.of("Barney", "Rubble"),
ImmutablePerson.of("Betty", "Rubble"));
}
This is hard to explain - you'll have to take my word on it... When you program with immutable objects and pure functions it changes the way you think.