Functional programming styles


Your task as a developer is to write a code for selecting clients that have a good credit history and age is above 30 and live in NY city.

You wrote a code like this:

Set<Client> selectedClients = new HasHSet();
for (Client client: clients) {
  Credit credit = finance.getCredit(client);
  Payments payments = credit.getHistory().getPayments();
  if (payments.notOverdue())
    if (client.getAge() > 30)
      if(client.address().getCity() == NY) {
        selectedClients.put (client);
      }
}

A senior programmer seeing your code:

You know that programmers don’t express any emotion, so you start to think that the senior face means something, something not very good…

Yes, the code is hard to test, because it contains a lot of conditions. Also the claity/readability is not very high. You are wondering if there is a way to write cleaner and testable code? You remember something about functional programming <link> Maybe this is a good approach? You back to the code again and noticed similaraty to:

Yes, the tower. Building blocks tower from functional post. This is it.

You rewrite code using the functional approach with one stream logic:

clients.stream
  .filter(client -> {
     Credit credit = finance.getCredit(client);
     Payments payments = credit.getHistory().getPayments();
     return payments.notOverdue();
   })
  .filter(client -> client.getAge() > 30)
  .filter(client -> client.address().getCity() == NY)
  .map(client -> calculateScoring (client))
  .collect(Collectors.toSet())

Hmm this code is not cleaner than original solution and most important hard to test. For example, if we consider this code fragment:

  .filter(client -> {
     Credit credit = finance.getCredit(client);
     Payments payments = credit.getHistory().getPayments();
     return payments.notOverdue();
   })

After some time you and your colleage have no ide what the code is responsible for, and what the requirement was.

You can try different approach – functions composition:

calculateScoring (
  clientsLivingInNYCity (
    clientsAgeOver30 (
      clientsWithGoodCreditHistory (clients)
)))
Set<Client> clientsWithGoodCreditHistory (Set<Client> clients) {
  return clients.stream.filter(client -> {
     Credit credit = finance.getCredit(client);
     Payments payments = credit.getHistory().getPayments();
     return payments.notOverdue();
   }).toSet();
}
Set<Client> clientsAgeOver30 (Set<Client> clients) {
  return clients.stream.filter(client ->
    client.getAge() > 30;
  ).toSet();
}
//... other methods

Main logic is very clear. Many methods, but particular conditions could be tested separately. Method names express conditions/requirements. A lot of benefits, but every function must process the collection of objects. Could it exists something even simpler?

Maybe hybrid solution – stream using a functions:

clients.stream()
  .filter(::hasGoodCreditHistory)
  .filter(::hasAgeOver30)
  .filter(::liveInNYCity)
  .toList();

bool hasGoodCreditHistory(client) {
  Credit credit = finance.getCredit(client);
  Payments payments = credit.getHistory().getPayments();
  return payments.notOverdue();
}
hasAgeOver30 (client) { return client.getAge() > 30; }
liveInNYCity (client) { return client.address().getCity() == NY }

The main code is clear, compound of methods that express the conditions/requirements and could be easily tested. Quite good. Yes.

If we do not need to test particular simple conditions, we can include them directly in stream:

clients.stream
  .filter(::hasGoodCreditHistory)
  .filter(client -> client.getAge() > 30)
  .filter(client -> client.address().getCity() == NY)
  .collect(Collectors.toList())

bool hasGoodCreditHistory(client) {
  Credit credit = finance.getCredit(client);
  Payments payments = credit.getHistory().getPayments();
  return payments.notOverdue();
}

Senior dev seeing your new solution:

Thx for reading.

,

Leave a Reply

Your email address will not be published. Required fields are marked *