有經驗的Java開發者和架構師容易犯的10個錯誤(上)

首先允許我們問一個嚴肅的問題?爲什麼Java初學者能夠方便的從網上找到相對應的開發建議呢?每當我去網上搜索想要的建議的時候,我總是能發現一大堆是關於基本入門的教程、書籍以及資源。同樣也發現網上到處充斥着從寬泛的角度描述一個大型的企業級項目:如何擴展你的架構,使用消息總線,如何與數據庫互聯,UML圖表使用以及其它高層次的信息。

這時問題就來了:我們這些有經驗的(專業的)Java開發者如何找到合適的開發建議呢?現在,這就是所謂的灰色區域,當然同樣的也很難找到哪些是針對於資深開發者、團隊領導者以及初級架構師的開發建議。你會發現網上那些紛雜的信息往往只關注於開發世界的一角,要麼是極致(甚至可以說變態級別)地關心開發代碼的細節,要麼是泛泛而談架構理念。這種拙劣的模仿需要有一個終結。

說了半天,大家可能明白我希望提供的是那些好的經驗、有思考的代碼、和一些可以幫助從中級到資深開發者的建議。本文記錄了在我職業生涯裏發現的那些有經驗的開發者最常犯的10個問題。發生這些問題大多是對於信息的理解錯誤和沒有特別注意,而且避免這些問題是很容易的。

讓我們開始逐個討論這些你可能不是很容易注意的問題。我之所以會用倒序是因爲第一個問題給我帶來了最大的困擾。但所有這10個問題(考慮一些額外的因素)對於你而言來說都有可能給你造成困擾(信不信由你);-)。

文章分上篇下篇,本文是上篇。

10、錯誤地使用或者誤解了依賴式注入

對於一個企業級項目來說,依賴式注入通常被認爲是好的概念。存在一種誤解——如果使用依賴注入就不會出現問題。但是這是真的嗎?

依賴式注入的一個基本思想是不通過對象本身去查看依賴關係,而是通過開發者以在創建對象之前預先定義並且初始化依賴關係(需要一個依賴式注入框架,譬如Spring)。當對象真正被創建時,僅僅需要在構造函數中傳入預先配置好的對象(構造函數注入)或者使用方法(方法注入)。

然而總的思想是指僅僅傳遞對象需要的依賴關係。但是我依然可以在一些新的項目裏發現如下的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CustomerBill {
 
        //Injected by the DI framework
        private ServerContext serverContext;
 
        public CustomerBill(ServerContext serverContext)
        {
                    this.serverContext = serverContext;
        }
 
        public void chargeCustomer(Customer customer)
        {
                    CreditCardProcessor creditCardProcessor = serverContext.getServiceLocator().getCreditCardProcessor();
                    Discount discount  = serverContext.getServiceLocator().getActiveDiscounts().findDiscountFor(customer);
 
                    creditCardProcessor.bill(customer,discount);
        }
}

當然,這不是真正的依賴注入。因爲這個對象始終需要由它自己進行初始化。在上面的代碼中 “serverContext.getServiceLocator().getCreditCardProcessor()”這一行代碼更加體現了該問題。

譯註:作者指的是 creditCardProcessor、discount 這兩個變量的初始化。

當然,最好的方式應該是隻注入那些真正需要的變量(最好是標記爲final):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CustomerBillCorrected {
 
        //Injected by the DI framework
        private ActiveDiscounts activeDiscounts;
 
        //Injected by the DI framework
        private CreditCardProcessor creditCardProcessor;
 
        public CustomerBillCorrected(ActiveDiscounts activeDiscounts,CreditCardProcessor creditCardProcessor)
        {
                    this.activeDiscounts = activeDiscounts;
                    this.creditCardProcessor = creditCardProcessor;
        }
 
        public void chargeCustomer(Customer customer)
        {
                    Discount discount  = activeDiscounts.findDiscountFor(customer);
 
                    creditCardProcessor.bill(customer,discount);
        }
}

譯註:請注意兩段代碼的區別在於對於代碼中需要的資源的範圍。從使用依賴注入的角度來看,前一段代碼中注入的範圍很大,那就意味着有了更多的變化空間,但是容易造成代碼的功能不單一,同時增加了代碼測試的複雜度。後一段代碼中注入的範圍就很精確,代碼簡單易懂測試起來也比較容易。

