如何編寫不算差的物件導向程式?

banq發表於2016-11-20
顯然物件導向程式設計方法曾被當作銀彈,但是無論如何作為技術架構師貨電腦科學專業畢業工作的人來說,掌握OOP這一技能會受到用人單位的相當重視。

我看到很多計算機程式設計師自豪地宣稱:耶,我以面對物件方式設計了程式碼,將我的資料成員定義為私有,只能透過公有方法訪問,我還建立了基類和子類的繼承關係,在基類中建立了虛擬方法,可以在派生類中共享。這就是物件導向嗎?

首先,我們需要了解物件導向程式設計背後的目的,除了編寫單個任務或使用一次就丟棄的小型程式以外,複雜的軟體應用程式總是需要反覆的修改,原始碼需要定期更新,新增新功能和修復錯誤,物件導向的程式設計方法可以幫助你組織程式碼,使得修改更加容易,除此以外,物件導向程式設計允許將大而複雜問題分解為更小更可重用的模組,這些模組用於管理大型程式碼的一致性。

大多數程式設計師認為他們理解物件導向的基礎,但是涉及到具體應用實際時,他們就抓狂了

物件導向程式設計方法的掌握需要有耐心,不斷練習和對自己的一點信心,它不只是一種程式設計技術,更是一種思維方式的轉變,需要大量實踐和良好的設計。

如何練習?起初嘗試物件導向程式設計看起來令人恐懼,注意不要過度複雜,不要試圖一次性學習所有的知識,嘗試先從採取一小步步驟開始,理解你的程式碼使用物件導向設計的動機,然後將其翻譯成程式碼,你將開始看到可以被抽象化 模組化和其它改善你設計的程式碼慢慢出現在你的視野裡。

大多數程式碼不是很完美,並且需要很長時間才能列出所有可能的缺陷,我將列出一些最常見和常見的錯誤,並給出一些建議來解決它們。

1.向不相關的類新增太多功能或職責:

class Account
{    
   public void Withdraw(decimal amount)    
   {        
      try        
      {            
          // logic to withdraw money       
      }        
      catch (Exception ex)        
      {            
          System.IO.File.WriteAllText(@"c:\logs.txt",ex.ToString());   
      }    
   }
}
<p class="indent">


這個類承擔了額外功能,將額外的異常日誌新增檔案這個職責增加了進來,將來如果需要更改日誌記錄機制,將需要更改Account這個類。

單一職責:
一個類只需要承擔一個責任,並且只有一個原因來改變類。 這可以透過將日誌記錄活動移動到一個單獨的類來解決,這個類只關注日誌異常並將其寫入日誌檔案。

public class Logger
{
   public void Handle(string error)
   {
       System.IO.File.WriteAllText(@"c:\logs.txt", error);
   }
 }
<p class="indent">


現在Account類具有將日誌記錄活動委託給Logger類的靈活性,並且它只能關注與帳戶相關的活動。

class Account
{
   private Logger obj = new Logger();    
   public void Withdraw(decimal amount)    
   {        
      try        
      {            
          // logic to withdraw money       
      }        
      catch (Exception ex)        
      {            
          obj.Handle(ex.ToString());
      }    
   }
}
<p class="indent">

考慮有一個新的要求來處理不同的帳戶型別,如儲蓄帳戶和當前帳戶。 為了適應這個,我新增一個屬性到Account類名為“AccountType”。 根據帳戶的型別,利率也不同。 我寫一個方法來計算帳戶型別的利息:


class Account
{
   private int _accountType;

   public int AccountType
   {
      get { return _accountType; }
      set { _accountType = value; }
   } 
   public decimal CalculateInterest(decimal amount)    
   {        
      if (_accountType == "SavingsAccount")
      {
         return (amount/100) * 3;
      }
      else if (_accountType == "CurrentAccount")
      {
         return (amount/100) * 2;
      }
   }
}
<p class="indent">

上面的程式碼的問題是“分支”。 如果將來有一個新的帳戶型別,再次需要更改帳戶類以滿足,需要增加新的if-else條件程式碼。 請注意,我們在每次需求變化時都要更改帳戶類。 這顯然不是一個可擴充套件的程式碼。

對擴充套件開放,對修改關閉。

我們可以嘗試透過新增新帳戶型別,是不是可以新增一個從Account類繼承的新類來擴充套件程式碼? 透過這樣做,我們不僅抽象了Account類,而且允許它在其子類中共享共同的行為。

public class Account
{
   public virtual decimal CalculateInterest(decimal amount)
   {
       // default 0.5% interest
       return (amount/100) * 0.5;
   }
}
public class SavingsAccount: Account
{
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 3;
   }
}
public class CurrentAccount: Account
{
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 2;
   }
}
<p class="indent">

如果需求再次變動:正在開發新的帳戶型別稱為線上帳戶。 此帳戶的利率為5%,擁有類似儲蓄銀行賬戶的所有利益。 但是,您不能從ATM提取現金。 你只能電匯。

public class Account
{
   public virtual void Withdraw(decimal amount)
   {
       // base logic to withdraw money
   }
   public virtual decimal CalculateInterest(decimal amount)
   {
       // default 0.5% interest
       return (amount/100) * 0.5;
   }
}
public class SavingsAccount: Account
{
   public override void Withdraw(decimal amount)
   {
        // logic to withdraw money
   }
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 3;
   }
}
public class OnlineAccount: Account
{
   public override void Withdraw(decimal amount)
   {
        throw new Exception("Not allowed");
   }
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 5;
   }
}
<p class="indent">

