Refactoring Legacy Code using Functional Programming
Sebastián Estrella
About me
Showtime!
Original Code Snippet
Table table = new Table();
for (User user : users) {
if (user.isEnabled()) {
Row row = new Row();
row.addColumn(new Column(user.fullName());
String role = user.isAdmin() ? "Admin" : "Member";
row.addColumn(new Column(role));
table.addRow(row);
}
}
table.render();
The Magical Number Seven, Plus or Minus Two
Separation of Concerns
Table table = new Table();
List<User> enabledUsers = new ArrayList<User>();
for (User user : users) {
if (user.isEnabled()) {
enabledUsers.add(user);
}
}
for (User user : enabledUsers) {
Row row = new Row();
row.addColumn(new Column(user.fullName());
String role = user.isAdmin() ? "Admin" : "Member";
row.addColumn(new Column(role));
table.addRow(row);
}
table.render();
Transformation
List<User> enabledUsers = new ArrayList<User>();
for (User user : users) {
if (user.isEnabled()) {
enabledUsers.add(user);
}
}
List<Row> rows = new ArrayList<Row>();
for (User user : enabledUsers) {
Row row = new Row();
row.addColumn(new Column(user.fullName());
String role = user.isAdmin() ? "Admin" : "Member";
row.addColumn(new Column(role));
rows.add(row);
}
Table table = new Table();
table.addRows(rows);
table.render();
Improvements
-
Readability
-
Testability
-
Clear code boundaries
Let's do some real refactoring!
Refactoring JDK 8+
Functional Programming
- First-class functions
- Higher-order functions
- Composition
Lambda Expressions
- Reference a function
- Pass a function as an argument
- Return a function
- Assign a function to a variable
Wholemeal Programming
Filter
List<User> enabledUsers = new ArrayList<User>();
for (User user : users) {
if (user.isEnabled()) {
enabledUsers.add(user);
}
}
List<User> enabledUsers = users.stream()
.filter(user -> user.isEnabled())
.collect(Collectors.toList());
Map
List<Row> rows = new ArrayList<Row>();
for (User user : enabledUsers) {
Row row = new Row();
row.addColumn(new Column(user.fullName());
String role = user.isAdmin() ? "Admin" : "Member";
row.addColumn(new Column(role));
rows.add(row);
}
List<Row> rows = enabledUsers.stream()
.map(user -> {
Row row = new Row();
row.addColumn(new Column(user.fullName());
String role = user.isAdmin() ? "Admin" : "Member";
row.addColumn(new Column(role));
rows.add(row);
})
.collect(Collectors.toList());
Composition
List<Row> rows = users.stream()
.filter(user -> user.isEnabled())
.map(user -> {
Row row = new Row();
row.addColumn(new Column(user.fullName());
String role = user.isAdmin() ? "Admin" : "Member";
row.addColumn(new Column(role));
rows.add(row);
})
.collect(Collectors.toList());
Table table = new Table();
table.addRows(rows);
table.render();
What about legacy code?
Handmade Higher-Order Functions
...but first!
JDK 5 | JDK 6 | JDK 8+ | |
---|---|---|---|
Interfaces | x | x | x |
Classes | x | x | x |
Anonymous Classes | x | x | x |
Generics | x | x | x |
Lambda Expressions | x |
Supported Features
Swing
Anonymous Classes
JButton submitButton = new JButton("Submit");
submitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// Do something
}
});
Filter - Implementation
interface Filter<A> {
boolean filter(A element);
}
<A> List<A> filter(Filter<A> predicate, List<A> list) {
List<A> result = new ArrayList<A>();
for (A element : list) {
if (predicate.filter(element)) {
result.add(element);
}
}
return result;
}
Filter - Usage
Filter<User> isEnabled = new Filter<User>() {
public boolean filter(User user) {
return user.isEnabled();
}
};
List<User> enabledUsers = filter(isEnabled, users);
Map - Implementation
interface Mapper<A, B> {
B map(A element);
}
<A, B> List<B> map(Mapper<A, B> mapper, List<A> list) {
List<B> result = new ArrayList<B>();
for (A element : list) {
result.add(mapper.map(element));
}
return result;
}
Map - Usage
List<Row> rows = map(new Mapper<User, Row>() {
public Row map(User user) {
Row row = new Row();
row.addColumn(new Column(user.fullName());
String role = user.isAdmin() ? "Admin" : "Member";
row.addColumn(new Column(role));
return row;
})
}, filter(new Filter<User>() {
public boolean filter(User user) {
return user.isEnabled();
}
}, users));
Generalizing
interface Filter<A> {
boolean filter(A element);
}
interface Mapper<A, B> {
B map(A element);
}
interface Filter<A> extends Function<A, Boolean> {
}
interface Function<A, B> {
B apply(A element);
}
Taking a deep look...
Generated Bytecode
Are these two code snippets equivalent?
public class FilterTest {
public int[] lambdaExpression(int[] numbers) {
IntPredicate isEven = x -> x % 2 == 0;
return Arrays.stream(numbers).filter(isEven).toArray();
}
}
public class FilterTest {
public int[] anonymousClass(int[] numbers) {
IntPredicate isEven = new IntPredicate() {
public boolean test(int x) {
return x % 2 == 0;
}
};
return Arrays.stream(numbers).filter(isEven).toArray();
}
}
Disassembling Code
public class FilterTest {
public int[] lambdaExpression(int[]);
private static boolean lambda$lambdaExpression$0(int);
}
public class FilterTest {
public int[] anonymousClass(int[]);
}
public class FilterTest$1 {
public boolean test(int);
}
Invoke Dynamic
- new // class FilterTest$1
- invokespecial // Method FilterTest$1."<init>":(LFilterTest;)V
+ invokedynamic // InvokeDynamic #0:test:()Ljava/util/function/IntPredicate;
invokestatic // Method java/util/Arrays.stream:([I)Ljava/util/stream/IntStream;
invokeinterface // InterfaceMethod java/util/stream/IntStream.filter:(Ljava/util/function/IntPredicate;)Ljava/util/stream/IntStream
invokeinterface // InterfaceMethod java/util/stream/IntStream.toArray:()[I
What about the performance?
Benchmark Results
Benchmark | Mode | Score | Error | Units |
---|---|---|---|---|
anonymousClass | thrpt | 3905620.807 | ± 97753.350 | ops/s |
lambdaExpression | thrpt | 3940706.986 | ± 136971.008 | ops/s |
Throughput (thrpt): Measures the number of operations per second
Takeaways
- Abstractions - The smallest unit is a function not a class
- Composition - Do one thing and do it well
- Functional Programming - A different way of thinking, it is not just about the features provided by the language
Sebastián Estrella
GitHub: sestrella
Twitter: @sestrelladev
Stack Builders
https://www.stackbuilders.com
Resources:
- https://slides.com/sestrella/refactoring-legacy-code
- https://github.com/stackbuilders/refactoring-legacy-code
Q&A
Thank you!
Refactoring Legacy Code using Functional Programming
By Sebastián Estrella
Refactoring Legacy Code using Functional Programming
FP features in Java are awesome, that is a fact, unfortunately sometimes is hard to connect that with the reality since developers have to deal with legacy code, however, even in those scenarios developers could build their own FP abstractions and reduce the complexity on their code
- 83