【问题标题】:How best design a scalable class?如何最好地设计一个可扩展的类?
【发布时间】:2010-07-07 15:05:27
【问题描述】:

我的意思是: 我现在基本上有一个具有太多属性和功能的类。为了保持高性能和易于理解,它需要以某种方式缩小。但我仍然需要所有这些属性和方法。 现在是这样的:

class Apple

  float seedCount;
  ...
  ...about 25 variables and properties here.
  void Update() <-- a huge method that checks for each property and updates if so

在大多数情况下,类几乎不需要这些属性。在某些情况下,需要能够非常有选择地增长并获得一个特征或失去一个特征。 我想出的唯一解决方案是创建一堆类并在其中放置一些属性。我只在需要其中一个属性时才初始化这个类对象,否则它保持为空。

class Apple

  Seed seed;

因此而产生的许多问题: 我必须不断地检查每一个对象,并为每一帧提供特色。如果种子没有初始化,我不需要为它计算任何东西。如果是,我必须这样做。 如果我决定将超过 1 个属性/功能放入 Seed 类,我还需要检查其中的每一个。 它只会变得越来越复杂。因此,我遇到的问题是,我需要对所有功能进行精细控制,并且无法将它们智能地拆分为更大的子类。任何形式的子类都只会包含一堆需要检查和更新的属性。 由于需要如此精细的控制,我不能完全创建 Apple 的子类。创建与属性组合一样多的类将是疯狂的。 我的主要目标:我想要简短的代码。

【问题讨论】:

  • 根据您对 Tesserex 的回答,我认为您可能错误地处理了问题。您正在尝试使用组合和/或继承,但似乎都不是解决方案。您能否提供一个更明确的示例来说明您想要实现的目标,并详细说明您的商店做什么,以及从现实世界的角度来看不同的组件应该如何交互?
  • dl.dropbox.com/u/700060/ResponsibilitiesClass.rar 好的,我制作了一个完整的 vs2010 解决方案,它以缩写形式显示了我当前的解决方案。它会编译,您可以单步执行该程序以查看会发生什么。如果你没有vs2010,可以看c#源码,我评论了所有希望改进的地方。 (对于愚蠢的解决方案/项目名称感到抱歉)对于改进这一点的任何想法感到高兴。它非常笨重。

标签: c# architecture class-design


【解决方案1】:

创建与属性组合一样多的类是很疯狂的。

