4

这是我的程序:

class Program
{
    //DESIGN 1
    abstract class AFoo
    {
        public string Bar { get; set; }
        public abstract string SayHi();
    }

    class LoudFoo : AFoo
    {
        public override string SayHi()
        {
            return this.Bar.ToUpper();
        }
    }

    class QuietFoo : AFoo
    {
        public override string SayHi() { return this.Bar.ToLower(); }
    }

    //DESIGN 2
    class Foo{
        public string Bar { get; set; }
        public Func<Foo, string> SayHi { get; set; }
    }

    static void Main(string[] args)
    {
        //USING DESIGN 1
        var quietFoo2 = new QuietFoo{ Bar = "Mariane"};

        var loudFoo2 = new LoudFoo{ Bar = "Ginger"};

        Console.WriteLine(quietFoo2.SayHi());
        Console.WriteLine(loudFoo2.SayHi());

        //USING DESIGN 2
        var quietFoo = new Foo
        {
            Bar = "Felix",
            SayHi = (f) => { return f.Bar.ToLower(); }
        };
        var loudFoo = new Foo
        {
            Bar = "Oscar",
            SayHi = (f) => { return f.Bar.ToUpper(); }
        };
        Console.WriteLine(quietFoo.SayHi(quietFoo));
        Console.WriteLine(loudFoo.SayHi(loudFoo));

    }
}

我可以完成“同样的事情”——实际上不是完全相同的事情,但类似的事情会走两条不同的路线。

设计 1)我可以创建一个抽象类,强制该类的实现者如何 SayHi()

- 或者 -

设计 2)我可以创建一个类来定义一个 SayHi 属性,它是一个函数。(我称它为代表——但我不确定这是否是正确的术语)

设计 1 困扰着我,因为它可能导致类的泛滥

然而....

设计 2 让我感到困扰,因为当我必须让 Foo 实际上是 SayHi() 时,我觉得它是多余的。

felix.SayHi(felix)

我的问题是使用设计 1 或设计 2 是否更好——或者两者都不使用。当我说得更好时,我说的是在能够维护我的程序方面更实用。我在创建不同的类时遇到了这个问题,这些类将用于从不同的云 API(Google Drive、Box.com、DropBox)下载文件——起初我创建了单独的类,但后来我走了另一条路。

4

6 回答 6

5

当涉及到这些类型的设计选择时,我发现根据您尝试建模的问题域来考虑对象是有帮助的。您已经展示了 LoudFoo 和 QuietFoo 在一个行为上的不同,但这是一个故意简化的示例。在实际系统中,您可能有令人信服的理由认为两个对象在概念上是不同的。

在前一个版本中,SayHi 是类行为的内在部分,如果该行为的性质以某种方式与其内部状态交互,则它是适当的。也许 SayHi 的实现取决于特定于该派生类类型的对象的属性。

在后一个版本中,SayingHi 更像是一个可以分发给各种实例的工具。当没有其他理由区分不同类型的 Foo 实例时,这是合适的。

Stream是前一种模式的一个很好的例子,它提供的各种方法是流操作的本质所固有的。各种派生类将利用不同的状态来实现它们的方法。

比较器是后一种模式的一个很好的例子,其中许多不同的对象类型都希望使用比较的概念进行操作。使用此功能的类不需要有任何其他共同点,除了想要使用这种特定类型的行为。


关于你提出这个问题的具体应用程序,多类方法怎么办?如果出现冗余,这可能表明可以以更好地模拟问题的不同方式来考虑责任。如果不了解有关特定问题的其他详细信息,很难说更多,但可能一个好的方法是将您提出的两者结合起来,由一些单个类负责操作的排序和一个单独的层次结构(或一组接口实现) 实施特定于每个服务的操作。本质上,接口(或基类)将您将分别传入的所有各种委托组合在一起。这类似于 StreamReader 如何获取 Stream 并使用在 Stream 上操作的其他行为来增强它。

于 2013-06-22T14:10:53.130 回答
4

在设计 1 中,您的行为在类内部实现,但在设计 2 中,您要求调用者定义行为。

我倾向于设计 1,因为它将行为实现保持在类中的黑盒状态。每当有人实例化一个新对象时,设计 2 可能会更改您的实现。我也不喜欢实现是调用者的责任。

如果您实现SayHi更改的方式,您在设计 1 中只有一个地方可以更改它,但如果您使用设计 2,您的代码中可能有多个地方可以更改它。

于 2013-06-22T14:12:48.857 回答
2

根据经验:更少的代码 == 更易于维护。

在您拥有的特定情况下,您还拥有一个解耦设计 - 如何的逻辑与SayHi说它的类分开,让您可以选择组合行为。低耦合也是通常要维护的代码的标志。

我更喜欢第二种设计。

于 2013-06-22T13:45:46.237 回答
1

第一个设计更标准,逻辑一致(意味着任何其他使用LoudFoo(or QuietFoo) 的类在任何地方都会有相同的结果。但是,它是可重用的,但只能在其继承路径中。意味着来自LoudFoo(比如说DerivedLoudFoo不能使用)SayHi中定义的逻辑QuietFoo

这听起来很简单,但以后可能会很麻烦。您可以在此处阅读我的回答, 了解真实案例。

第二个更具可扩展性,但缺点是它可以有不同的行为。不要将它用于核心业务流程(例如插入/更新/删除),因为它很难调试或修改。Framework但是,对于某些方法,如OnAfterInsert, ,最好在 level 中使用OnAfterSubmit

于 2013-06-24T10:04:53.627 回答
1

假设这不仅仅是一个完全虚构的示例,并且实际上可以翻译成真实的代码(我很难弄清楚它可能是什么),我发现选项 2 很糟糕。

  • 您几乎可以将任何内容分配给SayHi,包括与 a 无关的 lambda Bar,这似乎不是您的初衷。

  • 您基本上是在尝试将一个精心制作的功能钉插入一个古老的面向对象的孔中。使用 lambda 可以将数据 ( Bar) 与对其进行操作的行为分开,这是有效的功能实践,但是通过创建Foo.SayHi属性,您又回到了 OO 风格,试图将它们中的两个封装回同一个类中。似乎有点做作。

于 2013-06-24T12:42:55.463 回答
0

设计 2 让我感到困扰,因为当我必须让 Foo 实际上是 SayHi() 时,我觉得它是多余的。

如果Foo类被重新定义为

class Foo
    public property Bar as string
    public property SayHi as func(of string)
end class

然后可以使用闭包来创建和调用函数,而无需作为函数参数SayHi传递:Foo

dim Bar = "Felix"

dim Felix as new Foo with {
    .Bar = Bar,
    .SayHi = function() Bar.toLower
}

dim FelixSays = Felix.SayHi()

我倾向于设计 1,因为它将行为实现保持在类中的黑盒状态。

设计 2 始终准备好黑盒行为实现,例如在工厂方法内部:

function CreateQuietFoo(Bar as string) as Foo
    return new Foo with {
        .Bar = Bar,
        .SayHi = function() Bar.toLower
    }
end function

dim Felix = CreateQuietFoo("Felix")
dim Oscar = CreateQuietFoo("Oscar")

这样调用者不必提供SayHi创建安静Foo实例的方法,他只需使用CreateQuietFoo工厂方法。

我的问题是使用设计 1 或设计 2 是否更好——或者两者都不使用。

如果您更喜欢Composition 而不是 Inheritance ,请使用 Design 2 。它使代码更加灵活。

于 2015-09-14T11:05:04.083 回答