掌握J(rèn)ava函數(shù)式接口,輕松實(shí)現(xiàn)依賴反轉(zhuǎn)
你是否考慮過(guò)使用Java函數(shù)式接口來(lái)反轉(zhuǎn)Java項(xiàng)目?jī)?nèi)的依賴關(guān)系?在本文中,我們將探討如何通過(guò)使用三個(gè)關(guān)鍵接口——Supplier、Consumer和Function來(lái)實(shí)現(xiàn)這一目標(biāo)。
1. Supplier
Supplier接口用于在不需要任何輸入?yún)?shù)的情況下提供一個(gè)對(duì)象,以下是Supplier接口的定義。
public interface Supplier{
T get();
}
為了更好地理解使用這個(gè)接口的必要性,讓我們看一看下面的代碼。
public class Logger{
public void log(String message){
if(isLogEnabled()){
write(message);
}
}
}
// 使用Logger類
public class Controller{
@Inject Logger logger;
public void execute(){
logger.log(generateLogMessage());
}
}
在上面的代碼中,我們有一個(gè)Logger類負(fù)責(zé)在日志被啟用時(shí)寫(xiě)入日志消息。Controller類通過(guò)調(diào)用generateLogMessage方法來(lái)向Logger類傳遞消息。到目前為止,一切看起來(lái)都很順利。
然而,試想一下,如果generateLogMessage方法涉及大量處理或消耗大量資源,而日志記錄又被禁用了,那么這些有價(jià)值的資源就白白浪費(fèi)了,因?yàn)樯傻娜罩鞠⒉粫?huì)被使用。
解決這個(gè)問(wèn)題的辦法是向Logger類傳遞一個(gè)Supplier,它將在需要時(shí)返回消息,而Logger類只需在日志被啟用時(shí)調(diào)用該方法即可,代碼如下所示。
public class Logger{
public void log(Supplier messageSupplier){
if(isLogEnabled()){
write(messageSupplier.get());
}
}
}
// 使用Logger類
public class Controller{
@Inject Logger logger;
public void execute(){
logger.log(() -> generateLogMessage());
}
}
現(xiàn)在,generateLogMessage方法只會(huì)在Supplier的get方法被調(diào)用時(shí)執(zhí)行,這樣我們就能在日志未啟用時(shí)節(jié)省資源。此外,通過(guò)使用Supplier這種解決方案,我們可以靈活地實(shí)現(xiàn)復(fù)雜的日志記錄邏輯,并確保它只會(huì)在需要時(shí)被調(diào)用。
2. Function
通過(guò)Function接口,可以定義一個(gè)接收參數(shù)并產(chǎn)生結(jié)果的函數(shù)。以下是Function接口的定義(省略了一些默認(rèn)方法)。
public interface Function{
R apply(T t);
}
為了開(kāi)始探索Function接口,讓我們來(lái)看一個(gè)負(fù)責(zé)計(jì)算銷售訂單中商品價(jià)格的類。這個(gè)類需要接收輸入來(lái)計(jì)算最終價(jià)格,輸入包括產(chǎn)品、數(shù)量和適用的折扣(0到100之間)等。
public class PriceCalculator{
public BigDecimal calculatePrice(Product product,
Integer quantity,
BigDecimal discount){
var grossPrice = product.getUnitPrice()
.multiply(BigDecimal.valueOf(quantity));
var discountAmount = grossPrice.multiply(discount)
.divide(BigDecimal.valueOf(100));
return grossPrice.minus(discountAmount);
}
}
// 使用示例
var result = priceCalculator(product, 10, BigDecimal.value(10));
這個(gè)類首先計(jì)算總價(jià),然后應(yīng)用折扣,再?gòu)目們r(jià)中減去折扣金額?,F(xiàn)在,讓我們考慮一個(gè)新的需求:對(duì)價(jià)格進(jìn)行貨幣轉(zhuǎn)換。
一種方法可能是直接將貨幣轉(zhuǎn)換邏輯添加到這個(gè)類中,這可能會(huì)帶來(lái)錯(cuò)誤。更穩(wěn)健的解決方案是引入一個(gè)負(fù)責(zé)處理貨幣轉(zhuǎn)換的Function參數(shù)。
public class PriceCalculator{
public BigDecimal calculatePrice(
Product product,
Integer quantity,
BigDecimal discount,
Function converterFunction){
var grossPrice = product.getUnitPrice()
.multiply(BigDecimal.valueOf(quantity));
var discountAmount = grossPrice.multiply(discount)
.divide(BigDecimal.valueOf(100));
var netPrice = grossPrice.minus(discountAmount);
return converterFunction.apply(netPrice);
}
}
// 使用示例
var result = priceCalculator(product,
10,
BigDecimal.value(10),
netPrice -> netPrice.multiply(CURRENCY_RATE));
增加這個(gè)新需求對(duì)代碼的影響很小,我們成功地反轉(zhuǎn)了依賴關(guān)系。PriceCalculator類不再需要處理貨幣轉(zhuǎn)換;相反,它只是用提供的函數(shù)調(diào)用凈價(jià),并返回結(jié)果。這種設(shè)計(jì)使我們能夠在不修改PriceCalculator類的情況下,使用相同的類轉(zhuǎn)換為任何貨幣。
還有其他一些方法可以滿足這個(gè)需求,而不需要修改PriceCalculator類。你可以創(chuàng)建另一個(gè)類,充當(dāng)調(diào)用PriceCalculator的外觀,然后進(jìn)行貨幣轉(zhuǎn)換。通常,采用哪種解決方案是由具體項(xiàng)目決定的。
3. Consumer
Consumer接口支持定義一個(gè)接收參數(shù)、執(zhí)行特定任務(wù)但不返回任何值的函數(shù)。以下是Consumer接口的定義(省略了一些默認(rèn)方法)。
public interface Consumer{
void accept(T t);
}
為了解Consumer接口的運(yùn)行示例,我們來(lái)看看這個(gè)類,它在實(shí)體中設(shè)置了一些信息,并將其保存到數(shù)據(jù)庫(kù)中。
public class EntitySaver{
public void create(Entity entity){
entity.setCreationDate(new Date());
database.insert(entity);
}
}
// 使用示例
entitySaver.create(entity);
現(xiàn)在,假設(shè)我們需要在創(chuàng)建實(shí)體時(shí)通知其他類,但我們無(wú)法修改create方法的接口。在這種情況下,我們可以使用Consumer接口來(lái)實(shí)現(xiàn)發(fā)布-訂閱模式,下面是我們實(shí)現(xiàn)該模式的方法。
public class EntitySaver{
private List> consumerList = new ArrayList<>();
public void register(Consumer consumer){
consumerList.add(consumer);
}
public void create(Entity entity){
entity.setCreationDate(new Date());
database.insert(entity);
consumerList.forEach(consumer -> consumer.accept(entity));
}
}
// 使用示例
entitySaver.register(entity -> log.info(entity));
entitySaver.register(entity -> mailerService.notifyUser(entity));
entitySaver.create(entity);
在這個(gè)發(fā)布-訂閱模式的實(shí)現(xiàn)中,我們使用了Consumer接口。EntitySaver類現(xiàn)在維護(hù)了一個(gè)消費(fèi)者列表,并包含了一個(gè)register方法來(lái)添加消費(fèi)者到這個(gè)列表中。雖然create方法的接口保持不變,但我們引入了一行代碼來(lái)“消費(fèi)”創(chuàng)建的實(shí)體,方法是調(diào)用已注冊(cè)的消費(fèi)者。
4. 結(jié)語(yǔ)
Java函數(shù)式接口是多年前引入的,它們對(duì)我們開(kāi)發(fā)Java的方式產(chǎn)生了很大的影響。我們可以將它們用作lambda函數(shù),也可以用來(lái)反轉(zhuǎn)依賴關(guān)系,使我們的代碼更加簡(jiǎn)潔。