From object oriented to functional domain modeling
This document discusses moving from an object-oriented approach to a functional approach for domain modeling. It provides examples of modeling a bank account and salary calculator in both OOP and FP styles. Some key benefits of the functional approach highlighted include immutability, avoiding side effects, handling errors through monads instead of exceptions, and composing functions together through currying and composition. Overall the document advocates that while OOP and FP both have merits, modeling as functions can provide advantages in terms of understandability, testability, and extensibility of the code.
Reassigning a variable
Modifyinga data structure in place
Setting a field on an object
Throwing an exception or halting with an error
Printing to the console
Reading user input
Reading from or writing to a file
Drawing on the screen
A program created using only pure functions
What is a functional program?
No (observable) side effects allowed like:
Functional programming is a restriction on how we write
programs, but not on what they can do
}
}
avoidable
deferrable
3.
OOP makes codeunderstandable
by encapsulating moving parts
FP makes code understandable
by minimizing moving parts
- Michael Feathers
OOP vs FP
4.
Why Immutability?
➢ Immutableobjects are often easier to use.
Compare java.util.Calendar (mutable)
with java.time.LocalDate (immutable)
➢ Implementing an immutable object is often
easier, as there is less that can go wrong
➢ Immutable objects reduce the number of
possible interactions between different
parts of the program
➢ Immutable objects can be safely shared
between multiple threads
5.
A quick premise
Itis not only black or white ...
Object
Oriented
Programming
Functional
Programming
6.
A quick premise
Itis not only black or white ...
… there are (at least)
50 shades of gray in the middle
Object
Oriented
Programming
Functional
Programming
7.
The OOP/FP dualism- OOP
public class Bird { }
public class Cat {
private Bird catch;
private boolean full;
public void capture(Bird bird) {
catch = bird;
}
public void eat() {
full = true;
catch = null;
}
}
Cat cat = new Cat();
Bird bird = new Bird();
cat.capture(bird);
cat.eat();
The story
8.
The OOP/FP dualism- FP
public class Bird { }
public class Cat {
public CatWithCatch capture(Bird bird) { return new CatWithCatch(bird); }
}
public class CatWithCatch {
private final Bird catch;
public CatWithCatch(Bird bird) { catch = bird; }
public FullCat eat() { return new FullCat(); }
}
public class FullCat { }
BiFunction<Cat, Bird, FullCat> story =
((BiFunction<Cat, Bird, CatWithCatch>)Cat::capture)
.andThen(CatWithCatch::eat);
FullCat fullCat = story.apply( new Cat(), new Bird() );
Immutability
Emphasis on verbs
instead of names
No need to test internal state: correctness enforced by the compiler
More expressive
use of type system
9.
From Object toFunction centric
BiFunction<Cat, Bird, CatWithCatch> capture =
(cat, bird) -> cat.capture(bird);
Function<CatWithCatch, FullCat> eat =
CatWithCatch::eat;
BiFunction<Cat, Bird, FullCat> story = capture.andThen(eat);
Functions
compose
better than
objects
10.
A composable functionalAPI
public class API {
public static Cart buy(List<Item> items) { ... }
public static Order order(Cart cart) { ... }
public static Delivery deliver(Order order) { ... }
}
Function<Delivery, List<Item>> oneClickBuy =
((Function<Cart, List<Item>>) API::buy)
.andThen(API::order)
.andThen(API::deliver);
Delivery d = oneClickBuy.apply(asList(book, watch, phone));
11.
public static <T>void sort(List<T> list,
Comparator<? super T> c)
Essence of Functional Programming
Data and behaviors are the same thing!
Data
Behaviors
Collections.sort(persons,
(p1, p2) -> p1.getAge() – p2.getAge())
12.
Higher-order functions
Are theyso mind-blowing?
… but one of the most influent sw engineering
book is almost completely dedicated to them
public interface Converter{
double convert(double value);
}
A strategy pattern Converter
public abstract class AbstractConverter implements Converter {
public double convert(double value) {
return value * getConversionRate();
}
public abstract double getConversionRate();
}
public class Mi2KmConverter extends AbstractConverter {
public double getConversionRate() { return 1.609; }
}
public class Ou2GrConverter extends AbstractConverter {
public double getConversionRate() { return 28.345; }
}
15.
public List<Double> convertValues(List<Double>values,
Converter converter) {
List<Double> convertedValues = new ArrayList<Double>();
for (double value : values) {
convertedValues.add(converter.convert(value));
}
return convertedValues;
}
Using the Converter
List<Double> values = Arrays.asList(10, 20, 50);
List<Double> convertedDistances =
convertValues(values, new Mi2KmConverter());
List<Double> convertedWeights =
convertValues(values, new Ou2GrConverter());
16.
A functional Converter
publicclass Converter implements
ExtendedBiFunction<Double, Double, Double> {
@Override
public Double apply(Double conversionRate, Double value) {
return conversionRate * value;
}
}
@FunctionalInterface
public interface ExtendedBiFunction<T, U, R> extends
BiFunction<T, U, R> {
default Function<U, R> curry1(T t) {
return u -> apply(t, u);
}
default Function<T, R> curry2(U u) {
return t -> apply(t, u);
}
}
Function Composition
Celsius →Fahrenheit : F = C * 9/5 + 32
Converter
value
rate=9/5 andThen
n -> n+32
result
Celsius2FarenheitConverter
Function<Double, Double> c2fConverter =
new Converter().curry1(9.0/5)
.andThen(n -> n + 32);
19.
More Function Composition
@FunctionalInterface
publicinterface ExtendedBiFunction<T, U, R> extends
BiFunction<T, U, R> {
default <V> ExtendedBiFunction<V, U, R>
compose1(Function<? super V, ? extends T> before) {
return (v, u) -> apply(before.apply(v), u);
}
default <V> ExtendedBiFunction<T, V, R>
compose2(Function<? super V, ? extends U> before) {
return (t, v) -> apply(t, before.apply(v));
}
}
default <V> Function<V, R>
compose(Function<? super V, ? extends T> before) {
return (V v) -> apply(before.apply(v));
}
20.
More Function Composition
Fahrenheit→ Celsius : C = (F - 32) * 5/9
Converter
rate=5/9
value
n -> n-32
result
Farenheit2CelsiusConverter
Function<Double, Double> f2cConverter =
new Converter().compose2((Double n) -> n - 32)
.curry1(5.0/9);
Functions are building blocks to create other functions
compose2
21.
public class SalaryCalculator{
public double plusAllowance(double d) { return d * 1.2; }
public double plusBonus(double d) { return d * 1.1; }
public double plusTax(double d) { return d * 0.7; }
public double plusSurcharge(double d) { return d * 0.9; }
public double calculate(double basic, boolean... bs) {
double salary = basic;
if (bs[0]) salary = plusAllowance(salary);
if (bs[1]) salary = plusBonus(salary);
if (bs[2]) salary = plusTax(salary);
if (bs[3]) salary = plusSurcharge(salary);
return salary;
}
}
A Salary Calculator
22.
double basicBobSalary =...;
double netBobSalary =
new SalaryCalculator().calculate( basicBobSalary,
false, // allowance
true, // bonus
true, // tax
false // surcharge
);
Using the Salary Calculator
How can I
remember the
right sequence?
23.
public class SalaryCalculatorBuilderextends SalaryCalculator {
private boolean hasAllowance;
private boolean hasBonus;
private boolean hasTax;
private boolean hasSurcharge;
public SalaryCalculatorFactory withAllowance() {
hasAllowance = true;
return this;
}
// ... more withX() methods
public double calculate(double basic) {
return calculate( basic, hasAllowance, hasBonus,
hasTax, hasSurcharge );
}
}
A Salary Calculator Builder
24.
double basicBobSalary =...;
double netBobSalary = new SalaryCalculatorBuilder()
.withBonus()
.withTax()
.calculate( basicBobSalary );
Using the Salary Calculator Factory
Better,
but what if I have to
add another function?
25.
public final classSalaryRules {
private SalaryRules() { }
public static double allowance(double d) { return d * 1.2; }
public static double bonus(double d) { return d * 1.1; }
public static double tax(double d) { return d * 0.7; }
public static double surcharge(double d) { return d * 0.9; }
}
Isolating Salary Rules
26.
public class SalaryCalculator{
private final List<Function<Double, Double>> fs =
new ArrayList<>();
public SalaryCalculator with(Function<Double, Double> f) {
fs.add(f);
return this;
}
public double calculate(double basic) {
return fs.stream()
.reduce( Function.identity(), Function::andThen )
.apply( basic );
}
}
A Functional Salary Calculator
27.
double basicBobSalary =...;
double netBobSalary = new SalaryCalculator()
.with( SalaryRules::bonus )
.with( SalaryRules::tax )
.calculate( basicBobSalary );
Using the Functional Salary Calculator
➢ No need of any special
builder to improve readability
28.
double basicBobSalary =...;
double netBobSalary = new SalaryCalculator()
.with( SalaryRules::bonus )
.with( SalaryRules::tax )
.with( s -> s * 0.95 ) // regional tax
.calculate( basicBobSalary );
Using the Functional Salary Calculator
➢ No need of any special
builder to improve readability
➢ Extensibility comes for free
29.
public class SalaryCalculator{
private final Function<Double, Double> calc;
public SalaryCalculator() { this( Function::identity() ); }
private SalaryCalculator(Function<Double, Double> calc) {
this.calc = calc;
}
public SalaryCalculator with(Function<Double, Double> f) {
return new SalaryCalculator( calc.andThen(f) );
}
public double calculate(double basic) {
return calc.apply( basic );
}
}
A (better) Functional Salary Calculator
30.
JΛVΛSLΛNG
A functional Libraryfor Java 8
Immutable Collections
Pattern Matching
Failure Handling
Tuple3<Person, Account, Building>
final A result = Try.of(() -> bunchOfWork())
.recover(x -> Match
.caze((Exception_1 e) -> ...)
.caze((Exception_2 e) -> ...)
.caze((Exception_n e) -> ...)
.apply(x))
.orElse(other);
31.
Let's have acoffee break ...
public class Cafe {
public Coffee buyCoffee(CreditCard cc) {
Coffee cup = new Coffee();
cc.charge( cup.getPrice() );
return cup;
}
public List<Coffee> buyCoffees(CreditCard cc, int n) {
return Stream.generate( () -> buyCoffee( cc ) )
.limit( n )
.collect( toList() );
}
}
Side-effect
How can we test this without
contacting the bank or using a mock?
How can reuse that method to
buy more coffees without
charging the card multiple times?
32.
… but pleasea side-effect free one
import javaslang.Tuple2;
import javaslang.collection.Stream;
public class Cafe {
public Tuple2<Coffee, Charge> buyCoffee(CreditCard cc) {
Coffee cup = new Coffee();
return new Tuple2<>(cup, new Charge(cc, cup.getPrice()));
}
public Tuple2<List<Coffee>, Charge> buyCoffees(CreditCard cc, int n) {
Tuple2<Stream<Coffee>, Stream<Charge>> purchases =
Stream.gen( () -> buyCoffee( cc ) )
.subsequence( 0, n )
.unzip( identity() );
return new Tuple2<>( purchases._1.toJavaList(),
purchases._2.foldLeft( new Charge( cc, 0 ),
Charge::add) );
}
}
public Charge add(Charge other) {
if (cc == other.cc)
return new Charge(cc, amount + other.amount);
else
throw new RuntimeException(
"Can't combine charges to different cards");
}
33.
Error handling withExceptions?
➢
Often abused, especially for flow
control
➢
Checked Exceptions harm API
extensibility/modificability
➢
They also plays very badly with
lambdas syntax
➢
Not composable: in presence of
multiple errors only the first one is
reported
➢
In the end just a GLORIFIED
MULTILEVEL GOTO
34.
Error handling
The functionalalternatives
Either<Exception, Value>
➢
The functional way of returning a value which can actually be one
of two values: the error/exception (Left) or the correct value (Right)
Validation<List<Exception>, Value>
➢
Composable: can accumulate multiple errors
Try<Value>
➢
Signal that the required computation may eventually fail
35.
A OOP BankAccount...
public class Balance {
final BigDecimal amount;
public Balance( BigDecimal amount ) { this.amount = amount; }
}
public class Account {
private final String owner;
private final String number;
private Balance balance = new Balance(BigDecimal.ZERO);
public Account( String owner, String number ) {
this.owner = owner;
this.number = number;
}
public void credit(BigDecimal value) {
balance = new Balance( balance.amount.add( value ) );
}
public void debit(BigDecimal value) throws InsufficientBalanceException {
if (balance.amount.compareTo( value ) < 0)
throw new InsufficientBalanceException();
balance = new Balance( balance.amount.subtract( value ) );
}
}
Mutability
Error handling
using Exception
36.
… and howwe can use it
Account a = new Account("Alice", "123");
Account b = new Account("Bob", "456");
Account c = new Account("Charlie", "789");
List<Account> unpaid = new ArrayList<>();
for (Account account : Arrays.asList(a, b, c)) {
try {
account.debit( new BigDecimal( 100.00 ) );
} catch (InsufficientBalanceException e) {
unpaid.add(account);
}
}
List<Account> unpaid = new ArrayList<>();
Stream.of(a, b, c).forEach( account -> {
try {
account.debit( new BigDecimal( 100.00 ) );
} catch (InsufficientBalanceException e) {
unpaid.add(account);
}
} );
Mutation of enclosing scope
Cannot use a parallel Stream
Ugly syntax
37.
Error handling withTry monad
public interface Try<A> {
<B> Try<B> map(Function<A, B> f);
<B> Try<B> flatMap(Function<A, Try<B>> f);
boolean isFailure();
}
public Success<A> implements Try<A> {
private final A value;
public Success(A value) { this.value = value; }
public boolean isFailure() { return false; }
public <B> Try<B> map(Function<A, B> f) {
return new Success<>(f.apply(value));
}
public <B> Try<B> flatMap(Function<A, Try<B>> f) {
return f.apply(value);
}
}
public Failure<A> implements Try<T> {
private final Object error;
public Failure(Object error) { this.error = error; }
public boolean isFailure() { return false; }
public <B> Try<B> map(Function<A, B> f) { return (Failure<B>)this; }
public <B> Try<B> flatMap(Function<A, Try<B>> f) { return (Failure<B>)this; }
}
map defines monad's policy
for function application
flatMap defines monad's policy
for monads composition
38.
A functional BankAccount...
public class Account {
private final String owner;
private final String number;
private final Balance balance;
public Account( String owner, String number, Balance balance ) {
this.owner = owner;
this.number = number;
this.balance = balance;
}
public Account credit(BigDecimal value) {
return new Account( owner, number,
new Balance( balance.amount.add( value ) ) );
}
public Try<Account> debit(BigDecimal value) {
if (balance.amount.compareTo( value ) < 0)
return new Failure<>( new InsufficientBalanceError() );
return new Success<>(
new Account( owner, number,
new Balance( balance.amount.subtract( value ) ) ) );
}
}
Immutable
Error handling
without Exceptions
39.
… and howwe can use it
Account a = new Account("Alice", "123");
Account b = new Account("Bob", "456");
Account c = new Account("Charlie", "789");
List<Account> unpaid =
Stream.of( a, b, c )
.map( account ->
new Tuple2<>( account,
account.debit( new BigDecimal( 100.00 ) ) ) )
.filter( t -> t._2.isFailure() )
.map( t -> t._1 )
.collect( toList() );
List<Account> unpaid =
Stream.of( a, b, c )
.filter( account ->
account.debit( new BigDecimal( 100.00 ) )
.isFailure() )
.collect( toList() );
40.
From Methods toFunctions
public class BankService {
public static Try<Account> open(String owner, String number,
BigDecimal balance) {
if (initialBalance.compareTo( BigDecimal.ZERO ) < 0)
return new Failure<>( new InsufficientBalanceError() );
return new Success<>( new Account( owner, number,
new Balance( balance ) ) );
}
public static Account credit(Account account, BigDecimal value) {
return new Account( account.owner, account.number,
new Balance( account.balance.amount.add( value ) ) );
}
public static Try<Account> debit(Account account, BigDecimal value) {
if (account.balance.amount.compareTo( value ) < 0)
return new Failure<>( new InsufficientBalanceError() );
return new Success<>(
new Account( account.owner, account.number,
new Balance( account.balance.amount.subtract( value ) ) ) );
}
}
41.
Decoupling state andbehavior
import static BankService.*
Try<Account> account =
open( "Alice", "123", new BigDecimal( 100.00 ) )
.map( acc -> credit( acc, new BigDecimal( 200.00 ) ) )
.map( acc -> credit( acc, new BigDecimal( 300.00 ) ) )
.flatMap( acc -> debit( acc, new BigDecimal( 400.00 ) ) );
The object-oriented paradigm couples state and behavior
Functional programming decouples them
42.
… but Ineed a BankConnection!
What about dependency injection?
43.
A naïve solution
publicclass BankService {
public static Try<Account> open(String owner, String number,
BigDecimal balance, BankConnection bankConnection) {
...
}
public static Account credit(Account account, BigDecimal value,
BankConnection bankConnection) {
...
}
public static Try<Account> debit(Account account, BigDecimal value,
BankConnection bankConnection) {
...
}
}
BankConnection bconn = new BankConnection();
Try<Account> account =
open( "Alice", "123", new BigDecimal( 100.00 ), bconn )
.map( acc -> credit( acc, new BigDecimal( 200.00 ), bconn ) )
.map( acc -> credit( acc, new BigDecimal( 300.00 ), bconn ) )
.flatMap( acc -> debit( acc, new BigDecimal( 400.00 ), bconn ) );
Necessary to create the
BankConnection in advance ...
… and pass it to all methods
Introducing the Readermonad ...
public class Reader<R, A> {
private final Function<R, A> run;
public Reader( Function<R, A> run ) {
this.run = run;
}
public <B> Reader<R, B> map(Function<A, B> f) {
return new Reader<>((R r) -> f.apply( apply( r ) ));
}
public <B> Reader<R, B> flatMap(Function<A, Reader<R, B>> f) {
return new Reader<>((R r) -> f.apply( apply( r ) ).apply( r ));
}
public A apply(R r) {
return run.apply( r );
}
}
The reader monad provides a computation environment
46.
… and combiningit with Try
public class TryReader<R, A> {
private final Function<R, Try<A>> run;
public TryReader( Function<R, Try<A>> run ) {
this.run = run;
}
public <B> TryReader<R, B> map(Function<A, B> f) {
return new TryReader<R, B>((R r) -> apply( r )
.map( a -> f.apply( a ) ));
}
public <B> TryReader<R, B> mapReader(Function<A, Reader<R, B>> f) {
return new TryReader<R, B>((R r) -> apply( r )
.map( a -> f.apply( a ).apply( r ) ));
}
public <B> TryReader<R, B> flatMap(Function<A, TryReader<R, B>> f) {
return new TryReader<R, B>((R r) -> apply( r )
.flatMap( a -> f.apply( a ).apply( r ) ));
}
public Try<A> apply(R r) {
return run.apply( r );
}
}
47.
A more user-friendlyAPI
public class BankService {
public static TryReader<BankConnection, Account>
open(String owner, String number, BigDecimal balance) {
return new TryReader<>( (BankConnection bankConnection) -> ... )
}
public static Reader<BankConnection, Account>
credit(Account account, BigDecimal value) {
return new Reader<>( (BankConnection bankConnection) -> ... )
}
public static TryReader<BankConnection, Account>
debit(Account account, BigDecimal value) {
return new TryReader<>( (BankConnection bankConnection) -> ... )
}
}
TryReader<BankConnection, Account> reader =
open( "Alice", "123", new BigDecimal( 100.00 ) )
.mapReader( acc -> credit( acc, new BigDecimal( 200.00 ) ) )
.mapReader( acc -> credit( acc, new BigDecimal( 300.00 ) ) )
.flatMap( acc -> debit( acc, new BigDecimal( 400.00 ) ) );
Try<Account> account = reader.apply( new BankConnection() );