Jaka jest właściwa alternatywa dla dziedziczenia metod statycznych?


84

Rozumiem, że dziedziczenie metod statycznych nie jest obsługiwane w języku C #. Przeczytałem również kilka dyskusji (w tym tutaj), w których programiści twierdzą, że potrzebują tej funkcjonalności, na co typową odpowiedzią jest „jeśli potrzebujesz dziedziczenia statycznego elementu członkowskiego, w Twoim projekcie jest błąd”.

OK, biorąc pod uwagę, że OOP nie chce, żebym nawet myślał o dziedziczeniu statycznym, muszę stwierdzić, że moja widoczna potrzeba tego wskazuje na błąd w moim projekcie. Ale utknąłem. Naprawdę byłbym wdzięczny za pomoc w rozwiązaniu tego problemu. Oto wyzwanie ...

Chcę utworzyć abstrakcyjną klasę bazową (nazwijmy ją Fruit), która zawiera jakiś złożony kod inicjujący. Tego kodu nie można umieścić w konstruktorze, ponieważ część z nich będzie polegać na wywołaniach metod wirtualnych.

Owoce zostaną odziedziczone przez inne konkretne klasy (Apple, Orange), z których każda musi udostępniać standardową fabryczną metodę CreateInstance (), aby utworzyć i zainicjować instancję.

Gdyby dziedziczenie elementów statycznych było możliwe, umieściłbym metodę fabryczną w klasie bazowej i użyłbym wywołania metody wirtualnej do klasy pochodnej, aby uzyskać typ, z którego należy zainicjować konkretne wystąpienie. Kod klienta po prostu wywołałby Apple.CreateInstance () w celu uzyskania w pełni zainicjowanej instancji Apple.

Ale najwyraźniej nie jest to możliwe, więc czy ktoś może wyjaśnić, jak powinien zmienić się mój projekt, aby uwzględnić tę samą funkcjonalność.


2
Chciałbym zwrócić uwagę, że stwierdzenie „Tego kodu nie można umieścić w konstruktorze, ponieważ część z nich będzie polegać na wywołaniach metod wirtualnych” nie jest poprawne. MOŻESZ wywoływać metody wirtualne wewnątrz konstruktora. (Będziesz musiał starannie projektować swoje wywołania, ponieważ możesz mieć częściowo zainicjowaną klasę, gdy wywoływane są metody wirtualne, ale jest ona obsługiwana). Zobacz odpowiedź Ravadre, aby uzyskać więcej informacji.
Ruben

Odpowiedzi:


62

Jeden pomysł:

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

I zadzwoń tak:

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

Nie są potrzebne żadne dodatkowe klasy fabryczne.


9
IMHO, lepiej jest przenieść typ ogólny do metody CreateInstance zamiast umieszczać go na poziomie klasy. (Tak jak w mojej odpowiedzi).
Frederik Gheysels,

1
Twoje preferencje są odnotowane. Bardziej mile widziane byłoby krótkie wyjaśnienie twoich preferencji.
Matt Hamsmith,

9
W ten sposób nie „zanieczyszczasz” reszty klasy; parametr type jest konieczny tylko w metodzie Create (przynajmniej w tym przykładzie). Poza tym myślę, że: Fruit.Create <Apple> (); jest wtedy bardziej czytelny: Fruit <Apple> .Create ();
Frederik Gheysels,

2
Nie możesz uczynić Applekonstruktora mniejszym niż publicznym, ponieważ where T : Fruit<T>, new()dyktuje, że Tmusi mieć konstruktora publicznego. @Matt Hamsmith - Twój kod nie kompiluje się, dopóki nie usuniesz protected Apple() { }. Przetestowałem to już w VS.
Tohid

1
@Tohid - Masz rację. Redagowałem. To jednak naraża Apple na to, że można go zbudować bez użycia statycznej metody fabryki Fruit CreateInstance, ale wydaje się to nieuniknione.
Matt Hamsmith,

18

Przenieś metodę fabryki poza typ i umieść ją w jej własnej klasie 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();
     }
}

