1. Refatorando com a API
funcional do Java
Giovane Liberato @ JUG Vale 14
2. Agenda
● Introdução ao Refactoring
● Catálogo de Refactorings
○ Ouça (ou leia) sua IDE
○ Composição de regras com
Predicates
○ Optionals - Quando (não)
usar
○ Inversão de dependências
com Suppliers
○ Funções idiomáticas
3. Giovane Liberato
Senior Software Engineer no *
Foco em arquitetura de micro serviços, desenvolvimento seguro e
práticas ágeis. Interesse em privacidade digital e criptografia.
about.me/giovaneliberato
* Temos vagas! (E são remotas)
5. Refactoring is a controlled technique for improving the
design of an existing code base.
Its essence is applying a series of small
behavior-preserving transformations, each of which "too
small to be worth doing".
Martin Fowler,
autor do livro “Refactoring Improving the Design of Existing Code”
6. Mudanças pequenas e
constantes que geram enorme
benefício a longo prazo
Objetivos e focados em melhoria
de arquitetura, legibilidade e na
redução de débitos técnicos
Cercado de testes para garantir
que não há quebra de
comportamento
Red -> Green -> Refactor
Evoluir a codebase para
acomodar novas features e
reduzir acoplamento entre
componentes do sistema
Como fazer
12. Composição de regras com Predicates
Cenário
Dado um usuário que contém uma lista de tags, definir qual o valor do cupom de
desconto gerado
Atores
Account, VoucherService e VoucherPredicates
Code smells
Implementação obstruindo legibilidade, lógicas binárias encadeadas no mesmo
IF
13. public Voucher getVoucherForAccount(Account account) {
if (account.getTags().contains("new")) {
return Voucher.of(15);
}
if (account.getTags().contains("lover")) {
return Voucher.of(20);
}
if (account.getTags().contains("veg")
&& account.getTags().contains("new")) {
return Voucher.of(25);
}
if (account.getTags().contains("lover")
&& (account.getTags().contains("pizza_lover")
|| account.getTags().contains("burger_lover"))) {
return Voucher.of(35);
}
return Voucher.none();
}
14. public static BiPredicate<Account, String> containsTag =
(account, tag)-> account.getTags().contains(tag);
public static Predicate<Account> IS_NEW =
(account) -> containsTag.test(account, "new");
public static Predicate<Account> IS_LOVER =
(account) -> containsTag.test(account, "lover");
public static Predicate<Account> IS_VEG =
(account) -> containsTag.test(account, "veg");
public static Predicate<Account> IS_PIZZA_LOVER =
(account) -> containsTag.test(account, "pizza_lover");
public static Predicate<Account> IS_BURGER_LOVER =
(account) -> containsTag.test(account, "burger_lover");
15. public Voucher getVoucherForAccount(Account account) {
if (IS_NEW.test(account)) {
return Voucher.of(15);
}
if (IS_LOVER.test(account)) {
return Voucher.of(20);
}
if (IS_VEG.and(IS_NEW).test(account)) {
return Voucher.of(25);
}
if (IS_LOVER.and(IS_PIZZA_LOVER.or(IS_BURGER_LOVER)).test(account)) {
return Voucher.of(35);
}
return Voucher.none();
}
16. Optionals - Quando (não) usar
Cenário
Um usuário pode favoritar e bloquear restaurantes do seu menu. Ambas as listas
podem ser vazias.
Atores
Account, Restaurant, RestaurantService e RestaurantRepository
Code smells
Optional como atributo de classe, chamada dos métodos .isPresent e .get,
Optional representando estado domínio
17. public class Account {
private Optional<List<Restaurant>> starredRestaurants;
private Optional<List<Restaurant>> blockedRestaurants;
public Optional<List<Restaurant>> getBlockedRestaurants() {
return blockedRestaurants;
}
public Optional<List<Restaurant>> getStarredRestaurants() {
return starredRestaurants;
}
}
18. public List<Restaurant> getRestaurantsForAccount(Account account) {
var restaurants = RestaurantRepository.getAll();
if (account.getBlockedRestaurants().isPresent()) {
var blocked = account.getBlockedRestaurants().get();
restaurants = restaurants
.stream()
.filter((r -> blocked.contains(r)))
.collect(toList());
}
if (account.getStarredRestaurants().isPresent()) {
restaurants.addAll(account.getStarredRestaurants().get());
}
return restaurants;
}
19. Optionals - usando .orElse
public List<Restaurant> getRestaurantsForAccount(Account account) {
var restaurants = RestaurantRepository.getAll();
var blocked = account.getBlockedRestaurants().orElse(emptyList());
restaurants = restaurants
.stream()
.filter((r -> blocked.contains(r)))
.collect(toList());
restaurants.addAll(account.getStarredRestaurants().orElse(emptyList()));
return restaurants;
}
20. Optionals - removendo das classes
public class Account {
private List<Restaurant> starredRestaurants;
private List<Restaurant> blockedRestaurants;
public List<Restaurant> getBlockedRestaurants() {
return blockedRestaurants != null ?
blockedRestaurants : emptyList();
}
public List<Restaurant> getStarredRestaurants() { ... }
}
21. public List<Restaurant> getRestaurantsForAccount(Account account) {
var restaurantList = RestaurantRepository.getAll();
var blocked = account.getBlockedRestaurants();
var starred = account.getStarredRestaurants();
return Stream.concat(
starred.stream(),
restaurantList
.stream()
.filter((blocked::contains)))
.collect(toList());
}
22. Inversão de dependência com suppliers
Cenário
Criação de objetos complexos baseado em diferentes fontes de dados
Atores
Account, Driver, CampaignService e CampaignFactory
Code smells
Inveja de funcionalidade (feature envy) e assinatura de métodos parcialmente
repetidas
23. public class Account {
private String pushNotificationId;
public String getPushNotificationId() { … }
}
-----------------------------------------------------------------
public class Driver {
private String pushNotificationId;
public String getPushNotificationId() { … }
}
24. public class CampaignFactory {
private AccountRepository accountRepository;
private DriversRepository driversRepository;
public Campaign buildCampaignForNewUsers(Country country, Message message) { … }
public Campaign buildCampaign(Country country, Message message) { … }
public Campaign buildCampaign(List<Account> accounts,Message message) { … }
public Campaign buildCampaignForDrivers(Country country, Message message) { … }
}
25. public Campaign buildCampaignForNewUsers(
Country country, Message message) {
var pushIds = accountRepository
.findNewUsersByCountry(country)
.stream()
.map(Account::getPushNotificationId)
.collect(toList());
return Campaign
.builder()
.pushNotificationIds(pushIds)
.message(message)
.build();
}
26. public Campaign buildCampaignForDrivers(
Country country, Message message) {
var pushIds = driversRepository
.findDriversByCountry(country)
.stream()
.map(Driver::getPushNotificationId)
.collect(toList());
return Campaign
.builder()
.pushNotificationIds(pushIds)
.message(message)
.build();
}
28. public class CampaignService {
CampaignFactory campaignFactory;
public Campaign createCampaignForNewUsers(Country country) {
var message = new Message("welcome");
return campaignFactory.buildCampaignForNewUsers(country, message);
}
public Campaign createCampaignForAllUsers(Country country) {
var message = new Message("#lanches");
return campaignFactory.buildCampaign(country, message);
}
public Campaign createCampaignForUsers(List<Account> accounts) {
var message = new Message("#lanches");
return campaignFactory.buildCampaign(accounts, message);
}
public Campaign createCampaignForAllDrivers(Country country) {
var message = new Message("bonus!");
return campaignFactory.buildCampaignForDrivers(country, message);
}
}
29. public class CampaignService {
CampaignFactory campaignFactory;
private AccountRepository accountRepository;
private DriversRepository driversRepository;
public Campaign createCampaignForNewUsers(Country country) { .. }
public Campaign createCampaignForAllDrivers(Country country) { .. }
// ...
}
Invertendo dependência
30. public class CampaignFactory {
public Campaign buildCampaign(
Supplier<List<String>> idsSupplier, Message message) {
return Campaign
.builder()
.pushNotificationIds(idsSupplier.get())
.message(message)
.build();
}
}
31. public Campaign createCampaignForNewUsers(Country country) {
var message = new Message("welcome");
Supplier<List<String>> ids = () ->
accountRepository.findNewUsersByCountry(country)
.stream()
.map(Account::getPushNotificationId)
.collect(toList());
return campaignFactory.buildCampaign(ids, message);
}
32. public Campaign createCampaignForAllDrivers(Country country) {
var message = new Message("bonus!");
Supplier<List<String>> ids = () ->
driversRepository.findDriversByCountry(country)
.stream()
.map(Driver::getPushNotificationId)
.collect(toList());
return campaignFactory.buildCampaign(ids, message);
}
33. Funções idiomáticas
Cenário
Para usar funções customizadas, o contra-exemplo implementa a interface
Function sem necessidade.
Atores
Account, AccountToNameConverter
Code smells
Implementando interfaces funcionais para casos simples. Múltiplas classes para
contextos parecidos
34. public class AccountToNameConverter
implements Function<Account, String> {
@Override
public String apply(Account account) {
return String.format("%s %s",
account.getFirstName(), account.getLastName());
}
35. public class AccountService {
private AccountToNameConverter converter =
new AccountToNameConverter();
public List<String> getEveryonesName(List<Account> accounts) {
return accounts
.stream()
.map(converter)
.collect(toList());
}
}
36. public class AccountToNameConverter {
public static String convert(Account acc) {
return String.format("%s%s",
acc.getFirstName(), acc.getLastName());
}
public static String convertLastFirst(Account acc) {
return String.format("%s %s",
acc.getLastName(), acc.getFirstName());
}
}
37. public class AccountService {
public List<String> getEveryonesName(List<Account> accounts) {
return accounts
.stream()
.map(AccountToNameConverter::convert) // ou___
.map(AccountToNameConverter::convertLastFirst)
.collect(toList());
}
38. Referências
Refactoring - Improving the Design of Existing Code (Martin Fowler)
Effective Java, Third Edition Keepin' it Effective (J. Bloch)
Optional - The Mother of All Bikesheds (Stuart Marks)
Understanding the Economics of Refactoring (Leitch, Stroulia)
The Financial Implications of Technical Debt (Erik Frederick)