9/14/2009 1:32:02 AM

[設計案例] 生命遊戲#2, OOP版的範例程式

Microsoft.NET | C# | 我的作品 | 技術隨筆 | 物件導向

還好,第一版的程式沒有難產。這版的目的很簡單,就是把題目實作出來,同時我會盡量套用物件導向的理念去設計程式的結構,而不是只把結果算出來而已。其實我一直覺的,這類生命模擬的程式,是非常適合用OOPL來實作的範例,大概OOPL所有強調的特性 (封裝、繼承、多型、動態聯結... 等等) 都用的到,算是完美的應用範例題吧!

不過很奇怪的,我特地 GOOGLE 了一下,不知 OOPL 高手都不屑寫這種範例還是怎樣,找到的範例程式,不管用什麼語言 (C/C++/Java/C#都有) 寫的,清一色都很沒有物件導向的 fu ... 好吧,只好自己來寫一個。

第一步,一定是先看看你的程式,分析出需要那些類別/物件,及它們之間的關係。比較正規的作法就是 UML 的 UseCase 了。不過這範例其實不大,我就直接跳到 Class Diagram 了 (因為VS2008剛好有現成的...)... 主要的類別有兩個: World (世界) 及 Cell (細胞)。

World 就是給 Cell 生活的空間,我們只訂義一個有限大小的二維空間,就一個 M x N 的棋盤這樣。而 Cell 則是一個細胞,描述單一一個細胞本身,在各種不同的條件下會有什麼反應。先貼一下 class diagram:

 

image  
圖1. class diagram (World & Cell)

老實說,這張圖還蠻乏善可陳的,World對外公開的介面,大概包含了幾個主要功能,就是取得指定座標的 Cell (GetCell), 及把目前的整個 World 狀態印出來 (ShowMaps) 的 method 而已。而 Cell 的公開介面,不外乎是它目前是活著還是死的,還有它的建構式,及呼叫後會把狀態轉移到下一次狀態的 method。

其它都是 World / Cell 互相溝通用,或是 Init 用的 Method / Prop, 就不多作介紹。先來看看主程式,扮演上帝的你,如何讓這堆單細胞生物,在你的世界裡活起來:

Game Of Life 主程式[copy code]
   1:  static void Main(string[] args)
   2:  {
   3:      int worldSizeX = 30;
   4:      int worldSizeY = 30;
   5:      int maxGenerationCount = 100;
   6:      World realworld = new World(worldSizeX, worldSizeY);
   7:      for (int generation = 1; generation <= maxGenerationCount; generation++)
   8:      {
   9:          realworld.ShowMaps(string.Format("Generation: {0}", generation));
  10:          Thread.Sleep(1000);
  11:          for (int positionX = 0; positionX < worldSizeX; positionX++)
  12:          {
  13:              for (int positionY = 0; positionY < worldSizeY; positionY++)
  14:              {
  15:                  // do day pass
  16:                  Cell cell = realworld.GetCell(positionX, positionY) as Cell;
  17:                  cell.OnNextStateChange();
  18:              }
  19:          }
  20:      }
  21:  }

 

主程式我還沒把不相干的動作刪掉,也才廿一行... line 1 ~ 5 只是初始值,line 6 建立整個世界,之後就每跑完一個世代 (generation) 就休息一秒鍾,繼續下一次進化。這樣隨著時間的過去,畫面上會一直更新整個世界的狀態... 直到只定的次數到了為止。

 

class World 的部份就沒什麼特別的,就只是把一個二維陣列包裝一下而已。直接貼 Code 就混過去吧 XD,一樣沒有刪掉程式碼,原 CODE 照貼:

class World 的程式碼[copy code]
   1:  public class World
   2:  {
   3:      private int SizeX = 0;
   4:      private int SizeY = 0;
   5:      private Cell[,] _map;
   6:      public World(int maxPosX, int maxPosY)
   7:      {
   8:          this._map = new Cell[maxPosX, maxPosY];
   9:          this.SizeX = maxPosX;
  10:          this.SizeY = maxPosY;
  11:          for (int posX = 0; posX < maxPosX; posX++)
  12:          {
  13:              for (int posY = 0; posY < maxPosY; posY++)
  14:              {
  15:                  this._map[posX, posY] = new Cell(this, posX, posY);
  16:              }
  17:          }
  18:      }
  19:      internal void PutOn(Cell item, int posX, int posY)
  20:      {
  21:          if (this._map[posX, posY] == null)
  22:          {
  23:              this._map[posX, posY] = item;
  24:              item.PosX = posX;
  25:              item.PosY = posY;
  26:          }
  27:          else
  28:          {
  29:              throw new ArgumentException();
  30:          }
  31:      }
  32:      public Cell GetCell(int posX, int posY)
  33:      {
  34:          if (posX >= this.SizeX) return null;
  35:          if (posY >= this.SizeY) return null;
  36:          if (posX < 0) return null;
  37:          if (posY < 0) return null;
  38:          return this._map[posX, posY];
  39:      }
  40:      public void ShowMaps(string title)
  41:      {
  42:          Console.Title = title;
  43:          Console.SetWindowSize(this.SizeX * 2, this.SizeY);
  44:          Console.SetCursorPosition(0, 0);
  45:          Console.Clear();
  46:          for (int y = 0; y < this.SizeY; y++)
  47:          {
  48:              for (int x = 0; x < this.SizeX; x++)
  49:              {
  50:                  Cell item = this.GetCell(x, y);
  51:                  Console.SetCursorPosition(x * 2, y);
  52:                  Console.Write(item.IsAlive? "●":"○");
  53:              }
  54:          }
  55:      }
  56:  }

 

接下來是封裝每個細胞本身跟環境互動的影響,把上一篇講的規則對應成程式碼的樣子。先來看看 CODE:

class Cell 的程式碼[copy code]
   1:  public class Cell //: Life
   2:  {
   3:      protected World CurrentWorld { get; private set; }
   4:      internal int PosX = 0;
   5:      internal int PosY = 0;
   6:      private const double InitAliveProbability = 0.2D;
   7:      private static Random _rnd = new Random();
   8:      public Cell(World world, int posX, int posY) //: base(world, posX, posY)
   9:      {
  10:          this.CurrentWorld = world;
  11:          // setup world
  12:          this.PosX = posY;
  13:          this.PosY = posY;
  14:          this.CurrentWorld.PutOn(this, posX, posY);
  15:          this.IsAlive = (_rnd.NextDouble() < InitAliveProbability);
  16:      }
  17:      public bool IsAlive { get; private set; }
  18:      protected IEnumerable<Cell> FindNeighbors()
  19:      {
  20:          foreach (Cell item in new Cell[] {
  21:              this.CurrentWorld.GetCell(this.PosX -1, this.PosY-1),
  22:              this.CurrentWorld.GetCell(this.PosX, this.PosY-1),
  23:              this.CurrentWorld.GetCell(this.PosX+1, this.PosY-1),
  24:              this.CurrentWorld.GetCell(this.PosX-1, this.PosY),
  25:              this.CurrentWorld.GetCell(this.PosX+1, this.PosY),
  26:              this.CurrentWorld.GetCell(this.PosX-1, this.PosY+1),
  27:              this.CurrentWorld.GetCell(this.PosX, this.PosY+1),
  28:              this.CurrentWorld.GetCell(this.PosX+1, this.PosY+1)})
  29:          {
  30:              if (item != null) yield return item;
  31:          }
  32:          yield break;
  33:      }
  34:      public void OnNextStateChange()
  35:      {
  36:          int livesCount = 0;
  37:          foreach (Cell item in this.FindNeighbors())
  38:          {
  39:              if (item.IsAlive == true) livesCount++;
  40:          }
  41:          if (this.IsAlive == true && livesCount <1)
  42:          {
  43:              //孤單死亡:如果細胞的鄰居小於一個,則該細胞在下一次狀態將死亡。
  44:              this.IsAlive = false;
  45:          }
  46:          else if (this.IsAlive == true && livesCount >= 4)
  47:          {
  48:              //擁擠死亡:如果細胞的鄰居在四個以上,則該細胞在下一次狀態將死亡。
  49:              this.IsAlive = false;
  50:          }
  51:          else if (this.IsAlive == true && (livesCount == 2 || livesCount == 3))
  52:          {
  53:              //穩定:如果細胞的鄰居為二個或三個,則下一次狀態為穩定存活。
  54:              //this.IsAlive = true;
  55:          }
  56:          else if (this.IsAlive == false && livesCount == 3)
  57:          {
  58:              //復活:如果某位置原無細胞存活,而該位置的鄰居為三個,則該位置將復活一細胞。
  59:              this.IsAlive = true;
  60:          }
  61:          else
  62:          {
  63:              // ToDo: 未定義的狀態? assert
  64:          }
  65:      }
  66:  }

這裡開始應用到 OOPL 第一個特性: 封裝。從程式碼可以看到,主要的邏輯都被包在裡面了,就 Game Of Life 裡提到的四條規則。

程式這樣寫起來,比那些作業的標準答案看起來舒服多了吧? 雖然行數多了一些,不過看起來比較有 OO 的樣子了。當然只是看起來爽是沒用的,這樣的架構,到目前為只除了邏輯清楚一點之外,還看不到其它很明顯的好處。不過當這個規責稍微複雜一點,OOPL的優點就會被突顯出來了。

下回,把題目做點變化,再來看看程式該如何調整…   ((待續))

--
附件: 範例程式碼



Comments

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
Loading






精選文章

RUN! PC 文章及範例下載
2010/07. 結合檔案及資料庫的交易處理
2010/05. TxF讓檔案系統也能達到交易控制
2010/04. 生產者 vs 消費者 - 執行緒的供需問題
2008/11. 生產線模式的多執行緒應用
2008/09. 用ThreadPool發揮CPU運算能力
2008/06. SEMAPHORE在ASP.NET的應用
2008/04. 以ASP.NET開發同步WEB應用程式

如何學好 "寫程式" 系列
#1. 該如何學好 "寫程式" ??
#2. 為什麼 programmer 該學資料結構 ??
#3. 進階應用 - 資料結構 + 問題分析
#4. 你的程式夠 "可靠" 嗎?

#5. 善用 TRACE / ASSERT

安德魯是誰?

Andrew Wu | Create Your Badge

我喜歡鑽研物件導向、軟體工程及作業系統等相關技術。我會在這裡發表我的研究心得,也當作我自己的學習筆記。


Recent comments

Comment RSS