Ale, kiedy się temu przyjrzę, nie ma potrzeby przenoszenia metody Create do jej własnej klasy Factory (chociaż myślę, że jest to preferowane - oddzielenie obaw -), możesz umieścić ją w klasie Fruit:

public abstract class Fruit
{

   public abstract string Define();

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

}

Aby sprawdzić, czy to działa:

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

            Console.ReadLine ();
        }        
    }

1
Twój kod się nie skompiluje. potrzebujesz klauzuli where T: new (). Jest to jednak moja dokładna kopia.
John Gietzen

1
Chociaż można by stworzyć statyczną klasę FruitFactory, wolałbym interfejs IFruitFactory wraz z instancją klasy FruitFactory. Możesz otrzymać tylko jedną klasę FruitFactory, której metoda CreateFruit nie odwołuje się do jej instancji obiektu, a zatem równie dobrze mogłaby być metodą statyczną, ale jeśli kiedykolwiek zajdzie potrzeba oferowania wielu sposobów tworzenia owoców lub tworzenia owoców zawsze wymaga zdolności do utrzymania stanu, użycie metod instancji może się okazać przydatne.
supercat

4

Dlaczego nie utworzyć klasy fabrycznej (na podstawie szablonu) za pomocą metody tworzenia?

FruitFactory<Banana>.Create();

4

Zrobiłbym coś takiego

 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

WebRequestKlasa i jej rodzaje instrumentów pochodnych w .NET BCL stanowią dobry przykład tego, jak tego rodzaju konstrukcji może być realizowane stosunkowo dobrze.

WebRequestKlasa ma kilka podklas, w tym HttpWebRequesti FtpWebReuest. Teraz ta WebRequestklasa bazowa jest również typem fabrycznym i uwidacznia Createmetodę statyczną (konstruktory instancji są ukryte, zgodnie z wymaganiami wzorca fabryki).

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

Ta Createmetoda zwraca określoną implementację WebRequestklasy i używa identyfikatora URI (lub ciągu identyfikatora URI) do określenia typu obiektu do utworzenia i zwrócenia.

Daje to końcowy wynik następującego wzorca użycia:

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

Osobiście uważam, że jest to dobry sposób podejścia do problemu i rzeczywiście wydaje się, że jest to preferowana metoda twórców .NET Framework.


3

Po pierwsze, brak statycznych inicjatorów, które mogą być wirtualne, nie oznacza, że ​​nie można mieć „standardowych” metod składowych, które mogłyby zostać przeciążone. Po drugie, możesz wywołać swoje metody wirtualne z konstruktorów i będą one działać zgodnie z oczekiwaniami, więc nie ma tutaj problemu. Po trzecie, możesz użyć typów generycznych, aby mieć fabrykę bezpieczną dla typów.
Oto kod wykorzystujący metodę factory + member Initialize (), która jest wywoływana przez konstruktora (i jest chroniona, więc nie musisz się martwić, że ktoś wywoła go ponownie po utworzeniu obiektu):


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
Generalnie najlepiej jest unikać wywoływania metod wirtualnych z konstruktorów. Zobacz blogs.msdn.com/abhinaba/archive/2006/02/28/540357.aspx
TrueWill

To prawda, może to być trudne, chociaż jest to możliwe i zawsze będzie działać tak, jak powinno, problem polega na tym, aby zrozumieć, co oznacza „powinno”. Generalnie staram się unikać wywoływania bardziej skomplikowanych metod (nawet niewirtualnych, tj. Dlatego, że często są one skonstruowane w sposób, który pozwala im rzucać wyjątek, a nie lubię, gdy moi lekarze rzucają cokolwiek) , ale to decyzja programisty.
Marcin Deptuła

0

Powiedziałbym, że najlepiej jest stworzyć wirtualną / abstrakcyjną metodę Initialise na klasie owoców, którą należy wywołać, a następnie utworzyć zewnętrzną klasę „fabryki owoców” w celu utworzenia instancji:


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;
    }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.