Какая альтернатива наследованию статических методов?


84

Я понимаю, что наследование статических методов не поддерживается в C #. Я также прочитал ряд обсуждений (в том числе здесь), в которых разработчики заявляют о необходимости этой функциональности, на что обычно отвечают: «Если вам нужно статическое наследование членов, в вашем дизайне есть изъян».

Хорошо, учитывая, что ООП не хочет, чтобы я даже думал о статическом наследовании, я должен сделать вывод, что моя очевидная потребность в нем указывает на ошибку в моем дизайне. Но я застрял. Я был бы очень признателен за помощь в решении этой проблемы. Вот вызов ...

Я хочу создать абстрактный базовый класс (назовем его Fruit), который инкапсулирует некоторый сложный код инициализации. Этот код нельзя поместить в конструктор, поскольку часть его будет зависеть от вызовов виртуальных методов.

Fruit будет унаследован другими конкретными классами (Apple, Orange), каждый из которых должен предоставлять стандартный фабричный метод CreateInstance () для создания и инициализации экземпляра.

Если бы статическое наследование членов было возможным, я бы поместил фабричный метод в базовый класс и использовал бы вызов виртуального метода к производному классу, чтобы получить тип, из которого должен быть инициализирован конкретный экземпляр. Клиентский код просто вызовет Apple.CreateInstance () для получения полностью инициализированного экземпляра Apple.

Но очевидно, что это невозможно, поэтому может кто-нибудь объяснить, как мой дизайн должен измениться, чтобы обеспечить ту же функциональность.


2
Хочу отметить, что утверждение «Этот код нельзя поместить в конструктор, поскольку некоторые из них будут полагаться на вызовы виртуальных методов» неверно. Вы МОЖЕТЕ вызывать виртуальные методы внутри конструктора. (Вам нужно будет тщательно разрабатывать свои вызовы, потому что вы можете иметь частично инициализированный класс при вызове виртуальных методов, но он поддерживается.) См. Ответ Равадре для более подробной информации.
Рубен

Ответы:


62

Одна идея:

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();

Никаких дополнительных фабричных классов не требуется.


9
IMHO, лучше переместить универсальный тип в метод CreateInstance, а не на уровне класса. (Как я сделал в своем ответе).
Frederik Gheysels

1
Ваше предпочтение отмечено. Было бы полезно дать краткое объяснение ваших предпочтений.
Мэтт Хэмсмит

9
Поступая так, вы не «загрязняете» остальной класс; параметр типа необходим только в методе 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, но это кажется неизбежным.
Мэтт Хэмсмит

18

Переместите фабричный метод из типа и поместите его в собственный класс 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 в его собственный класс Factory (хотя я думаю, что это предпочтительнее - разделение проблем -), вы можете поместить его в класс 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 (). Однако это точный мой обман.
Джон Гитцен, 04

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

WebRequestКласс и его производные типы в .NET BCL представляют собой хороший пример того , как такого рода конструкции могут быть выполнены относительно хорошо.

У WebRequestкласса есть несколько подклассов, включая HttpWebRequestи FtpWebReuest. Теперь этот 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

Прежде всего, отсутствие статических инициализаторов, которые могут быть виртуальными, не означает, что у вас не может быть «стандартных» методов-членов, которые могут быть перегружены. Во-вторых, вы можете вызывать свои виртуальные методы из конструкторов, и они будут работать должным образом, так что здесь нет никаких проблем. В-третьих, вы можете использовать дженерики для создания типобезопасной фабрики.
Вот код, который использует метод factory + member 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
Как правило, лучше избегать вызова виртуальных методов из конструкторов. См. Blogs.msdn.com/abhinaba/archive/2006/02/28/540357.aspx
TrueWill,

Это правда, это может быть сложно, хотя это возможно и всегда будет работать так, как должно, проблема в том, чтобы понять, что означает «это должно». В общем, я стараюсь избегать вызова каких-либо более сложных методов (даже не виртуальных, т. Е. Потому, что они часто создаются таким образом, чтобы они могли генерировать исключение, и мне не нравится, что мои ctors что-либо бросают) , но это решение разработчика.
Marcin Deptuła

0

Я бы сказал, что лучше всего создать виртуальный / абстрактный метод Initialise для класса фруктов, который должен быть вызван, а затем создать внешний класс `` фабрика фруктов '' для создания экземпляров:


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;
    }
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.