9、像使用perl一樣來使用Java

(跟其它編程語言比較)Java提供了一個好的屬性,就是它的類型安全性。可能在一些小型項目中開發者只有你自己,你可以使用任何喜歡的編程風格。但如果是一個代碼量很大以及複雜系統的Java項目中, 在錯誤發生時你需要早一些得到警示。大多數的錯誤應該在編譯階段而不是在到運行期就被發現(如果你對Java不甚瞭解,請閱讀Java的相關資料)。

Java提供了許多特性去輔助產生這些編譯器的警告。但是如果你寫出下面的代碼編譯器還是沒有辦法捕獲到對應的警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AnimalFactory {
 
        public static Animal createAnimal(String type)
        {
                    switch(type)
                    {
                    case "cat":
                                return new Cat();
                    case "cow":
                                return new Cow();
                    case "dog":
                                return new Dog();
                    default:
                                return new Cat();
                    }
        }
 
}

譯註:請注意這段代碼只能工作在jdk 1.7 下。JDK 1.7以下的版本編譯不能通過。

這段代碼是非常危險,而且編譯器不會產生任何的警告幫到你。一個開發者也許會調用工廠方法以一個錯誤拼寫“dig”創建一個Cat對象。但實際上,他需要的是一個Dog對象。這段代碼不但會編譯通過,而且錯誤往往只能在運行期被發現。更嚴重的是,這個錯誤的產生依賴於應用程序本身的特性,因而有可能在程序上線幾個月以後才能發現它。

你是否希望Java編譯器可以通過某種機制幫你提前捕獲到這樣錯誤呢?這裏提供一個更正確的方式來確保代碼只有被正確的使用的情況下才能編譯通過(當然還有其他的解決方案)。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AnimalFactoryCorrected {
        public enum AnimalType { DOG, CAT,COW,ANY};
 
        public static Animal createAnimal(AnimalType type)
        {
                    switch(type)
                    {
                    case CAT:
                                return new Cat();
                    case COW:
                                return new Cow();
                    case DOG:
                                return new Dog();
                    case ANY:
                    default:
                                return new Cat();
                    }
        }
 
}

譯註:
1. 這裏使用了java enum類型。
2. 由於對Perl語言不慎瞭解,猜測作者隱含的意思是perl語言如果按照第一種寫法,被錯誤調用的時候是否在編譯器就會報錯。
如果知道的人可以幫忙解釋一下。

8、像C語言一樣使用Java (即不理解面向對象編程的理念)

回到C語言編程的時代,C語言建議用過程化的形式來書寫代碼。開發者使用結構體存儲數據,通過函數來描述那些發生在數據上的操作。這時數據是愚笨的,方法反而是聰明的。

譯註:作者估計是想說,數據和函數是分離的沒有直接的上下文來描述之間的關係。

然而Java正好是反其道而行。由於Java是一門面向對象的語言,在創建類的時候數據和函數被聰明地綁定在一起。

然而大多數的Java開發者要麼不理解上述兩門語言之間的區別,要麼就是他們討厭編寫面向對象代碼。雖然他們知道過程化開發方式與Java有些格格不入。

一個在Java應用程序中,最顯而易見的過程化編程就是使用instanceof,並在隨後的代碼中判斷向上轉換或向下轉換。instanceof有它合適使用的情況,但在企業級的代碼中通常它是一個嚴重的反模式示例。

下面的示例代碼描述了這種情況:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void bill(Customer customer, Amount amount) {
 
                    Discount discount = null;
                    if(customer instanceof VipCustomer)
                    {
                                VipCustomer vip = (VipCustomer)customer;
                                discount = vip.getVipDiscount();
                    }
                    else if(customer instanceof BonusCustomer)
                    {
                                BonusCustomer vip = (BonusCustomer)customer;
                                discount = vip.getBonusDiscount();
                    }
                    else if(customer instanceof LoyalCustomer)
                    {
                                LoyalCustomer vip = (LoyalCustomer)customer;
                                discount = vip.getLoyalDiscount();
                    }
 
                    paymentGateway.charge(customer, amount);
 
}

使用面向對象的來重構以後的代碼如下:

