編寫可復(fù)用的代碼

在日常開發(fā)中,我們經(jīng)常聽到這樣的話:“把這段代碼提成一個(gè)單獨(dú)的方法(類),這樣就可以在被復(fù)用了”卫键,然而,我們由于我們抽取的方法不同虱朵,導(dǎo)致有些模塊并不具備復(fù)用的條件莉炉,而有的模塊不僅可以小規(guī)模復(fù)用钓账,甚至可以大規(guī)模復(fù)用。本文就和大家探討下如何寫出大規(guī)男跄可復(fù)用的代碼梆暮。

這里借用《Scala函數(shù)式編程》里舉的一個(gè)例子,來感受下:

場景:購買咖啡

Given: 我要用信用卡購買一杯咖啡

When:提供一個(gè)合法的信用卡號(hào)

Then:完成計(jì)費(fèi)绍昂,并能給客戶一杯咖啡

這是一個(gè)典型的場景啦粹,我們第一反應(yīng)下的代碼應(yīng)該是這樣的:

public class BuyCoffeeService{
  @Autowired
  BankService bankService;
  
  public Coffee buyCoffee(CreditCard creditCard){
    Coffee coffee=new Coffee();
    BigDecimal price=new BigDecimal("10.0");
    bankService.decrease(creditCard,price);
    }
}

一般來講,我們基本上完成了需求窘游,即使使用BDD測試套件唠椭,也察覺不到有什么問題。但是如何測試這個(gè)代碼是不是可復(fù)用呢忍饰,很簡單贪嫂,我們一次性買10杯咖啡。

public class BuyCoffeeService{
  @Autowired
  BankService bankService;
  
  public Coffee buyCoffee(CreditCard creditCard){
    Coffee coffee=new Coffee();
    BigDecimal price=new BigDecimal("10.0");
    bankService.decrease(creditCard,price);
    }
  public List<Coffee> buyCoffee(CreditCard creditCard,int count){
    if(count>0){
      List<Coffee> coffees=new ArrayList(n);
      for(int i=0;i<n;i++){
        coffees.add(this.buyCoffee(creditCard));
      }
      return coffees;
    }else{
      return Collections.emptyList();
    }
  }
}

? 看起來也能復(fù)用艾蓝。力崇。∮可是等我們上線后不久亮靴,客戶經(jīng)理氣呼呼的過來找你,說你的程序上線出事故了敌厘!有的客戶買了三杯咖啡台猴,結(jié)果提示用戶購買失敗,但是卻扣了兩杯的錢俱两!而商家說我一次性賣10杯饱狂,為什么扣10次手續(xù)費(fèi)?宪彩?

找來找去休讳,發(fā)現(xiàn)他們?nèi)际钦{(diào)用buyCoffee(CreditCard creditCard,int count)這個(gè)方法。因?yàn)锽ankService是一個(gè)遠(yuǎn)程調(diào)用尿孔,買多次咖啡可能引發(fā)上層應(yīng)用超時(shí)俊柔,而且,扣商家十筆手續(xù)費(fèi)確實(shí)不妥活合。

那么我們在這種場景下是不是可以做批量操作呢雏婶?答案是可以的。

public class BuyCoffeeService{
  @Autowired
  BankService bankService;
  
  public Coffee buyCoffee(CreditCard creditCard){
    AbstractMap.Entry<Coffee,BigDecimal> result=    cupOfCoffee();
    bankService.decrease(creditCard,result.value());
    }
  
  private AbstractMap.Entry<Coffee,BigDecimal> cupOfCoffee(){
    return new AbstractMap.Entry(new Coffee,new new BigDecimal("10.0"));
  }
  
  public List<Coffee> buyCoffee(CreditCard creditCard,int count){
    if(count>0){
      List<AbstractMap.Entry<Coffee,BigDecimal>> coffees=new ArrayList(n);
      for(int i=0;i<n;i++){
        coffees.add(this.cupOfCoffee());
      }
      bankService.decrease(creditCard,coffees.stream()
                           .map(entry->entry.value())
                           .reduce(new BigDecimal(0),(r1,r2)->r1.add(r2)));
      return coffees.stream().map(entry->entry.key()).collect(Collectors.toList());
    }else{
      return Collections.emptyList();
    }
  }
}

仔細(xì)分析這段代碼白指,會(huì)發(fā)現(xiàn)留晚,我們通過把生成賬單的這段代碼實(shí)現(xiàn)了邏輯的復(fù)用,消除了代碼之外隱含的問題告嘲。

舉這個(gè)例子错维,我是想提出一個(gè)觀點(diǎn):

可以完美復(fù)用的代碼奖地,一定是無副作用的 。

所謂副作用赋焕,指的是對外部世界的影響参歹,我們調(diào)用一段函數(shù)的時(shí)候,如果不能完全了解其對外部世界的影響隆判,就會(huì)出現(xiàn)隱含的bug犬庇。從外觀上看,調(diào)用buyCoffee的時(shí)候只是返回了一杯coffee侨嘀,完全不知道它還調(diào)用外部進(jìn)行了扣款操作械筛。這就是我們無法控制其副作用的表現(xiàn)。

那么如何控制代碼的副作用呢飒炎?

首先,副作用是無法消除的笆豁,但是可以被推遲處理郎汪。我們的軟件就是靠其產(chǎn)生的副作用產(chǎn)生價(jià)值的,所以沒辦法消除副作用闯狱,我們可以把副作用推到最外層處理煞赢,使計(jì)算部分和輸入輸出部分分開,正如上面那段代碼哄孤,計(jì)算部分就是產(chǎn)生coffee和賬單的cupOfCoffee 方法照筑,而buyCoffee則根據(jù)cupOfCoffee的結(jié)果與外部進(jìn)行通信。

