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