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

還好,第一版的程式沒有難產。這版的目的很簡單,就是把題目實作出來,同時我會盡量套用物件導向的理念去設計程式的結構,而不是只把結果算出來而已。其實我一直覺的,這類生命模擬的程式,是非常適合用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 主程式
        static void Main(string[] args)
        {
            int worldSizeX = 30;
            int worldSizeY = 30;
            int maxGenerationCount = 100;

            World realworld = new World(worldSizeX, worldSizeY);
            for (int generation = 1; generation <= maxGenerationCount; generation++)
            {
                realworld.ShowMaps(string.Format("Generation: {0}", generation));
                Thread.Sleep(1000);

                for (int positionX = 0; positionX < worldSizeX; positionX++)
                {
                    for (int positionY = 0; positionY < worldSizeY; positionY++)
                    {
                        // do day pass
                        Cell cell = realworld.GetCell(positionX, positionY) as Cell;
                        cell.OnNextStateChange();
                    }
                }
            }
        }

 

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

 

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

class World 的程式碼
    public class World
    {
        private int SizeX = 0;
        private int SizeY = 0;
        private Cell[,] _map;
        public World(int maxPosX, int maxPosY)
        {
            this._map = new Cell[maxPosX, maxPosY];
            this.SizeX = maxPosX;
            this.SizeY = maxPosY;

            for (int posX = 0; posX < maxPosX; posX++)
            {
                for (int posY = 0; posY < maxPosY; posY++)
                {
                    this._map[posX, posY] = new Cell(this, posX, posY);
                }
            }
        }

        internal void PutOn(Cell item, int posX, int posY)
        {
            if (this._map[posX, posY] == null)
            {
                this._map[posX, posY] = item;
                item.PosX = posX;
                item.PosY = posY;
            }
            else
            {
                throw new ArgumentException();
            }
        }

        public Cell GetCell(int posX, int posY)
        {
            if (posX >= this.SizeX) return null;
            if (posY >= this.SizeY) return null;
            if (posX < 0) return null;
            if (posY < 0) return null;

            return this._map[posX, posY];
        }

        public void ShowMaps(string title)
        {
            Console.Title = title;
            Console.SetWindowSize(this.SizeX * 2, this.SizeY);
            Console.SetCursorPosition(0, 0);
            Console.Clear();

            for (int y = 0; y < this.SizeY; y++)
            {
                for (int x = 0; x < this.SizeX; x++)
                {
                    Cell item = this.GetCell(x, y);
                    Console.SetCursorPosition(x * 2, y);
                    Console.Write(item.IsAlive? "●":"○");
                }
            }
        }
    }

 

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

class Cell 的程式碼
    public class Cell //: Life
    {
        protected World CurrentWorld { get; private set; }

        internal int PosX = 0;
        internal int PosY = 0;

        private const double InitAliveProbability = 0.2D;


        private static Random _rnd = new Random();
        public Cell(World world, int posX, int posY) //: base(world, posX, posY)
        {
            this.CurrentWorld = world;

            // setup world
            this.PosX = posY;
            this.PosY = posY;
            this.CurrentWorld.PutOn(this, posX, posY);

            this.IsAlive = (_rnd.NextDouble() < InitAliveProbability);
        }

        public bool IsAlive { get; private set; }

        protected IEnumerable<Cell> FindNeighbors()
        {
            foreach (Cell item in new Cell[] {
                this.CurrentWorld.GetCell(this.PosX -1, this.PosY-1),
                this.CurrentWorld.GetCell(this.PosX, this.PosY-1),
                this.CurrentWorld.GetCell(this.PosX+1, this.PosY-1),
                this.CurrentWorld.GetCell(this.PosX-1, this.PosY),
                this.CurrentWorld.GetCell(this.PosX+1, this.PosY),
                this.CurrentWorld.GetCell(this.PosX-1, this.PosY+1),
                this.CurrentWorld.GetCell(this.PosX, this.PosY+1),
                this.CurrentWorld.GetCell(this.PosX+1, this.PosY+1)})
            {
                if (item != null) yield return item;
            }
            yield break;
        }

        public void OnNextStateChange()
        {
            int livesCount = 0;
            foreach (Cell item in this.FindNeighbors())
            {
                if (item.IsAlive == true) livesCount++;
            }

            if (this.IsAlive == true && livesCount <1)
            {
                //孤單死亡:如果細胞的鄰居小於一個,則該細胞在下一次狀態將死亡。
                this.IsAlive = false;
            }
            else if (this.IsAlive == true && livesCount >= 4)
            {
                //擁擠死亡:如果細胞的鄰居在四個以上,則該細胞在下一次狀態將死亡。
                this.IsAlive = false;
            }
            else if (this.IsAlive == true && (livesCount == 2 || livesCount == 3))
            {
                //穩定:如果細胞的鄰居為二個或三個,則下一次狀態為穩定存活。
                //this.IsAlive = true;
            }
            else if (this.IsAlive == false && livesCount == 3)
            {
                //復活:如果某位置原無細胞存活,而該位置的鄰居為三個,則該位置將復活一細胞。
                this.IsAlive = true;
            }
            else
            {
                // ToDo: 未定義的狀態? assert
            }
        }
    }

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

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

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

--
附件: 範例程式碼






Facebook Pages

Edit Post (Pull Request)

Post Directory