我們能做的瘦陈,只能是隔離計(jì)算和輸入輸出凝危。

一般情況下Java的函數(shù)式API已經(jīng)夠我們用的了,我這里主要介紹幾種技巧晨逝,來消除計(jì)算過程中的副作用蛾默。

異常處理

異常處理在Java中有很多流派,在《Effective Java》中建議我們接口中方法聲明的時(shí)候不要聲明受檢異常捉貌,聲明受檢異常意味著接口方法要了解其實(shí)現(xiàn)支鸡,違背了面向接口編程的初衷。而在日常編碼中趁窃,我們常常像下面那樣操作

    private static void checkBounds(byte[] bytes, int offset, int length) {
        if (length < 0)
           throw new StringIndexOutOfBoundsException(length);
        if (offset < 0)
            throw new StringIndexOutOfBoundsException(offset);
       if (offset > bytes.length - length)
            throw new StringIndexOutOfBoundsException(offset + length);
    }

上面的例子來自JDK牧挣,嚴(yán)格的講,這也不是一個(gè)好的實(shí)踐醒陆,因?yàn)樗袀€(gè)假設(shè)瀑构,就是我們只能在同一個(gè)線程中調(diào)用該函數(shù)。下面的代碼將不能很好的運(yùn)行

public String findSomeName(Integer id){
   SomeOne someone= mapper.findeSomeNone(id)
     return someone.getName();
}

public List<String> memberNames(List<SomeOne> list){
  return list.parallelStream().map(this::findSomeName).collect(Collectors.toList());
}

上面如果 some沒有找到的話统求,會(huì)拋空指針異常检碗,但是不會(huì)在當(dāng)前線程中拋出据块,而且,無法從函數(shù)聲明的返回值類型無法覆蓋所有的結(jié)果(并沒有告訴調(diào)用者這方法將返回異常折剃,所以客戶端可能會(huì)漏掉異常處理)另假。這種情況,我們可以定義Either容器怕犁,關(guān)于Either容器边篮,這里就不詳細(xì)介紹了,直接貼解決代碼

public Either<String,Exception> findSomeName(Integer id){
  try{
    SomeOne someone= mapper.findeSomeNone(id)
     return Either.right(someone.getName());
  } catch(Excetion e){
    return Either.left(someone.getName());
  }
}

public List<String> memberNames(List<Integer> ids){
 
  List<Either<String,Exception>> result=list.parallelStream().map(this::findSomeName).collect(
  Collectors.toList());
   return result.stream().filter(Either::isRight).collect(Collectors.toList());
}

這樣處理 奏甫,既沒有把異常吞掉戈轿,又有效的對結(jié)果進(jìn)行了組合

多返回值

多返回值的問題也是經(jīng)常困擾開發(fā)人員的一個(gè)問題。在購買coffee的改造后的函數(shù)中阵子,我們返回了一個(gè)AbstractMap.Entry 作為返回值類型思杯,有的看官可能比較奇怪,這是因?yàn)镴ava中并沒有原生的Turple類型挠进,如果想要把Coffee和賬單同時(shí)返回色乾,一般情況下,需要定義一個(gè)DTO作為返回值领突,但是如果每抽取一個(gè)函數(shù)都要定義一個(gè)DTO的話暖璧,我想大多數(shù)人都不會(huì)愿意抽取函數(shù)了,所以我們可以定義一些公用的容器類君旦,作為返回值的容器澎办,

public static class Tuple2<A, B>{
       
        public final A _1;
        
        public final B _2;

        private Tuple2(A _1, B _2) {
            this._1 = _1;
            this._2 = _2;
        }
        public static <A,B> tuple2(A a,B b){
          return new Tuple2(a,b);
        }
    }

之后我們就可以用這個(gè)tuple輔助抽取一些需要多返回值的函數(shù)了,對于tuple2不滿足的可以定義一些tuple3金砍,tuple4...

本文介紹了一些日常開發(fā)用的技巧局蚀,如果想了解類似更多的技巧,可以了解些函數(shù)式編程的理論恕稠,在這里我就不展開講了至会。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市谱俭,隨后出現(xiàn)的幾起案子奉件,更是在濱河造成了極大的恐慌,老刑警劉巖昆著,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件县貌,死亡現(xiàn)場離奇詭異,居然都是意外死亡凑懂,警方通過查閱死者的電腦和手機(jī)煤痕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人摆碉,你說我怎么就攤上這事塘匣。” “怎么了巷帝?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵忌卤,是天一觀的道長。 經(jīng)常有香客問我楞泼,道長驰徊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任堕阔,我火速辦了婚禮棍厂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘超陆。我一直安慰自己牺弹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布时呀。 她就那樣靜靜地躺著例驹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪退唠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天荤胁,我揣著相機(jī)與錄音瞧预,去河邊找鬼。 笑死仅政,一個(gè)胖子當(dāng)著我的面吹牛垢油,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播圆丹,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼滩愁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了辫封?” 一聲冷哼從身側(cè)響起硝枉,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎倦微,沒想到半個(gè)月后妻味,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡欣福,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年责球,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡雏逾,死狀恐怖嘉裤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情栖博,我是刑警寧澤屑宠,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站笛匙,受9級(jí)特大地震影響侨把,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜妹孙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一秋柄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蠢正,春花似錦骇笔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至雹舀,卻和暖如春芦劣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背说榆。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工虚吟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人签财。 一個(gè)月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓串慰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親唱蒸。 傳聞我的和親對象是個(gè)殘疾皇子邦鲫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容