Яка правильна альтернатива успадкуванню статичних методів?


83

Я розумію, що успадкування статичних методів не підтримується в C #. Я також прочитав ряд обговорень (в тому числі тут), в яких розробники заявляють про необхідність цієї функціональності, на що типовою відповіддю є "якщо вам потрібне статичне успадкування членів, у вашому дизайні є недолік".

Добре, враховуючи, що ООП не хоче, щоб я навіть думав про статичне успадкування, я повинен зробити висновок, що моя очевидна потреба в ньому вказує на помилку в моєму дизайні. Але, я застряг. Я був би дуже вдячний за допомогу у вирішенні цієї проблеми. Ось виклик ...

Я хочу створити абстрактний базовий клас (назвемо його 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
Роблячи це, ви не забруднюєте решту класу; параметр type необхідний лише у методі Create (принаймні в цьому прикладі). Поруч з цим, я думаю, що: Fruit.Create <Apple> (); стає читабельнішим тоді: Fruit <Apple> .Create ();
Фредерік Гейзельс

2
Ви не можете зробити Appleконструктор меншим за загальнодоступний, оскільки це where T : Fruit<T>, new()вимагає Tпублічний конструктор. @Matt Hamsmith - Ваш код не компілюється, доки ви не видалите protected Apple() { }. Я вже тестував це у VS.
Тохід

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

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.