2013年1月4日 星期五

[軟體工程]如何寫出意圖使人犯錯的程式碼

因為一些因素,讓我學習到一段程式碼,這段程式碼就是這篇主題的主角,意圖使人出錯的程式碼。
這段code的規格是
初始化玩遊戲後,確保有一個使用者在遊戲裡面,所以要在init結束前呼叫join。
至於規格為什麼要這樣設計,不要問我XD
 原本的code是python的。我把python的code轉成用c++來表達,所以function前面都會有virtual,以符合python情形。




#include <cstdio>
class Father{
public:
 virtual int init(){
  printf("father init start\n");
  printf("father init end\n");
  return join();
 }
 virtual int join(){
  printf("father join start\n");
  printf("father join end\n");
 }
};

int main(){
 Father father;
 father.init();

}


坦白講,這段程式碼看起來還好。除了一個function做兩件事情外。也沒什麼。執行順序也如我們的預期
father init start 
father init end
father join start 
father join end
可是如果他是基底類別。會有子類別繼承他
#include <cstdio>
class Father{
public:
 virtual int init(){
  printf("father init start\n");
  printf("father init end\n");
  return join();
 }
 virtual int join(){
  printf("father join start\n");
  printf("father join end\n");
 }
};

class Child:public Father{
public:
 virtual int init(){
  printf("child init start\n");
  int ret = Father::init();
  // do something It need.
  printf("child init end\n");
  return ret;
 }
 virtual int join(){
  printf("child join start\n");
  Father::join();
  printf("child join end\n");
 }
};

int main(){
 Child child;
 child.init();

}

程式碼看起來還好,只是執行解果有點詭異。恩....子類別join發生在子類別init完成前。
child init start
father init start
father init end
child join start
father join start
father join end
child join end
child init end

假設你不care這情形,過了一段時間,你突然想新增一些東西像這樣
#include <cstdio>
class Father{
public:
 virtual int init(){
  printf("father init start\n");
  printf("father init end\n");
  return join();
 }
 virtual int join(){
  printf("father join start\n");
  printf("father join end\n");
 }
};

class Child:public Father{
public:
 int special;
 virtual int init(){
  printf("child init start\n");
  int ret = Father::init();
  special = 1;
  // do something It need.
  printf("child init end\n");
  return ret;
 }
 virtual int join(){
  printf("child join start\n");
                Father::join();
  printf("special = %d\n", special);
  printf("child join end\n");
 }
};

int main(){
 Child child;
 child.init();

}


你在子類別新增一個變數,初始化時需要給他一個值,子類別的join的時候會需要他。你會發現這樣寫,special的值不合你預期(廢話,因為join發生在special = 1之前)。

怎麼辦,把special = 1移到Father::init前面就好了。 恩....
問題好像解決了,也沒什麼呀

有一天,父類別要做一些更動,要多加一個屬性name,父類別的name是從1開始算,子類別的name是從2開始算。
像這樣


#include <cstdio>
class Father{
public:
 int name;
 virtual int init(){
  printf("father init start\n");
  name = 1;
  printf("father init end\n");
  return join();
 }
 virtual int join(){
  printf("father join start\n");
  printf("name = %d\n", name);
  printf("father join end\n");
 }
};

class Child:public Father{
public:
 int special;
 virtual int init(){
  printf("child init start\n");
  name = 2;
  special = 1;
  int ret = Father::init();
  // do something It need.
  printf("child init end\n");
  return ret;
 }
 virtual int join(){
  printf("child join start\n");
                name += 1;
  Father::join();
  printf("special = %d\n", special);
  printf("child join end\n");
 }
};

int main(){
 Child child;
 child.init();

}


你會發現,印出來的 name 值會是父類別的值再加一,而不是子類別的值再加一。怎麼辦?
在join那邊初始化?

不行,join在那邊會被呼叫很多次,所以在join初始化會一直讓那個變數被初始化
所以不能在那初始化。

所以怎麼辦?
不要呼叫父類別的函數好了,把父類別的函數程式碼直接複製到子類別好了
很好,問題解決了,只是這樣幹嘛用繼承呢?
如果你哪天父類別的init改了,恭喜你。你要把父類別的code重新複製到子類別去。
每改一次,複製一次 ...
不小心改錯,你的code就gg了。

有人會說,我就是要初始化完呼叫,那該怎麼辦呢?
事實上也不難。

#include <cstdio>
class Father{
public:
 int name;
 virtual int init(){
  printf("father init start\n");
  name = 1;
  printf("father init end\n");
 }
 virtual int join(){
  printf("father join start\n");
  printf("name = %d\n", name);
  printf("father join end\n");
 }
 
 int init_and_join(){
  //the functino can't be overrided.
  init();
  return join();
 }
};

class Child:public Father{
public:
 int special;
 virtual int init(){
  printf("child init start\n");
  int ret = Father::init();
  name = 2;
  special = 1;
  // do something It need.
  printf("child init end\n");
  return ret;
 }
 virtual int join(){
  printf("child join start\n");
  name += 1;
  Father::join();
  printf("special = %d\n", special);
  printf("child join end\n");
 }
};

int main(){
 Child child;
 child.init_and_join();

}

這樣問題就解決了,我們只要呼叫init_and_join()就可以了。他會幫我們完成初始化和join的動作。如果你在寫java,你還可以再init_and_join()前面加個final,防止子類別override,導致行為改變。

沒有留言:

張貼留言