1
2
3
4
5
6
7
public void bill(Customer customer, Amount amount) {
 
                   Discount discount = customer.getAppropriateDiscount();
 
                   paymentGateway.charge(customer, amount);
 
}

譯註:這裏可以認爲使用設計模式當中的Factory method模式。

每個繼承Customer(或者實現Customer接口)定義了一個返回折扣的方法。這樣做的好處在於你可以假如新的類型的Customer而不需要關係customer的管理邏輯。而使用instanceof的判斷每次添加一個新的類型的Customer意味着你需要修改customer打印代碼、財務代碼、聯繫代碼等等,當然同時還需要添加一個If判斷。

你可能也需要查看一下關於充血模型vs貧血模型的討論

7、濫用延遲初始化 (即不能真正的理解對象的生命週期)

我經常能發現如下的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CreditCardProcessor {
        private PaymentGateway paymentGateway = null;
 
        public void bill(Customer customer, Amount amount) {
 
            //Billing a customer always needs a payment gateway anyway
            getPaymentGateway().charge(customer.getCreditCart(),amount);
 
        }
 
        private PaymentGateway getPaymentGateway()
        {
                    if(paymentGateway == null)
                    {
                                paymentGateway = new PaymentGateway();
                                paymentGateway.init(); //Network side Effects  here
                    }
                    return paymentGateway;
        }
 
}

延遲初始化初衷是好的,即如果你有個非常昂貴的對象(譬如對象需要網絡連接或者連接Web API等等),當然應該只在需要的時候創建它。然而,在你的項目中使用這項技術的時候最好確認以下兩點:

  • 這個對象真的很“昂貴”(你是如何給出這樣的結論或者定義?)
  • 存在這個對象不被使用的情況 (確實不需要創建這個對象)

在實際開發中,我不斷髮現延遲初始化被用在對象上。但實際上,這樣的對象要麼不是真的那麼“昂貴”,要麼總是在運行期創建。延遲初始化這種對象能得到什麼好處呢?

過度使用延遲初始化的主要問題在於它隱藏了組件的生命週期。一個經過良好搭建的應用程序應該對它主要部件的生命週期有清晰的瞭解。應用程序需要非常清楚對象什麼時候應該被創建、使用和銷燬。依賴注入可以幫助定義對象的生命週期。

但依賴注入在對象創建時也有副作用。使用以來注入表明應用程序狀態依賴於對象被創建的順序(按照要求的類型順序)。由於涵蓋了過多的用例,對應用程序調試就變成了一件不可能完成的事情。復現生產環境也變成一項巨大的工程,因爲你必須十分清楚場景執行的順序。

相應的我們需要做的就是定義應用程序啓動時需要的所有對象。這也帶來了一個額外的好處,可以在應用程序發佈過程中捕獲任何致命的錯誤。

6、把GOF(俗稱四人幫)當作聖經

我十分羨慕設計模式的幾位作者。這本書籍以其他書籍所無可比擬的氣勢影響了整個IT界。如果你沒看過《設計模式》,沒有記住模式的名字或者準則的話,那麼在面試中就可能無法通過。期望這樣的錯誤可以慢慢改善。

不要誤解我,這本書本身是沒有問題的。問題出在人們如何解釋以及使用它。下面是通常場景:

  1. 架構師馬克,拿到這本書開始閱讀。他覺得這本書牛逼壞了!
  2. 馬克趁熱打鐵開始閱讀現在工作的代碼。
  3. 馬克選擇了一種設計模式並應用到了代碼當中。
  4. 隨後馬克把這本書推薦給了那些跟他重複同樣步驟的資深開發者。

結果就是一團糟。

如何正確使用這本書實際上已經在導讀中做了清晰的說明(提醒那些不看導讀的人)——“在過去你有個問題,而且這個問題總是一遍又一遍地困擾着你”。注意到其中的順序了嗎?先有一個問題,然後查看這本書之後找到對應的解決方案。

不要掉進看這本書的陷阱當中——“找到一個方案然後嘗試把它應用在自己的的代碼中。尤其要注意的是,一些書中描述的模式在現實當中已經不再正確。”(請參見下篇第5條)。


原文鏈接: zeroturnaround 翻譯: ImportNew.com Andy.Song
譯文鏈接: http://www.importnew.com/6953.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章