Was ist die richtige Alternative zur statischen Methodenvererbung?

82

Ich verstehe, dass die Vererbung statischer Methoden in C # nicht unterstützt wird. Ich habe auch eine Reihe von Diskussionen gelesen (einschließlich hier), in denen Entwickler behaupten, dass diese Funktionalität benötigt wird. Die typische Antwort lautet: "Wenn Sie eine statische Mitgliedervererbung benötigen, liegt ein Fehler in Ihrem Design vor."

OK, da OOP nicht möchte, dass ich überhaupt über statische Vererbung nachdenke, muss ich zu dem Schluss kommen, dass mein offensichtlicher Bedarf darauf auf einen Fehler in meinem Design hinweist. Aber ich stecke fest. Ich würde mich sehr über Hilfe bei der Lösung dieses Problems freuen. Hier ist die Herausforderung ...

Ich möchte eine abstrakte Basisklasse erstellen (nennen wir es eine Frucht), die einen komplexen Initialisierungscode enthält. Dieser Code kann nicht in den Konstruktor eingefügt werden, da ein Teil davon auf virtuellen Methodenaufrufen beruht.

Obst wird von anderen konkreten Klassen (Apple, Orange) geerbt, von denen jede eine Standard-Factory-Methode CreateInstance () verfügbar machen muss, um eine Instanz zu erstellen und zu initialisieren.

Wenn eine Vererbung statischer Elemente möglich wäre, würde ich die Factory-Methode in die Basisklasse einfügen und einen virtuellen Methodenaufruf an die abgeleitete Klasse verwenden, um den Typ zu erhalten, von dem eine konkrete Instanz initialisiert werden muss. Der Client-Code würde einfach Apple.CreateInstance () aufrufen, um eine vollständig initialisierte Apple-Instanz zu erhalten.

Dies ist jedoch eindeutig nicht möglich. Kann jemand erklären, wie sich mein Design ändern muss, um die gleiche Funktionalität zu gewährleisten?

Tim Coulter
quelle
1
Ich möchte darauf hinweisen, dass die Aussage "Dieser Code kann nicht in den Konstruktor eingefügt werden, da ein Teil davon auf virtuellen Methodenaufrufen beruht" nicht korrekt ist. Sie können virtuelle Methoden in einem Konstruktor aufrufen. (Sie müssen Ihre Aufrufe sorgfältig entwerfen, da Sie beim Aufrufen der virtuellen Methoden eine teilweise initialisierte Klasse haben können, diese wird jedoch unterstützt.) Weitere Informationen finden Sie in der Antwort von Ravadre.
Ruben

Antworten:

61

Eine Idee:

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() { ... }
}

Und ruf so an:

Apple myAppleVar = Fruit<Apple>.CreateInstance();

Keine zusätzlichen Fabrikklassen erforderlich.

Matt Hamsmith
quelle
9
IMHO ist es besser, den generischen Typ in die CreateInstance-Methode zu verschieben, als ihn auf Klassenebene zu setzen. (Wie ich es in meiner Antwort getan habe).
Frederik Gheysels
1
Ihre Präferenz wird notiert. Eine kurze Erklärung Ihrer Präferenz wäre mehr dankbar.
Matt Hamsmith
9
Auf diese Weise verschmutzen Sie nicht den Rest der Klasse. Der Typparameter ist nur in der Create-Methode erforderlich (zumindest in diesem Beispiel). Daneben denke ich, dass: Fruit.Create <Apple> (); ist dann besser lesbar: Fruit <Apple> .Create ();
Frederik Gheysels
2
Sie können den AppleKonstruktor nicht weniger als öffentlich machen, da dies einen öffentlichen Konstruktor where T : Fruit<T>, new()vorschreibt T. @ Matt Hamsmith - Ihr Code wird erst kompiliert, wenn Sie ihn entfernen protected Apple() { }. Ich habe es bereits in VS getestet.
Tohid
1
@Tohid - Du bist richtig. Ich habe bearbeitet. Dadurch kann Apple zwar ohne die statische Factory-Methode von Fruit CreateInstance erstellt werden, dies scheint jedoch unvermeidlich.
Matt Hamsmith
17

Verschieben Sie die Factory-Methode aus dem Typ und fügen Sie sie in eine eigene Factory-Klasse ein.

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

