none
設計模式中,每次要用到子類別的屬性都要向下轉型? RRS feed

  • 問題

  • 我有4個類別分別是Chara(腳色)、PlayerChara(玩家腳色)、Spell(施法)、PlayerSpell(玩家施法)

    「腳色類別」為「玩家腳色類別」的父類別,「施法類別」為「玩家施法類別」的父類別

    這是我的父類別初始化

        Chara Self;//施法者

        /// <summary>
        /// 初始化施法,傳入施法ID與施法者
        /// </summary>
        public Spell(int _spellID, Chara _self)
        {
            ID = _spellID;
            Self = _self;
        }

        public string GetSpeller()//取得施法者的名稱

    { return Self.Name;}

    這是我的子類別

        public PlayerSpell(int _spellID, PlayerChara _self)
            : base(_spellID, _self)
        {
        }

    但現在問題來了,我的PlayerSpell子類別的建構式是傳入PlayerChara確保傳入的腳色為玩家腳色而不是其他腳色,但是當我要讓玩家施法時消耗魔力就這樣下

    Self.Consume();//施法者消耗魔力

    但由於一般腳色是沒有魔力的,所以不會消耗魔力,只有玩家腳色會消耗魔力,而我的Self是宣告為Chara而非子類別PlayerChara,所以我必須要向下轉型 ((PlayerChara)Self).Consume();
    有這樣的問題是正常的嗎?如果有問題納我該怎麼去設計結構才能避免這樣的問題?

    2015年11月16日 下午 01:54

解答

  • 簡單來說 "向下轉型幾乎就是錯的, 只有在完全搞不出來的時候才會這麼硬幹". 因為要向下轉型就表示抽象的過程有問題.

    其實你的設計我有點難理解, 照理說, 主角應該是 "角色", 而不是 "法術".

    我們從中文來理解這過程:

    通常你會講 "魔法師施展了法術" , 而不會表達成 "法術從魔法師身上發出來了" . 所以主詞是誰就看得出來的吧.

    還有另一個在你設計上的缺點是, 你的類別耦合程度太深,  PlayerChara 和 PlayerSpell 根本是綁死的. 那如果玩家的法術有很多種怎麼辦 ? 然後有些法術, 玩家也能用, NPC 也能用, 怎麼辦 ?

    我用以下的程式碼表示一個簡單的設計方式, 把衍生類別的耦合程度降低, 僅止於示範, 這對你來說不會是最佳做法, 因為你的需求只有你自己最懂, 所謂的最佳做法是以需求面為準的, 對我的情境最佳, 對你的情境則未必. 而且也不想用太複雜或艱深的技巧做這個示範, 剩下就靠你自己理解了. 但你可以發現我的程式碼沒有向下轉型這件事.

    namespace ConsoleApplication2
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("產生玩家");
                string playername = "小叮噹";
                Role player = RoleFactory.GetSpellRole(RoleType.Player, playername);
                Console.WriteLine(string.Format("玩家 {0} 產生成功", playername));
    
                Console.WriteLine("產生敵人");
                string npcname = "魯夫";
                Role npc = RoleFactory.GetSpellRole(RoleType.Npc, npcname);
                Console.WriteLine(string.Format("敵人 {0} 產生成功", npcname));
                for (int i = 0; i < 11; i++ )
                {
                    player.Execute();
                }
    
                npc.Execute();
    
    
                Console.ReadLine();
            }
        }
    
    
        public abstract class Role
        {
            public string Name { get; private set; }
            protected List<ISpell> spells;
            protected Role(string name)
            {
                Name = name;
    
            }
    
            public abstract void Execute();
    
        }
    
        public class PlayerRole : Role
        {
            public PlayerRole(string name) : base(name)
            {
                mp = 100;
                spells = new List<ISpell>();
                spells.Add(SpellFactory.GetSpell(SpellType.Fire));
                spells.Add(SpellFactory.GetSpell(SpellType.Water));
            }
    
            private int mp;
            public override void Execute()
            {
                foreach (var spell in spells)
                {
                    if (mp < 5)
                    {
                        Console.WriteLine(string.Format("{0} : {1} ", Name, "沒魔等死"));
                    }
                    else
                    {
                        spell.Do(Name);
                        mp -= 5;
                        Console.WriteLine(string.Format("{0} : {1} : {2} ", Name, "MP還剩下", mp));
                    }
                }
    
            }
        }
    
        public class NpcRole : Role
        {
            public NpcRole(string name) : base(name)
            {
                spells = new List<ISpell>();
                spells.Add(SpellFactory.GetSpell(SpellType.Water));
                spells.Add(SpellFactory.GetSpell(SpellType.Stone));
            }
    
            public override void Execute()
            {
                foreach (var spell in spells)
                {
                    spell.Do(Name);
                }
            }
        }
    
    
        public enum RoleType
        {
            Player,
            Npc
        }
    
        public class RoleFactory
        {
            public static Role GetSpellRole(RoleType roleType, string name)
            {
                if (roleType == RoleType.Player)
                {
                    return new PlayerRole(name);
                }
                else
                {
                    return new NpcRole(name);
                }
            }
        }
    
        public interface ISpell
        {
            void Do(string name);
    
        }
    
        public enum SpellType
        {
            Fire,
            Water,
            Stone
        }
    
        public class Fire : ISpell
        {
            public void Do(string name)
            {
                Console.WriteLine(string.Format("{0} : {1} ", name, "放火放火"));
            }
        }
    
    
        public class Water : ISpell
        {
            public void Do(string name)
            {
                Console.WriteLine(string.Format("{0} : {1} ", name, "噴水噴水"));
            }
        }
    
        public class Stone : ISpell
        {
            public void Do(string name)
            {
                Console.WriteLine(string.Format("{0} : {1} ", name, "丟石頭丟石頭"));
            }
        }
    
        public class SpellFactory
        {
            public static ISpell GetSpell(SpellType spellType)
            {
                switch (spellType)
                {
                    case SpellType.Water:
                        return new Water();
                    case SpellType.Stone:
                        return new Stone();
                    default:
                        return new Fire();
                }
            }
        }
    
    }


    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    2015年11月16日 下午 09:42
    版主

