Refatorando com a API
funcional do Java
Giovane Liberato @ JUG Vale 14
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
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)
Introdução ao
Refactoring
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”
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
Catálogo de
Refactorings
Lembrete
Isso não é o refactoring mais grandioso do mundo,
isso é apenas um tributo
Ouça (ou leia) sua IDE
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
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();
}
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");
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();
}
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
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;
}
}
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;
}
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;
}
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() { ... }
}
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());
}
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
public class Account {
private String pushNotificationId;
public String getPushNotificationId() { … }
}
-----------------------------------------------------------------
public class Driver {
private String pushNotificationId;
public String getPushNotificationId() { … }
}
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) { … }
}
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();
}
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();
}
public Campaign buildCampaign(List<Account> accounts, Message message) {
var pushIds = accounts
.stream()
.map(Account::getPushNotificationId)
.collect(toList());
return Campaign
.builder()
.pushNotificationIds(pushIds)
.message(message)
.build();
}
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);
}
}
public class CampaignService {
CampaignFactory campaignFactory;
private AccountRepository accountRepository;
private DriversRepository driversRepository;
public Campaign createCampaignForNewUsers(Country country) { .. }
public Campaign createCampaignForAllDrivers(Country country) { .. }
// ...
}
Invertendo dependência
public class CampaignFactory {
public Campaign buildCampaign(
Supplier<List<String>> idsSupplier, Message message) {
return Campaign
.builder()
.pushNotificationIds(idsSupplier.get())
.message(message)
.build();
}
}
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);
}
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);
}
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
public class AccountToNameConverter
implements Function<Account, String> {
@Override
public String apply(Account account) {
return String.format("%s %s",
account.getFirstName(), account.getLastName());
}
public class AccountService {
private AccountToNameConverter converter =
new AccountToNameConverter();
public List<String> getEveryonesName(List<Account> accounts) {
return accounts
.stream()
.map(converter)
.collect(toList());
}
}
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());
}
}
public class AccountService {
public List<String> getEveryonesName(List<Account> accounts) {
return accounts
.stream()
.map(AccountToNameConverter::convert) // ou___
.map(AccountToNameConverter::convertLastFirst)
.collect(toList());
}
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)
Códigos disponíveis em
https://github.com/giovaneliberato/refactoring-java-8plus
Obrigado!
about.me/giovaneliberato

Refatorando com a API funcional do Java

  • 1.
    Refatorando com aAPI funcional do Java Giovane Liberato @ JUG Vale 14
  • 2.
    Agenda ● Introdução aoRefactoring ● 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 SoftwareEngineer 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)
  • 4.
  • 5.
    Refactoring is acontrolled 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 constantesque 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
  • 9.
  • 10.
    Lembrete Isso não éo refactoring mais grandioso do mundo, isso é apenas um tributo
  • 11.
  • 12.
    Composição de regrascom 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(Accountaccount) { 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(Accountaccount) { 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(Accountaccount) { 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 - removendodas 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(Accountaccount) { 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ênciacom 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( Countrycountry, 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( Countrycountry, Message message) { var pushIds = driversRepository .findDriversByCountry(country) .stream() .map(Driver::getPushNotificationId) .collect(toList()); return Campaign .builder() .pushNotificationIds(pushIds) .message(message) .build(); }
  • 27.
    public Campaign buildCampaign(List<Account>accounts, Message message) { var pushIds = accounts .stream() .map(Account::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(Countrycountry) { 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(Countrycountry) { 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 usarfunçõ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 implementsFunction<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 - Improvingthe 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)
  • 39.
  • 40.