Wenn ich mir das anschaue, ist es nicht erforderlich, die Create-Methode in eine eigene Factory-Klasse zu verschieben (obwohl ich denke, dass dies vorzuziehen ist - Trennung von Bedenken -). Sie können sie in die Fruit-Klasse einfügen:

public abstract class Fruit
{

   public abstract string Define();

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

}

Und um zu sehen, ob es funktioniert:

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

            Console.ReadLine ();
        }        
    }
Frederik Gheysels
quelle
1
Ihr Code wird nicht kompiliert. Sie benötigen die where T: new () -Klausel. Dies ist jedoch ein exakter Betrüger von mir.
John Gietzen
1
Obwohl man eine statische FruitFactory-Klasse erstellen könnte, würde ich eine IFruitFactory-Schnittstelle zusammen mit einer instanziierbaren FruitFactory-Klasse bevorzugen. Möglicherweise haben Sie nur eine FruitFactory-Klasse, deren CreateFruit-Methode keinen Verweis auf ihre Objektinstanz enthält und daher genauso gut eine statische Methode sein könnte, aber wenn es jemals notwendig wird, mehrere Möglichkeiten zum Erstellen von Früchten anzubieten, oder wenn Obst erstellt wird Die Verwendung von Instanzmethoden kann sich als nützlich erweisen.
Supercat
4

Warum nicht eine Factory-Klasse (mit Vorlagen) mit einer create-Methode erstellen?

FruitFactory<Banana>.Create();
Daniel Rodriguez
quelle
4

Ich würde so etwas tun

 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()
Bob
quelle
3

Die WebRequestKlasse und ihre Ableitungstypen in der .NET BCL sind ein gutes Beispiel dafür, wie diese Art von Design relativ gut implementiert werden kann.

Die WebRequestKlasse hat mehrere Unterklassen, einschließlich HttpWebRequestund FtpWebReuest. Diese WebRequestBasisklasse ist jetzt auch ein Factory-Typ und macht eine statische CreateMethode verfügbar (die Instanzkonstruktoren sind ausgeblendet, wie es das Factory-Muster erfordert).

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

Diese CreateMethode gibt eine bestimmte Implementierung von zurückWebRequest Klasse zurück und bestimmt anhand des URI (oder der URI-Zeichenfolge) den Objekttyp, der erstellt und zurückgegeben werden soll.

Dies hat das Endergebnis des folgenden Verwendungsmusters:

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/");

Ich persönlich denke, dies ist ein guter Weg, um das Problem anzugehen, und es scheint tatsächlich die bevorzugte Methode der .NET Framework-Ersteller zu sein.

Noldorin
quelle
3

Erstens bedeutet das Fehlen von statischen Initialisierern, die virtuell sein können, nicht, dass Sie keine "Standard" -Mitgliedmethoden haben können, die überladen sein könnten. Zweitens können Sie Ihre virtuellen Methoden von Konstruktoren aus aufrufen, und sie funktionieren wie erwartet, sodass hier kein Problem auftritt. Drittens können Sie Generika verwenden, um eine typsichere Fabrik zu haben.
Hier ist ein Code, der die vom Konstruktor aufgerufene Methode factory + member Initialize () verwendet (und geschützt ist, sodass Sie sich keine Sorgen machen müssen, dass jemand sie nach dem Erstellen eines Objekts erneut aufruft):


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());
    }
}
Marcin Deptuła
quelle
1
Im Allgemeinen ist es am besten zu vermeiden, virtuelle Methoden von Konstruktoren aufzurufen. Siehe blogs.msdn.com/abhinaba/archive/2006/02/28/540357.aspx
TrueWill
Das stimmt, es kann schwierig sein, obwohl es möglich ist und immer so funktioniert, wie es sollte. Das Problem ist, zu verstehen, was "es sollte" bedeutet. Im Allgemeinen vermeide ich es, kompliziertere Methoden aufzurufen (auch nicht virtuelle, dh weil sie oft so aufgebaut sind, dass sie eine Ausnahme auslösen können, und ich mag es nicht, wenn meine Ctors etwas auslösen). , aber das ist die Entscheidung des Entwicklers.
Marcin Deptuła
0

Ich würde sagen, das Beste ist, eine virtuelle / abstrakte Initialisierungsmethode für die Fruchtklasse zu erstellen, die aufgerufen werden muss, und dann eine externe 'Fruchtfabrik'-Klasse zu erstellen, um Instanzen zu erstellen:


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;
    }
}
Lee
quelle