静态方法继承的正确替代方案是什么?

90
我了解在C#中不支持静态方法继承。我也阅读过许多讨论(包括这里),其中开发人员声称需要这种功能,而典型的回答是“如果您需要静态成员继承,则设计存在缺陷”。
好的,既然面向对象编程不希望我甚至考虑静态继承,我必须得出结论,我的显然需要指向我的设计错误。 但是,我卡住了。我真的很感激有人能够帮助我解决这个问题。以下是挑战...
我想创建一个抽象的基类(我们称之为Fruit),该类封装了一些复杂的初始化代码。 这些代码无法放置在构造函数中,因为其中一些将依赖于虚方法调用。
Fruit将被其他具体类(Apple,Orange)继承,每个类都必须公开一个标准工厂方法CreateInstance()来创建和初始化实例。
如果可行的话,我会将工厂方法放在基类中,并使用虚方法调用派生类来获取必须从中初始化具体实例的类型。客户端代码只需调用Apple.CreateInstance()即可获得一个完全初始化的Apple实例。
但显然这是不可能的,那么有人能否请解释我的设计如何改变以适应相同的功能。

3
我想指出的是,“这段代码不能放在构造函数中,因为其中一些代码将依赖于虚方法调用”这个说法是不正确的。你可以在构造函数中调用虚方法。(你必须仔细设计你的调用,因为当虚方法被调用时,你可能会有一个部分初始化的类,但它是被支持的。)更多细节请参见Ravadre的答案。 - Ruben
8个回答

65

一个想法:

public abstract class Fruit<T>
    where T : Fruit<T>, new()
{
    public static T CreateInstance()
    {
        T newFruit = new T();
        newFruit.Initialize();  // Calls Apple.Initialize
        return newFruit;
    }

    protected abstract void Initialize();
}

public class Apple : Fruit<Apple>
{
    protected override void Initialize() { ... }
}

并这样调用:
Apple myAppleVar = Fruit<Apple>.CreateInstance();

不需要额外的工厂类。

10
在我看来,最好将泛型类型移到CreateInstance方法中,而不是放在类级别上(就像我在我的回答中所做的那样)。 - Frederik Gheysels
1
已经记录下了你的偏好。更详细地解释一下你的偏好会更加受欢迎。 - Matt Hamsmith
10
这样做可以避免“污染”整个类;类型参数仅在Create方法中是必需的(至少在此示例中是如此)。 除此之外,我认为: Fruit.Create<Apple>(); 比以下写法更易读: Fruit<Apple>.Create(); - Frederik Gheysels
2
你不能将Apple构造函数设置为不公开,因为where T : Fruit<T>, new()规定了T必须具有公共构造函数。 @Matt Hamsmith - 除非你删除protected Apple() { },否则你的代码无法编译。我已经在VS中测试过了。 - Tohid
1
@Tohid - 你说得对。我已经编辑过了。这确实使得Apple不能使用Fruit CreateInstance静态工厂方法来构建,但似乎是不可避免的。 - Matt Hamsmith
显示剩余7条评论

20

将工厂方法从类型中移出,并将其放入自己的Factory类中。

public abstract class Fruit
{
    protected Fruit() {}

    public abstract string Define();

}

public class Apple : Fruit
{
    public Apple() {}

    public override string Define()
    {
         return "Apple";
    }
}

public class Orange : Fruit
{
    public Orange() {}

    public override string Define()
    {
         return "Orange";
    }
}

public static class FruitFactory<T> 
{
     public static T CreateFruit<T>() where T : Fruit, new()
     {
         return new T();
     }
}

但是,就我而言,我认为没有必要将Create方法移至单独的工厂类中(尽管我认为这是可取的——责任分离),您可以将其放在Fruit类中:

public abstract class Fruit
{

   public abstract string Define();

   public static T CreateFruit<T>() where T : Fruit, new()
   {
        return new T();
   }

}

还有,为了确认它是否有效:

    class Program
    {
        static void Main( string[] args )
        {
            Console.WriteLine (Fruit.CreateFruit<Apple> ().Define ());
            Console.WriteLine (Fruit.CreateFruit<Orange> ().Define ());

            Console.ReadLine ();
        }        
    }

1
你的代码无法编译。你需要使用 where T : new() 子句。然而,这与我的代码完全相同。 - John Gietzen
1
虽然可以创建一个静态的FruitFactory类,但我更喜欢使用IFruitFactory接口以及可实例化的FruitFactory类。你最终可能只会得到一个FruitFactory类,它的CreateFruit方法不引用其对象实例,因此也可以成为静态方法,但如果需要提供多种创建水果的方式,或者如果水果的创建需要保持状态,则使用实例方法可能会很有用。 - supercat