听起来您可能正在寻找Decorator Pattern. 它的目的是使管理可以具有许多不同属性组合的对象变得更容易,而不会呈指数级增长。每个属性或行为只有一个小子类(不一定是一个 C# 属性,只是可以组合在一起的东西),然后您可以在运行时将它们组合在一起。

在您的情况下,每个 Apple 装饰器类都将覆盖您的 Update 方法,并对其部分进行必要的计算,然后调用 base.Update 将其传递给下一行。

您的最终答案在很大程度上取决于您的“Apple”到底是什么。

【讨论】:

  • 它就像商店类:它有这么多属性,因为它必须能够根据客户的选择吐出这么多不同种类的东西。
  • 假设我有 30 个类,那么当我想要它们时,性能不会受到很大影响吗?
  • 我在实现时才意识到装饰器并不能解决最大的问题:设置属性。访问属性真的很痛苦,我需要记住装饰对象的实际类型并在每次我想设置属性时进行强制转换。
  • 初始化这样的复合类几乎是不可能的。
  • 好的,如果是商店,您是说您要尝试“选择商品 1 2 和 3 的商店”、“选择商品 2 4 和 7 的商店”、a “选择商品 1、2、4、5、8 购物”等?这是怎么回事?
【解决方案2】:

在我的other answer 中查看了您的 cmets 和示例后,我考虑了装饰器模式以及它的使用方式以及您希望事情如何工作。我得出的结论是,装饰器不适合这个目的。我正在考虑策略。我修改了之前的sample code给大家看看。

我已经完全摆脱了装饰器。 Broodfather 抽象类仍然存在。它有两个额外的属性:IBroodfatherMagicAbility 和 IBroodfatherBloodthirstAbility。这两个属性将允许您访问与这些能力相关的不同属性,但是这一切的关键是实现这些能力的策略可以在运行时改变(参见Strategy pattern)。

有两个类分别实现了嗜血和魔法的“策略”。

  • IBroodfatherBloodthirstAbility.cs - 这是所有“嗜血策略”都必须实现的接口。
  • BroodfatherNonBloodThristy.cs - 实现非嗜血属性的类。
  • BroodfatherBloodThristy.cs - 实现嗜血属性的类。

  • IBroodfatherMagicAbility.cs - 这是所有“魔法策略”都必须实现的接口。

  • BroodfatherNonMagical.cs - 实现非魔法策略的类。
  • BroodfatherMagical.cs - 实现魔法策略的类。

  • BasicBroodfather.cs - 这与前面的示例类似,不同之处在于现在创建实例时,会将魔法和嗜血属性设置为非魔法和非嗜血策略对象的新实例。

  • Program.cs 是显示类以及如何在运行时换入和换出不同策略的驱动程序。

我认为你会发现它更适合你想要的工作方式。

【讨论】:

  • 这增加了很多代码。一个新类只是为了表明没有使用扩展?老实说,我想我在绕圈子。每次我考虑作曲时,我都会想到一百万件关于它的坏事。但装饰器也是如此。这让我相信我需要更好地定义问题......似乎我无法轻松访问每个属性+拥有很多属性+小类。当我有时间时,我将围绕这个示例开始编写游戏。现在,任何一种解决方案都可以工作,我在工作中实现了装饰器。
  • 真正的问题可能是我如何将所有属性动态组合成一些合理的输出。假设我的亲父有 30 个不同的扩展名。一旦他攻击,这些扩展需要以某种方式协同工作。如果他有更好的延伸,他不应该扔火球。好吧,就像我说的:我太模糊了,想要的太多了。一旦我开始真正的游戏,我就会知道更多。不过,+1 让我思考。
【解决方案3】:

你可以在 Apple 类中使用嵌套类 http://msdn.microsoft.com/en-us/library/ms173120(VS.80).aspx

【讨论】:

  • 这无济于事,因为它实际上增加了代码量
【解决方案4】:

我认为这里的关键是你试图将所有东西都放在一个类中。因此,班级必须不断检查它有什么和没有什么。解决方案是创建已经知道它们是否具有特定事物的子类或装饰器。这样他们就不必每次都检查它。

因为您有这么多可能以不同方式组合的属性,听起来装饰器解决方案更适合您。

【讨论】:

    【解决方案5】:

    我认为您走在正确的道路上:作曲。你用其他需要的类组成你的类。但是您还需要相应地委派责任。在您的示例中,应该是 Seed 类负责检查其内部状态,而 Apple 只是委托给它。

    至于可选功能的问题,也许你可以使用null objects 代替空引用。这样就不用每次都检查null,代码更加一致。

    【讨论】:

    • 感谢空对象的提示,我也想知道如何克服这个问题。
    【解决方案6】:

    我一直在思考这个问题,并想出了一个替代解决方案。这可能有点不正统和反对象导向,但如果你不是胆小,请继续阅读......

    以 Apple 示例为基础:Apple 类可以包含许多属性,这些属性可以分为相关组。例如,我使用了一个 Apple 类,其中一些属性与苹果种子相关,而其他属性与苹果皮相关。

    1. 苹果
      一种。种子
      1。获取种子计数
      a2。 ...
      湾。皮肤
      b1。获取皮肤颜色
      b2。 ...

    我正在使用字典对象来存储所有苹果属性。

    我编写了扩展方法来定义属性的访问器,使用不同的类来保持它们的分离和组织。

    通过对属性使用字典,您可以在任何时候遍历存储的所有属性(如果您必须检查所有属性,因为这听起来像是您在更新方法中需要的那样)。不幸的是,您丢失了数据的强类型(至少在我的示例中我这样做了,因为我使用的是 Dictionary。您可以为所需的每种类型使用单独的字典,但这需要更多的管道代码来路由属性访问正确的字典。

    使用扩展方法来定义属性的访问器允许您为每个逻辑类别的属性分离代码。这样可以将事物组织成独立的相关逻辑块。

    这是我想出的一个示例,用于测试这将如何工作,并给出了标准警告,即如果您要继续沿此路径进行稳健化(验证、错误处理等)。

    Apple.cs

    namespace ConsoleApplication1
    {
        using System.Collections.Generic;
        using System.Text;
    
        public class Apple
        {
            // Define the set of valid properties for all apple objects.
            private static HashSet<string> AllowedProperties = new HashSet<string>(
                new string [] {
                    "Color",
                    "SeedCount"
                });
    
            // The main store for all properties
            private Dictionary<string, string> Properties = new Dictionary<string, string>();
    
            // Indexer for accessing properties
            // Access via the indexer should be restricted to the extension methods!
            // Unfortunately can't enforce this by making it private because then extension methods wouldn't be able to use it as they are now.
            public string this[string prop]
            {
                get
                {
                    if (!AllowedProperties.Contains(prop))
                    {
                        // throw exception
                    }
    
                    if (Properties.ContainsKey(prop))
                    {
                        return this.Properties[prop];
                    }
                    else
                    {
                        // TODO throw 'property unitialized' exeception || lookup & return default value for this property || etc.
    
                        // this return is here just to make the sample runable
                        return "0"; 
                    }
                }
    
                set
                {
                    if (!AllowedProperties.Contains(prop))
                    {
                        // TODO throw 'invalid property' exception
    
                        // these assignments are here just to make the sample runable
                        prop = "INVALID";
                        value = "0";
                    }
    
                    this.Properties[prop] = value.ToString();
                }
            }
    
            public override string ToString()
            {
                StringBuilder sb = new StringBuilder();
    
                foreach (var kv in this.Properties)
                {
                    sb.AppendFormat("{0}={1}\n", kv.Key, kv.Value);
                }
    
                return sb.ToString();
            }
        }
    }
    

    AppleExtensions.cs

    namespace AppleExtensionMethods
    {
        using System;
        using ConsoleApplication1;
    
       // Accessors for Seed Properties
        public static class Seed
        {
            public static float GetSeedCount(this Apple apple)
            {
                return Convert.ToSingle(apple["SeedCount"]);
            }
    
            public static void SetSeedCount(this Apple apple, string count)
            {
                apple["SeedCount"] = count;
            }
        }
    
       // Accessors for Skin Properties
        public static class Skin
        {
            public static string GetSkinColor(this Apple apple)
            {
                return apple["Color"];
            }
    
            public static void SetSkinColor(this Apple apple, string color)
            {
                apple["Color"] = ValidSkinColorOrDefault(apple, color);
            }
    
            private static string ValidSkinColorOrDefault(this Apple apple, string color)
            {
                switch (color.ToLower())
                {
                    case "red":
                        return color;
    
                    case "green":
                        return color;
    
                    default:
                        return "rotten brown";
                }
            }
        }
    }
    

    这是试驾:

    Program.cs

    namespace ConsoleApplication1
    {
        using System;
        using AppleExtensionMethods;
    
        class Program
        {
            static void Main(string[] args)
            {
                Apple apple = new Apple();
    
                apple.SetSkinColor("Red");
                apple.SetSeedCount("8");
    
                Console.WriteLine("My apple is {0} and has {1} seed(s)\r\n", apple.GetSkinColor(), apple.GetSeedCount());
    
                apple.SetSkinColor("green");
                apple.SetSeedCount("4");
    
                Console.WriteLine("Now my apple is {0} and has {1} seed(s)\r\n", apple.GetSkinColor(), apple.GetSeedCount());
    
                apple.SetSkinColor("blue");
                apple.SetSeedCount("0");
    
                Console.WriteLine("Now my apple is {0} and has {1} seed(s)\r\n", apple.GetSkinColor(), apple.GetSeedCount());
    
                apple.SetSkinColor("yellow");
                apple.SetSeedCount("15");
    
                Console.WriteLine(apple.ToString());
    
                // Unfortunatly there is nothing stopping users of the class from doing something like that shown below.
                // This would be bad because it bypasses any behavior that you have defined in the get/set functions defined
                // as extension methods.
                // One thing in your favor here is it is inconvenient for user of the class to find the valid property names as
                // they'd have to go look at the apple class. It's much easier (from a lazy programmer standpoint) to use the
                // extension methods as they show up in intellisense :) However, relying on lazy programming does not a contract make.
                // There would have to be an agreed upon contract at the user of the class level that states, 
                //  "I will never use the indexer and always use the extension methods!"
                apple["Color"] = "don't panic";
                apple["SeedCount"] = "on second thought...";
    
                Console.WriteLine(apple.ToString());
            }
        }
    }
    

    从 7 月 11 日开始处理您的评论(日期,而不是商店):)
    在您提供的示例代码中,有一条注释指出:

    “如你所见,我不能打电话 “怪物”​​上的基本育母方法

    你意识到你可以在那个时候做这样的事情:

    BasicBroodmother bm = monster as BasicBroodmother;
    if (bm != null)
    {
        bm.Eat();
    }
    

    您的代码没有太多内容,(我知道这只是一个示例),但是当我查看它时,我觉得您应该能够改进设计。我的直接想法是为 broodmother 提供一个抽象类,其中包含所有 broodmother 共有的任何属性/动作的默认实现。然后,专门的育母,如魔法育母,将包含任何特定于魔法育母的专门属性/动作,但也继承自抽象类,并在必要时覆盖必要的基本属性/动作。

    我会看一下用于设计动作的策略模式,以便可以根据怪物的类型交换动作(即吃、产卵、攻击等行为)。

    [编辑 7/13]
    现在没有时间详细介绍(需要睡觉),但我整理了一些 sample code 展示了不同的方法。

    代码组成:

    • Broodfather.cs - 抽象类,其中包含不同 Broodfathers“类型”共有的所有内容。
    • BasicBroodFather.cs - 从 Broodfather 继承的具体类。
    • BroodfatherDecorator.cs - 由所有 Broodfather 装饰器继承的抽象类。
    • MagicalBroodfather.cs - 这个类用“魔法”装饰/包装一个育父
    • BloodthirstyBroodfather.cs - 这个类用“bloodthirst”装饰/包装一个育父
    • program.cs - 演示了两个示例:第一个示例从一个被魔法包裹的基本育父开始,然后被嗜血包裹。第二个从一个基本的育父开始,然后将其包裹在另一个顺序嗜血,然后是魔法。

    【讨论】:

    • 罗伯特好主意。我记得我确实遇到了类似的问题,并最终得到了类似的结构。是的,最大的问题是类型安全(必须将所有内容都作为字符串)。 +1
    • 这仅适用于可以轻松转换为字符串的属性。一直转换也可能对性能非常不利。想想一直转换一个 4x4 矩阵需要做多少工作。或者是一个有很多属性的类。通过反思,这样的问题很容易。我可以以编程方式遍历所有装饰器类的所有属性。但由于性能,我也不会这样做。
    • @Blub True,这就是为什么我提到可以实现特定类型的字典和一些“路由”代码来控制使用哪个字典。这可以通过使用附加的“路由字典”来实现,该字典将属性名称作为键,将强类型的“属性字典”作为值。索引器需要输入为对象,而不是现在的字符串。然后在索引器的 get/set 中,您需要在路由字典中查找属性名称,以获取适当的强类型字典以从中检索值或将值设置为。
    • @your 7/11 编辑:不,我不能。至少没有达到我想要的效果。所有实际变量都位于最底部的装饰器中。在我的示例中,对任何东西的每个调用都会向下路由,直到它被某个装饰器覆盖。如果我调用 bm.Eat(),它将调用 MagicalBroodmother 的 BasicBroodmother 基类的 Eat() 方法。然而,Eat() 的实际实现和重写方法在于 monster.decorater,它也是一个 BasicBroodmother。
    • 您的 Eat() 调用不会引发任何异常,但可能会使用未使用或未初始化的变量。我会在模式中称这种未定义的行为。您对使用具有通用实现的抽象类的建议与我的 BasicBroodmother 基本相同。它将覆盖 Mother 类提供的所有常用方法。 MagicalBroodmother 已经被设计为覆盖特定于 MagicalBroodmother 的 Mother 的特定方法。
    【解决方案7】:

    也许你的方法不是他们应该的?

    如果将Seed类与Apple类分开,为什么不把使用Seed信息的方法也移到Seed类中呢?

    如果这些方法需要有关其他 Apple 属性的信息,您可以将其作为参数传递。

    通过这样做,我想您可以消除初始化检查...

    这是一本关于如何解决这类问题的好书:

    Refactoring

    【讨论】:

    • 正如我所提到的:每个属性都需要(几乎)所有其他属性。将您建议的所有类如此强大地耦合在一起是没有意义的。它只会使属性之间的交互更加困难,导致代码更加复杂。
    • 抱歉,Blub,我想我不明白你的问题。你能给我们举一个更接近现实的例子吗?通常,当一个类有太多的属性时,这意味着它有太多的责任。也许我们可以找到与您的类更好的类比,并帮助您分离属性(如果是这样的话)?
    • 你能看看我添加到 Tesserex 答案的评论吗?我猜它有点隐藏在折叠区域下。
    【解决方案8】:

    我的主要目标:我想要短代码。

    选项:

    1. 将所有函数重写为静态函数并为每个函数创建一个类。
    2. 用 Perl 重写您的代码库。
    3. 删除所有 cmets。

    【讨论】:

    • sry,我刚刚编辑了帖子,说我几乎无法创建与属性组合一样多的子类。另外:我真的没有那么多功能。我只是有一个巨大的更新功能和很多属性。
    • @mcandre,正如我所见,他要求提供可维护的代码。将随机的东西设为静态当然无助于可维护性。
    • 要求可维护代码与要求短代码不同。询问前 Perl 程序员。
    • @mcandre,当然,但我认为这就是他的意思。他想让当前的实现更好(可扩展、可维护等)。使东西静态化当然无济于事。
    • 没错,我认为没有人想要不可维护的代码是不言而喻的。如果要求快速代码,我可能不想要“用汇编程序重写你的代码库”之类的答案。我希望你能理解,但还是谢谢你。特别是当我添加一个“c#”标签时。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-03-26
    • 1970-01-01
    • 2011-09-01
    • 2023-03-03
    • 2020-02-06
    • 2021-04-08
    • 2011-02-14
    相关资源
    最近更新 更多