現在,考慮我想關閉我的所有帳戶。 所以,每個帳戶的所有錢必須在完全關閉帳戶之前取出。

public void CloseAllAccounts()
{
    // Retrieves all accounts related to this customer
    List<Account> accounts = customer.GetAllAccounts();
    foreach(Account acc in accounts)
    {
      // Exception occurs here
      acc.Withdraw(acc.TotalBalance);
    }
}
<p class="indent">


根據繼承層次結構,Account物件可以指向其任何一個子物件。 在編譯期間沒有注意到異常行為。 但是,在執行時,它丟擲異常“不允許”。 我們從中推斷出什麼? 父物件無法無縫替換子物件。

讓我們建立2個介面 - 一個處理興趣(IProcessInterest)和另一個處理撤回(IWithdrawable)

interface IProcessInterest
{
    decimal CalculateInterest(decimal amount);
}
interface IWithdrawable
{
    void Withdraw(double amount);
}

<p class="indent">

OnlineAccount類將僅實現IProcessInterest,而Account類將實現IProcessInterest和IWithdrawable。

public class OnlineAccount: IProcessInterest
{
    public decimal CalculateInterest(decimal amount)
    {
       return (amount/100) * 5;
    }
}
public class Account: IProcessInterest, IWithdrawable
{
   public virtual void Withdraw(decimal amount)
   {
       // base logic to withdraw money
   }
   public virtual decimal CalculateInterest(decimal amount)
   {
       // default 0.5% interest
       return (amount/100) * 0.5;
   }
}

<p class="indent">


現在,這看起來很乾淨。 我們可以建立一個IWithdrawable列表並向其中新增相關的類。 如果透過新增OnlineAccount到GetAllAccounts方法中的列表而產生錯誤,我們將得到一個編譯時錯誤。

public void CloseAllAccounts()
{
    // Retrieves all withdrawable accounts related to this customer
    List<IWithdrawable> accounts = customer.GetAllAccounts();
    foreach(Account acc in accounts)
    {
      acc.Withdraw(acc.TotalBalance);
    }
}

<p class="indent">


▪ 假設我們的Account類又面對新的需求。 商業建議提出一個API,允許從不同的第三方銀行的ATM提款。 我們暴露了一個Web服務,其它銀行可以開始使用Web服務提款。 一切聽起來不錯,直到現在。 幾個月後,該業務又出現了另一個要求,即一些其他銀行也要求從其ATM中設定該帳戶的提款限額。 沒問題,提出更改請求 - 它可以很容易做到。

interface IWithdrawable
	▪	{
	▪	   void Withdraw(decimal amount);
	▪	   void SetLimit(decimal limit);
	▪	}
	▪	
	▪	

我們剛剛做的是很奇怪。 透過改變現有的介面,你正在新增一個破壞性改變,並打擾所有的原本很愉快消費我們網路服務的銀行,只是因為新增撤回這個功能。 現在你迫使他們也使用新暴露的方法。 這種方式不好。

修復此問題的最佳方法是建立新介面,而不是修改現有介面。 當前介面IWithdrawable可以不動,增加一個新的介面 - 例如,IExtendedWithdrawable建立實現IWithdrawable。
interface IExtendedWithdrawable: IWithdrawable
{
void SetLimit(decimal limit);
}

因此,舊客戶端將繼續使用IWithdrawable,新客戶端可以使用IExtendedWithdrawable。 簡單,但有效!


▪ 讓我們回到第一個問題,我們新增Logger類以委託來自Account類的日誌記錄的責任。 它將異常記錄到檔案。 有時為了方便訪問,很容易透過電子郵件獲取日誌檔案或將其與某些第三方日誌檢視器整合。 讓我們實現:

interface ILogger
{
    void Handle(string error);
}
public class FileLogger : ILogger
{
   public void Handle(string error)
   {
       System.IO.File.WriteAllText(@"c:\logs.txt", error);
   }
}
public class EmailLogger: ILogger
{
   public void Handle(string error)
   {
       // send email
   }
}
public class IntuitiveLogger: ILogger
{
   public void Handle(string error)
   {
       // send to third party interface
   }
}
class Account : IProcessInterest, IWithdrawable
{
   private ILogger obj;    
   public void Withdraw(decimal amount)    
   {        
      try        
      {            
          // logic to withdraw money       
      }        
      catch (Exception ex)        
      {            
          if (ExType == "File")
          {
             obj = new FileLogger();
          }
          else if(ExType == "Email")
          {
             obj = new EmailLogger();
          }
          obj.Handle(ex.Message.ToString());
      }    
   }
}

<p class="indent">


我們編寫了一個優秀的可擴充套件程式碼 - ILogger,它作為其他日誌記錄機制的通用介面。 然而,我們再次違背了Account類單一職責的意願,賦予其更多的責任,也就是它要決定建立的Logging類的哪個例項。 這不是Account類的工作。

解決方案是“反轉依賴”到一些其他類,而不是Account類。 讓我們透過Account類的建構函式注入依賴。 因此,我們正在把責任推諉給客戶端,它來決定必須應用哪種日誌記錄機制。 客戶端可能依賴於應用程式配置設定。 我們不必擔心本文中的配置級別理解 - 至少。

class Account : IProcessInterest, IWithdrawable
{
   private ILogger obj;
   public Customer(ILogger logger)
   {
       obj = logger;
   }
}
// Client side code
IWithdrawable account = new SavingsAccount(new FileLogger());

<p class="indent">


如果您密切關注,你可能有容易猜到的是,所有這些建議的修復都不過是應用物件導向設計的SOLID原則 。



How to write an object oriented program that doesn

相關文章