Working Effectively with Legacy Code - Ch25 - 解依賴技術 (6) 21. 子類別化並覆寫方法 「子類別化並覆寫方法」是物件導向程式中姐依賴的核心技術,這個手法就是讓我們可以在測試環境下,利用繼承將我們不關心的行為架空,或是覆寫你在乎的行為讓他可以達到感測的目的,事實上這本書中其他的很多手法都是這個手法的變形。
before 上有許多方法,假設我們今天要加入測試的方法是 ```sendMessage()``` ,而他的過程中會呼叫一個私有方法 ```createForwardMessage()``` 去生成一個新的訊息,但過程中用到了 ```Session``` 物件,可是測試過程我們沒辦法對真正的 ```Session``` 去做操作。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 **MessageForwarder** ```php= class MessageForwarder {     public function sendMessage()     {         $session = new Session();         $message = $this->createForwardMessage($session, new Message());         // do something         // do something         // do something         $this->send($message);     }     private function createForwardMessage(Session $session, Message $message): Message     {         $forward = new MimeMessage ($session);         $forward->setFrom();         $forward->addRecipients();         $forward->setSubject();         $forward->setSentDate();         $forward->addHeader();         $forward = $this->buildForwardContent($message, $forward);         return $forward;     }     private function buildForwardContent(Message $message, MimeMessage $forward): Message     {         // build forward content         return $forward;     }     private function send(Message $message)     {         // send message     } } 
after 為了讓 createForwardMessage() 的過程與 Session 解依賴,我們將 createForwardMessage() 設定為 proteted ,並新增一個 MessageForwarder 的子類別 TestingMessageForwarder ,然後將 createForwardMessage() 給覆寫,讓他直接回傳我們偽造過後的 FakeMessage
MessageForwarder 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class  MessageForwarder     public  function  sendMessage (      {        $session  = new  Session ();         $message  = $this ->createForwardMessage ($session , new  Message ());                                    $this ->send ($message );     }     protected  function  createForwardMessage (Session $session , Message $message  ): Message       {        $forward  = new  MimeMessage  ($session );         $forward ->setFrom ();         $forward ->addRecipients ();         $forward ->setSubject ();         $forward ->setSentDate ();         $forward ->addHeader ();         $forward  = $this ->buildForwardContent ($message , $forward );         return  $forward ;     }     private  function  buildForwardContent (Message $message , MimeMessage $forward  ): Message       {                 return  $forward ;     }     private  function  send (Message $message  )      {             } } 
TestingMessageForwarder 
1 2 3 4 5 6 7 class  TestingMessageForwarder  extends  MessageForwarder     protected  function  createForwardMessage (Session $session , Message $message  ): Message       {        return  new  FakeMessage ();     } } 
在這個新建立的 TestingMessageForwarder 子類別中,我們可以架空我們用不到的功能,並且可以達成感測與分離的目的
22. 替換實例變數 有些語言不支援在建構子中呼叫虛擬函式,例如 C++
before Pager 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class  Pager  { public :    Pager () {          reset ();         formConnection();      }     virtual  void  formConnection ()           assert (state == READY);              } 	     void  sendMessage (const  std::string& address,                        const  std::string& message)          formConnection();     }  } 
TestingPager 
1 2 3 4 5 6 class  TestingPager  : public  Pager { public :    virtual  void  formConnection ()       }  } 
Test 
1 2 3 4 5 TEST (messaging,Pager) {    TestingPager pager;     pager.sendMessage ("5551212" , "Hey, wanna go to a party? XXXOOO" );     LONGS_EQUAL (OKAY, pager.getStatus ());  } 
以上是一個 C++ 的例子,我對它實施了「子類別化並覆寫方法」的手段,但這種方式在 C++ 中會出問題,當我們將 TestingPager 給實例化的時候, Pager 的 formConnection() 會被執行,而不是 TestingPager 的 formConnection() ,這使得我法成功運用「子類別化並覆寫方法」去取代原本的 formConnection() 行為。
after 如果在建構子內使用了某個物件,而我們想要將他替換掉,可以採用「替換實例變數」手法
1 2 3 4 5 6 7 8 9 BlendingPen::BlendingPen ()  { 	setName ("BlendingPen" ); 	m_param = ParameterFactory::createParameter ("cm" , "Fade" , "Aspect Alter" );  	m_param->addChoice ("blend" ); 	m_param->addChoice ("add" ); m_param->addChoice ("filter" ); 	setParamByName ("cm" , "blend" );  } 
BlendingPen 的建構子透過一個工廠來建立 Parameter 物件,我們可以新增一個方法,直接替換掉 Parameter
1 2 3 4 5 void  BlendingPen::supersedeParameter (Parameter *newParameter)  	delete  m_param; 	m_param = newParameter;  } 
在測試的時候,我們可以根據需要去建立 BlendingPen 物件,並在需要放入感測物件的時候呼叫 supersedeParameter() 方法。
23. 模板重定義 如果你的語言支援泛型以及類型別名,則可以使用「模板重新定義」來解依賴
before AsyncReceptionPort.h 
1 2 3 4 5 6 7 8 9 10 11 12 class  AsyncReceptionPort  { private :    CSocket m_socket;      Packet m_packet;      int  m_segmentSize;      ... public :    AsyncReceptionPort ();     void  Run ()      ...  }; 
AsynchReceptionPort.cpp 
1 2 3 4 5 6 7 8 9 10 11 12 13 void  AsyncReceptionPort::Run ()      for (int  n = 0 ; n < m_segmentSize; ++n) {         int  bufferSize = m_bufferMax;          if  (n = m_segmentSize - 1 )             bufferSize = m_remainingSize;          m_socket.receive (m_receiveBuffer, bufferSize);          m_packet.mark ();          m_packet.append (m_receiveBuffer,bufferSize);          m_packet.pack ();     }     m_packet.finalize ();  } 
對於以上的程式碼如果我想要修改 Run() 並加入測試,就必須面對 Socket 的依賴
after 在 C++ 中我們可以透過把 AsyncReceptionPort 做成一個類別模板來避開這個問題
AsynchReceptionPort.h 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 template <typename  SOCKET> class  AsyncReceptionPortImpl  { private :    SOCKET m_socket;      Packet m_packet;      int  m_segmentSize;      ... public :     AsyncReceptionPortImpl ();     void  Run ()      ... }; template <typename  SOCKET>void  AsyncReceptionPortImpl<SOCKET>::Run () {     for (int  n = 0 ; n < m_segmentSize; ++n) {          int  bufferSize = m_bufferMax;         if  (n = m_segmentSize - 1 )             bufferSize = m_remainingSize;          m_socket.receive (m_receiveBuffer, bufferSize);          m_packet.mark ();          m_packet.append (m_receiveBuffer,bufferSize);          m_packet.pack ();     }     m_packet.finalize ();  } typedef  AsyncReceptionPortImpl<CSocket> AsyncReceptionPort;
有了這個步驟,我們就可以在測試檔案中用一個 FakeSocket 來替換 Socket
TestAsynchReceptionPort.cpp 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include  "AsyncReceptionPort.h"  class  FakeSocket  { public :    void  receive (char  *, int  size)   }; TEST (Run,AsyncReceptionPort) {     AsyncReceptionPortImpl<FakeSocket> port; ...  } 
24. 文字重定義 一些直譯式語言提供了很不錯的解依賴途徑,在直譯的時候,方法可以被重新定義
before 假設我們想測試 Account 裡面的 deposit() ,但過程中的 report_deposit() 卻不是我們關心的對象
Account.rb 
1 2 3 4 5 6 7 8 9 10 11 12 class  Account     def  report_deposit (value )               end      def  deposit (value )          @balance  += value          report_deposit(value)     end      def  withdraw (value )          @balance  -= value     end   end 
after 我們想要架空 report_deposit() 的行為,那麼我們只需要在測試檔案中重新定義
AccountTest.rb 
1 2 3 4 5 6 7 8 9 10 11 12 require  "runit/testcase"  require  "Account" class  Account     def  report_deposit (value )               end  end class  AccountTest  < RUNIT : :TestCase      end