4
我会这样做。
 public abstract class Fruit() {
      public abstract void Initialize();
 }

 public class Apple() : Fruit {
     public override void Initialize() {

     }
 }

 public class FruitFactory<T> where T : Fruit, new {
      public static <T> CreateInstance<T>() {
          T fruit = new T();
          fruit.Initialize();
          return fruit;  
      }
 } 


var fruit = FruitFactory<Apple>.CreateInstance()

3
为什么不创建一个工厂类(模板),并且拥有一个create方法?
FruitFactory<Banana>.Create();

3

WebRequest 类及其在 .NET BCL 中派生的类型是如何实现这种设计的一个很好的例子。

WebRequest 类有几个子类,包括 HttpWebRequestFtpWebReuest。现在,这个 WebRequest 基类也是一种工厂类型,并公开了一个静态的 Create 方法(实例构造函数被隐藏,这是工厂模式所要求的)。

public static WebRequest Create(string requestUriString)
public static WebRequest Create(Uri requestUri)

这个Create方法返回WebRequest类的一个具体实现,并使用URI(或URI字符串)来确定要创建和返回的对象类型。

这最终导致以下使用模式:

var httpRequest = (HttpWebRequest)WebRequest.Create("http://stackoverflow.com/");
// or equivalently
var httpRequest = (HttpWebRequest)HttpWebWebRequest.Create("http://stackoverflow.com/");

var ftpRequest = (FtpWebRequest)WebRequest.Create("ftp://stackoverflow.com/");
// or equivalently
var ftpRequest = (FtpWebRequest)FtpWebWebRequest.Create("ftp://stackoverflow.com/");

我个人认为这是一个很好的解决问题的方法,而且它似乎确实是.NET Framework创建者首选的方法。


3
首先,没有静态初始化程序可以是虚拟的,并不意味着您不能有“标准”的成员方法,这些方法可以被重载。其次,您可以从构造函数中调用虚拟方法,并且它们将按预期工作,因此这里没有问题。第三,您可以使用泛型来拥有类型安全的工厂。
下面是一些代码,使用工厂+成员Initialize()方法,该方法由构造函数调用(并且它是受保护的,因此您不必担心在创建对象后再次调用它):

abstract class Fruit
{
    public Fruit()
    {
        Initialize();
    }

    protected virtual void Initialize()
    {
        Console.WriteLine("Fruit.Initialize");
    }
}

class Apple : Fruit
{
    public Apple()
        : base()
    { }

    protected override void Initialize()
    {
        base.Initialize();
        Console.WriteLine("Apple.Initialize");
    }

    public override string ToString()
    {
        return "Apple";
    }
}

class Orange : Fruit
{
    public Orange()
        : base()
    { }

    protected override void Initialize()
    {
        base.Initialize();
        Console.WriteLine("Orange.Initialize");
    }

    public override string ToString()
    {
        return "Orange";
    }
}

class FruitFactory
{
    public static T CreateFruit<T>() where T : Fruit, new()
    {
        return new T();
    }
}

public class Program
{

    static void Main()
    {
        Apple apple = FruitFactory.CreateFruit<Apple>();
        Console.WriteLine(apple.ToString());

        Orange orange = new Orange();
        Console.WriteLine(orange.ToString());

        Fruit appleFruit = FruitFactory.CreateFruit<Apple>();
        Console.WriteLine(appleFruit.ToString());
    }
}

1
通常最好避免从构造函数中调用虚方法。请参见http://blogs.msdn.com/abhinaba/archive/2006/02/28/540357.aspx。 - TrueWill
没错,这可能有点棘手,但是它是可行的,并且总是按照应该的方式工作,问题在于理解什么是“应该”。一般来说,我倾向于避免调用任何更复杂的方法(即使是非虚拟的,因为它们通常以一种允许它们抛出异常的方式构建,而我不喜欢我的构造函数抛出任何异常),但这是开发人员的决定。 - Marcin Deptuła

1
我觉得最好的做法是在水果类上创建一个虚拟/抽象的初始化方法,必须调用该方法,然后创建一个外部的“水果工厂”类来创建实例:

public class Fruit
{
    //other members...
    public abstract void Initialise();
}

public class FruitFactory()
{
    public Fruit CreateInstance()
    {
        Fruit f = //decide which fruit to create
        f.Initialise();

        return f;
    }
}

0

所有这些想法都弥补了语言的明显缺陷,使用生硬的语言结构。

如果我想要一个苹果,我不会在水果上调用方法。为什么我要学习类的家族谱呢?我也不想像疯子一样重复自己或者复制整个层次结构的构造函数。计算机是为了让我远离这种无聊的事情而发明的。

现在已经注意到并且做得正确的两种语言是PHP和Swift。它们都支持静态继承和协变。

至于那些所谓纯洁教义的旧的OOP圣书,你可以把它们烧掉,因为只要能让我编写优雅的代码并且编译通过就是好的。


目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接