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!

Made with Slides.com