所有回覆

  • 不是很了解你真正的需求. 但照一般的情況來看. 只要是 "需要轉型為子類別", 那就代表有問題.

    這種設計多半應該會把 "角色" 以類別方式呈現, 而 "施法" 以介面方式呈現. 衍生自 "角色" 的類別若具備 "施法" 的技能, 就實作 "施法介面" .

    也就是我要判斷由 "角色" 衍生的子類別會不會施法, 只要看他有沒有實作 "施法介面" 就行了. 不用管子類別倒底是甚麼.

    當然, 我這麼說是有點籠統, 因為要了解全盤的狀況才能精準地看出應該怎麼設計.

     

    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    2015年11月16日 下午 02:17
    版主
  • 呼叫時父類別去依賴子類別的屬性是件很奇怪的事。

    你應該要先把你的那些物件整理一下,做一次抽象化,把共有的屬性和方法切出來,變成介面,然後再由各子類別實作。

    先不要去想什麼設計模式的問題,先把介面和抽象化弄清楚,否則告訴你用哪個設計模式也沒用。


    強力監督SQL Injection問題!!

      • 小朱的技術隨手寫:http://www.dotblogs.com.tw/regionbbs/
      • 雲端學堂Facebook: http://www.facebook.com/studyazure

    2015年11月16日 下午 03:02
    版主
  • 我的腳色有分「玩家腳色」與「敵方腳色」
    法術也有分「玩家的施法」與「敵方腳色的施法」
    玩家腳色有魔力的設定,而敵方腳色則沒有
    腳色類別中有擁有的法術清單
    「玩家腳色」用的是「玩家法術」,「敵方腳色」用的是「敵方法術」
    這是我簡化後的設定,在玩家法術那裏施法用到了向下轉型為子類別了
    /////////////////////////////////////////////////////////////////////腳色父類別
    public abstract  class Chara 
    {
    public string Name{get;private set;}//腳色名稱
    protected List<Spell> SpellList;//施法清單
        /// <summary>
        /// 起始設定
        /// </summary>
        public virtual void IniChara()
        {
    Spell spellA=new Spell();
    Spell spellB=new Spell();
    SpellList.add(spell);
    }
    public void ExecuteSpell(int _index)
    {
    SpellList[_index].Execute();
    }

    }
    ////////////////////////////////////////////////////////腳色子類別
    public class  PlayerChara:Chara
    {
    public int MP{get;private set;}
    public void ConsumeMP(int _value)
    {
    Mp-=_value;
    }
    }
    ///////////////////////////////////////////////////////////法術父類別
    public abstract Spell
    {
    Chara Speller;
    public Spell(Chara _speller)
    {
    Speller=_speller;
    }
    public abstract void Execute()
    {
    Debug.Log(Speller.Name+"執行施法");
    }
    }
    /////////////////////////////////////////////////////////////////法術子類別
    public class PlayerSpell:Spell
    {
    public int  ConsumeMP{get;private set;}

    public PlayerChara(PlayerChara _speller)
       :base(_speller);
    {
    }
    public override void Execute()
    {
    ((PlayerChara)Chara).ConsumeMP( ConsumeMP);
    Debug.Log(Speller.Name+"執行施法且消耗mp");
    }
    }


    另外,一般的情況來看. 只要是 "需要轉型為子類別", 那就代表有問題.那什麼時候會真的需要向下轉型為子類別


    • 已編輯 Scozirge 2015年11月16日 下午 03:14
    2015年11月16日 下午 03:13
  • 簡單來說 "向下轉型幾乎就是錯的, 只有在完全搞不出來的時候才會這麼硬幹". 因為要向下轉型就表示抽象的過程有問題.

    其實你的設計我有點難理解, 照理說, 主角應該是 "角色", 而不是 "法術".

    我們從中文來理解這過程:

    通常你會講 "魔法師施展了法術" , 而不會表達成 "法術從魔法師身上發出來了" . 所以主詞是誰就看得出來的吧.

    還有另一個在你設計上的缺點是, 你的類別耦合程度太深,  PlayerChara 和 PlayerSpell 根本是綁死的. 那如果玩家的法術有很多種怎麼辦 ? 然後有些法術, 玩家也能用, NPC 也能用, 怎麼辦 ?

    我用以下的程式碼表示一個簡單的設計方式, 把衍生類別的耦合程度降低, 僅止於示範, 這對你來說不會是最佳做法, 因為你的需求只有你自己最懂, 所謂的最佳做法是以需求面為準的, 對我的情境最佳, 對你的情境則未必. 而且也不想用太複雜或艱深的技巧做這個示範, 剩下就靠你自己理解了. 但你可以發現我的程式碼沒有向下轉型這件事.

    namespace ConsoleApplication2
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("產生玩家");
                string playername = "小叮噹";
                Role player = RoleFactory.GetSpellRole(RoleType.Player, playername);
                Console.WriteLine(string.Format("玩家 {0} 產生成功", playername));
    
                Console.WriteLine("產生敵人");
                string npcname = "魯夫";
                Role npc = RoleFactory.GetSpellRole(RoleType.Npc, npcname);
                Console.WriteLine(string.Format("敵人 {0} 產生成功", npcname));
                for (int i = 0; i < 11; i++ )
                {
                    player.Execute();
                }
    
                npc.Execute();
    
    
                Console.ReadLine();
            }
        }
    
    
        public abstract class Role
        {
            public string Name { get; private set; }
            protected List<ISpell> spells;
            protected Role(string name)
            {
                Name = name;
    
            }
    
            public abstract void Execute();
    
        }
    
        public class PlayerRole : Role
        {
            public PlayerRole(string name) : base(name)
            {
                mp = 100;
                spells = new List<ISpell>();
                spells.Add(SpellFactory.GetSpell(SpellType.Fire));
                spells.Add(SpellFactory.GetSpell(SpellType.Water));
            }
    
            private int mp;
            public override void Execute()
            {
                foreach (var spell in spells)
                {
                    if (mp < 5)
                    {
                        Console.WriteLine(string.Format("{0} : {1} ", Name, "沒魔等死"));
                    }
                    else
                    {
                        spell.Do(Name);
                        mp -= 5;
                        Console.WriteLine(string.Format("{0} : {1} : {2} ", Name, "MP還剩下", mp));
                    }
                }
    
            }
        }
    
        public class NpcRole : Role
        {
            public NpcRole(string name) : base(name)
            {
                spells = new List<ISpell>();
                spells.Add(SpellFactory.GetSpell(SpellType.Water));
                spells.Add(SpellFactory.GetSpell(SpellType.Stone));
            }
    
            public override void Execute()
            {
                foreach (var spell in spells)
                {
                    spell.Do(Name);
                }
            }
        }
    
    
        public enum RoleType
        {
            Player,
            Npc
        }
    
        public class RoleFactory
        {
            public static Role GetSpellRole(RoleType roleType, string name)
            {
                if (roleType == RoleType.Player)
                {
                    return new PlayerRole(name);
                }
                else
                {
                    return new NpcRole(name);
                }
            }
        }
    
        public interface ISpell
        {
            void Do(string name);
    
        }
    
        public enum SpellType
        {
            Fire,
            Water,
            Stone
        }
    
        public class Fire : ISpell
        {
            public void Do(string name)
            {
                Console.WriteLine(string.Format("{0} : {1} ", name, "放火放火"));
            }
        }
    
    
        public class Water : ISpell
        {
            public void Do(string name)
            {
                Console.WriteLine(string.Format("{0} : {1} ", name, "噴水噴水"));
            }
        }
    
        public class Stone : ISpell
        {
            public void Do(string name)
            {
                Console.WriteLine(string.Format("{0} : {1} ", name, "丟石頭丟石頭"));
            }
        }
    
        public class SpellFactory
        {
            public static ISpell GetSpell(SpellType spellType)
            {
                switch (spellType)
                {
                    case SpellType.Water:
                        return new Water();
                    case SpellType.Stone:
                        return new Stone();
                    default:
                        return new Fire();
                }
            }
        }
    
    }


    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    2015年11月16日 下午 09:42
    版主
  • 謝謝!這樣完全解決我的問題了
    不過有些地方不太清楚為什麼,像是
    1.為什麼需要都透過RoleFactory來取得角色,在外面直接new一個PlayerRole不好嗎?
    2.為什麼施法是繼承自ISpell,而PlayerRole是繼承自Role,Stone,Fire,Water那些可以是繼承自abstract的Spell嗎?

    2015年11月17日 上午 02:16
  • 1)不透過RoleFactory來產生取得角色,直接從外面new一個PlayerRole,會有個問題是相依性,如果今天PlayerRole的產生方式要做調整或是要增加其它RoleType,那麼你有多少隻AP就可能都要翻出來改,你可以看一下設計模式中的工廠模式,就可以了解
    2)施法是一個動作行為,不同的法術差別只在於它的法術內容,所以建立一個ISpell介面,然後讓不同法術繼承ISpell介面去實作它的行為即可

    微軟免費線上課程

    HTML5 & JavaScript程式開發實戰(MyBook)

    開發ASP.NET您要瞭解的基楚

    http://www.dotblogs.com.tw/ian (MyBlog)

    2015年11月17日 上午 03:09
  • (1) 18 已經把我用 Factory 的原因講的很清楚了, 降低耦合度是寫物件導向程式很重要的觀念.

    (2) 除了 18 說的原因外, 你可以想像一件事, ISpell 如果用抽象類別, 依照我的程式碼推演, 他就只會有一個抽象方法. 所以完全沒有共用程式碼必要, 寫成抽象類別就感覺很多餘.

    (3) 其實我們偏愛介面更勝於抽象類別. 不過這要解釋就故事有點長了. 你恐怕得花點時間去體會.


    在現實生活中,你和誰在一起的確很重要,甚至能改變你的成長軌跡,決定你的人生成敗。 和什麼樣的人在一起,就會有什麼樣的人生。 和勤奮的人在一起,你不會懶惰; 和積極的人在一起,你不會消沈; 與智者同行,你會不同凡響; 與高人為伍,你能登上巔峰。

    2015年11月17日 上午 04:14
    版主