Laden...

Macht es Sinn ein Factory Design Pattern zu implementieren, obwohl man nur eine konkrete Klasse hat?

Erstellt von Saftsack vor 4 Jahren Letzter Beitrag vor 4 Jahren 1.013 Views
S
Saftsack Themenstarter:in
12 Beiträge seit 2018
vor 4 Jahren
Macht es Sinn ein Factory Design Pattern zu implementieren, obwohl man nur eine konkrete Klasse hat?

Hallo zusammen,
macht es Sinn ein Factory Design Pattern zu implementieren, obwohl man nur eine konkrete Klasse hat?
In meinem konkreten Beispiel habe ich eigentlich nur eine konkrete Klasse. Aber kann es trotzdem vorteilhaft sein solche Design Pattern als Strukturierungshilfe einzusetzen? - Falls ja - wo sollten die "default" Werte am besten hingepackt werden. - Vielen Dank für eure Ratschläge

Hier mein "strukturiertes Chaos":

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PandI.Interfaces;
using PandI.Interfaces.ProductConnectionInterface;

namespace PandI.PPERequests.PipeRequests
{

    public interface IPipeRequest : IPPERequest
    {

        /// <summary>
        /// the material of the pipe
        /// </summary>
        /// <value>
        /// scale: nominal
        /// </value>
        string Material { get; set; }
        /// <summary>
        /// the nominal diameter of the pipe
        /// </summary>
        /// <value>
        /// scale: nominal
        /// </value>
        string DN { get; set; }

    }
    public class PipeRequest
    {
        public IPipeRequest piperequest { get; internal set; }
        public void Create()
        {
            piperequest = new StandardPipeRequestCreator().CreatePipeRequest();
        }
        public void Create(string DN)
        {
            piperequest.DN = DN;
        }

    }    


    /// <summary>
    /// abstract pipe request creator
    /// </summary>
    /// <remarks>
    /// factory class
    /// </remarks>
    public abstract class PipeRequestCreator
    {
        //This method is going to be implemented by the sub classes
        protected abstract IPipeRequest NewPipeRequest();
        public IPipeRequest CreatePipeRequest()
        {
            return this.NewPipeRequest();
        }
    }
    /// <summary>
    /// standard pipe request creator
    /// </summary>
    /// <remarks>
    /// creates a pipe request with standard pipe
    /// </remarks>
    public class StandardPipeRequestCreator : PipeRequestCreator
    {
        protected override IPipeRequest NewPipeRequest()
        {
            IPipeRequest piperequest = new StandardPipeRequest();
            // Set default values:
            piperequest.DN = "DN15";
            piperequest.Form = "round pipe";
            piperequest.PN = "PN1";
            piperequest.Position = "horizontal";
            piperequest.TotalVolume = 0;
            piperequest.ISO10628Type = "404";
            piperequest.Material = "CWxx";
            ProductInterface P1 = new ProductInterface();
            P1.Create(ProductInterface.Type.ProductConnectionPoint, "P1", "", "", piperequest.DN);
            ProductInterface P2 = new ProductInterface();
            P2.Create(ProductInterface.Type.ProductConnectionPoint, "P2","","",piperequest.DN);
            piperequest.ProductConnectionPoints.Add(P1.Point);
            piperequest.ProductConnectionPoints.Add(P2.Point);


            return piperequest;
        }
    }

    /// <summary>
    /// concrete class of a pipe
    /// </summary>
    /// <remarks>
    /// defines an unbrunched channel
    /// </remarks>
    public class StandardPipeRequest : IPipeRequest
    {
        /// <summary>
        /// the material of the pipe
        /// </summary>
        /// <value>
        /// scale: nominal
        /// </value>
        public string Material { get; set; }
        /// <summary>
        /// the ISO Code of the pipe according to ISO 10628
        /// </summary>
        /// <value>
        /// scale: nominal
        /// </value>
        public string ISO10628Type { get; set; }
        /// <summary>
        /// position of the pipe according to his surrounding
        /// </summary>
        /// <value>
        /// complex
        /// </value>
        public string Position { get; set; }
        /// <summary>
        /// nominal pressure of the product area
        /// </summary>
        /// <value>
        /// scale: ordinal; range of values according to ISO 2944 or smilar
        /// default value: PN0.01
        /// </value>
        public string PN { get; set; }
        /// <summary>
        /// geometric form of the product area
        /// </summary>
        /// <value>
        /// complex
        /// </value>
        public string Form { get; set; }
        /// <summary>
        /// total volume of the pipe
        /// </summary>
        /// <value>
        /// scale: ratio scale; range of values [0; x] m³
        /// </value>
        public double TotalVolume { get; set; }
        /// <summary>
        /// nominal diameter of the pipe
        /// </summary>
        /// <value>
        /// scale: ordinal; value range DN1..
        /// </value>
        public string DN { get; set; }
        /// <summary>
        /// List holding the connection points
        /// </summary>
        public List<IProductInterface> ProductConnectionPoints { get; set; }
        /// <summary>
        /// List holding the environment points
        /// </summary>
        public List<IProductInterface> ProductEnvironmentPoints { get; set; }
        // constructor
        public StandardPipeRequest()
        {
            ProductConnectionPoints = new List<IProductInterface>();
            ProductEnvironmentPoints = new List<IProductInterface>();
        }
    }
}
T
2.223 Beiträge seit 2008
vor 4 Jahren

Wenn absolut sicher ist, dass es nur eine Konkrete Implementierung gibt, würde ich auf ein Interface verzichten und nur eine Klasse ohne entsprechende Ableitung anlegen und damit programmieren.
Interfaces sind nur dann sinnvoll, wenn es mehr als eine Implementierung geben kann/soll.

Das ganze hat weniger mit Strukturierungshilfe als mehr von sauberen Klassen Design zu tun.
Der Sinn von Interfaces ist es, die Implementierung offen zu lassen und nur die Grundstruktur für die Ableitungen vorzugeben.
Und dann sollten alle Methode/Klassen, die auf für eine Implementierung deines Interfaces gedacht sind, auch als Parameter/Typen immer das Interface bekommen.
Dadurch programmierst du auch, wie es sich auch gehört, gegen das Interface und nicht gegen eine Konkrete Klasse/Implementierung.

Außerdem baut man durch unnötige Interfaces, die nur einmal abgeleitet werden, unnötige Strukturen ein die ab einer gewissen Menge auch undursichtig werden können.
Spar die die Interfaces komplett aus und leg einfach eine konkrete Klasse an.

In deinem Beispiel würde ich auch, sofern möglich, die zusätzliche Creator Klasse auch verwerfen.
Das richtig teilweise schon wieder nach Java, wo jede Klasse ihren eigenen Builder mitbringt.
Ist in einigen Fällen sehr sinnvoll und gar nötig, in vielen Fällen aber unnötige Komplexität ohne Mehrwert.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

S
Saftsack Themenstarter:in
12 Beiträge seit 2018
vor 4 Jahren

Vielen Dank für deine ausführliche Antwort. Ja meine Intentiion war, die Interfaces als "Muster" zu verweden, um in Falle einer Erweiterung weniger Aufwand zu haben. Da mir die Erfahrung im Klassendesign fehlt, und ich blutiger Anfänger bin, habe ich natürlich erstmal alles versucht so zu implementieren, dass möglichst alle Fälle abgedeckt sind und ich im Falle einer Änderung schon ein Grundgerüst stehen habe, in das ich "reinpgroprammieren" kann. - Im Endeffekt habe ich bei diesen Problem eigentlich versucht zu viel "Methodik" anzuwenden. 😃

16.830 Beiträge seit 2008
vor 4 Jahren

Interfaces sind nur dann sinnvoll, wenn es mehr als eine Implementierung geben kann/soll.

Käse.
Ordentliche Software Tests nach modernen Prinzipien erfordern nicht überall aber i.d.R. Interfaces.
Klar, wenn man Software nicht testet.... 🙁

Der Sinn von Interfaces ist es, die Implementierung offen zu lassen und nur die Grundstruktur für die Ableitungen vorzugeben.

Nein.
Der Sinn von Interfaces ist primär die Modularisierung und die Trennung von Schnittstelle und Implementierung.
Der Nutzer einer Schnittstelle soll nichts von der konkreten Implementierung wissen.

Die Implementierung "offen zu lassen" ist ein Nebeneffekt.

Außerdem baut man durch unnötige Interfaces, die nur einmal abgeleitet werden, unnötige Strukturen ein die ab einer gewissen Menge auch undursichtig werden können.
Spar die die Interfaces komplett aus und leg einfach eine konkrete Klasse an.

Quatsch. Bitte so nicht machen!
Interfaces sind keine unnötigen Strukturen!

macht es Sinn ein Factory Design Pattern zu implementieren, obwohl man nur eine konkrete Klasse hat?

Aus der Sicht der Software Architektur ist der Weg mit Interfaces in Deinem Beispiel der absolut richtige Weg - die Umsetzung hat aber noch potential.

Kurzer Absprung:
Wenn man gegen eine Datenbank programmiert, nennt man sein Interface i.d.R. sowas wie "IUserRepository".
Dies zeigt, dass dies eine Schnittstelle ist, die User in der Datenbank managed.

Man würde aber niemals die Klasse selbst UserRepository nennen, weil es namentlich nicht die Implementierung darstellt.
Man würde es eben "UserMssqlRepository" oder "UserMysqlRepository" nennen, sodass direkt die Implementierung klar ist.

Wenn man sich nun Deinen Code so anschaut, dann ist insbesondere bei der Gesamtstruktur noch Potential nach Oben:

  public class PipeRequest
    {
        public IPipeRequest piperequest { get; internal set; }

Du hast ein Interface IPipeRequest und eine Klasse PipeRequest; wobei die Klasse die namentlich verwandte Schnittstelle gar nicht implementiert.
Stattdessen ist es ein Property. Sieht nach Konzeptionsfehler aus.

Im Gegenzug hast Du aber in der Methode Create eine fixe Dependency auf eine Klasse

  public void Create()
        {
            piperequest = new StandardPipeRequestCreator().CreatePipeRequest();
        }

Das würde man heute so vermeiden.
Man würde hier ein IPipeRequestCreator als Interface zur Verfügung stellen und StandardPipeRequestCreator via Dependency Injection injizieren - letzteres geht eben nur mit Interfaces.
Würde dann so aussehen:

     public class PipeRequest
    {
        private IPipeRequestCreator _requestCreator;
        public PipeRequest(IPipeRequestCreator requestCreator)
        {
            _requestCreator = requestCreator;
        }
        
        public IPipeRequest piperequest { get; internal set; }
        public void Create()
        {
            piperequest = requestCreator.CreatePipeRequest();
        }
    }

Riesen Vorteil: man hat keine Abhängigkeit im Code und kann die default Implementierung des Pipeline Creators direkt über die Konfiguration ändern.
Hinzu ist dies vollständig mit Mocks testbar; das in Deinem Falle mit einer direkten Abhängigkeit in der Klasse nicht möglich wäre.
Alternativ kann man aber auch den Request Creator völlig aus der Klasse lassen und dies wirklich in eine Factory auslagern.
Da gibts prinzipiell auch nur die Antwort: kommt auf die Gesamtsituation an, was man eher macht.

Beispielsweise könnte es folgendermaßen aussehen (strukturell, ohne spezifischen Code):

namespace PandI.PPERequests.PipeRequests
{

    public interface IPipeRequest { }

    public class DefaultPipeRequest : IPipeRequest { }


    public interface IPipeRequestCreator
    {
        IPipeRequest Create();
    }

    public class DefaultPipeRequestCreator : IPipeRequestCreator
    {
        public IPipeRequest Create()
        {
            DefaultPipeRequest piperequest = new DefaultPipeRequest();

            return piperequest;
        }
    }
}

Vorteile:

PS: Neben der grundsätzlich eher suboptimalen Struktur passen auch schon Deine Namespaces nicht.
Namespaces in .NET verfolgen eine relativ strikte Richtlinie, die hilfreich bzw. notwendig für modulare und/oder erweiterbare Software ist.
Man würde niemals ein Namespace Element "PandI.Interfaces.ProductConnectionInterface" oder "PandI.Interfaces.ProductConnectionInterface" nennen - verstößt vollkommen gegen den Sinn von Namespaces bzw. der Idee dahinter.
Names of Namespaces

Die Struktur von Namespaces in C#/.NET sind das absolute A und O.
Wenn man das Konzept von Namespaces falsch anwendet, entsteht sehr schnell sehr schlechter Code, der nur mit großem Aufwand und einer kompletten Neustrukturierung korrigierbar ist.

Leider wird aber gerade das sehr sehr oft falsch gemacht; erlebe ich tagtäglich, wenn ich Kunden-Code reviewe.

P
64 Beiträge seit 2011
vor 4 Jahren

Der Sinn von Interfaces ist primär die Modularisierung und die Trennung von Schnittstelle und Implementierung.
Der Nutzer einer Schnittstelle soll nichts von der konkreten Implementierung wissen.

Ist das nicht ein Relit aus alten C Zeiten? Sogar modernes C++ geht davon weg (siehe modules). Und nur weil es kein IFoo gibt hat Bar nicht gleich kein definiertes Interface nach außen. Selbst das .NET Framework packt nicht alles in Interfaces ( z.Bsp.: File, Exception, EventArgs )

Quatsch. Bitte so nicht machen!
Interfaces sind keine unnötigen Strukturen!

Das widerspricht aber schön dem KISS prinzip. Für eine Klasse, von der es keine weitere Variante gibt brauche ich kein extra Interface und auch keinen Factory. Der Code lässt sich aber trozdem leicht erweitern, falls es doch mal nötig wird, weil sich eine Interface Struktur ( ich weiß grad nicht, wie ich ein Interface als solches selber von dem Interface, wie in IDisposable, sprachlich unterscheiden soll) sehr leicht drüber stülpen lässt.

Wenn ich jeden Code immer so anlegen würde, bekäme ich jedes mal ganz schnell einen unübersichtlichen Moloch durch den sich man ziemlich schlecht navigieren kann. Ein Interface zwischegeschaltet sorgt leider auch dafür, dass sich die Implementierung zum teil nicht so leicht finden lässt.

PS: Mir fällt gerade auf, dass es OT ist und vor allem deine absoluten Aussagen betrifft. für den TO ist es unerheblich. Seine Klasse gehört in ein Interface und ein Factory.

2.079 Beiträge seit 2012
vor 4 Jahren

Wenn ich jeden Code immer so anlegen würde, bekäme ich jedes mal ganz schnell einen unübersichtlichen Moloch durch den sich man ziemlich schlecht navigieren kann. Ein Interface zwischegeschaltet sorgt leider auch dafür, dass sich die Implementierung zum teil nicht so leicht finden lässt.

Mit VisualStudio schon: How to find the implementations of an interface in Visual Studio

Der Code lässt sich aber trozdem leicht erweitern, falls es doch mal nötig wird, weil sich eine Interface Struktur ( [...]) sehr leicht drüber stülpen lässt.

Ein Interface lohnt nur dann, wenn man es auch anstelle der Klasse nutzt. Wenn Du nun z.B. für UnitTests ein Interface einführen möchtest, müsstest Du erst jede Nutzung der Implementierung durch das Interface austauschen.
Einfach nur ein Interface dran klatschen reicht eben nicht aus.

Das widerspricht aber schön dem KISS prinzip. Für eine Klasse, von der es keine weitere Variante gibt brauche ich kein extra Interface und auch keinen Factory.

Ein Stück weit hast Du Recht, das widerspricht dem KISS-Prinzip, aber KISS ist auch nicht der Weisheit letzter Schluss. Ich hab z.B. die letzten Jahre an einer Anwendung gearbeitet, die nach KISS aufgebaut wurde und das Ergebnis war:

  • Kein einziger UnitTest
  • Viele Änderungen zogen sich durch die meisten Hierarchieebenen
  • Modulares Erweitern war nur mehr schlecht als recht möglich

Klar, wenn man nach KISS arbeitet, lässt sich der Code schön einfach lesen und nachvollziehen, man kann auch immer die "Definition öffnen"-Funktion nutzen - das fällt natürlich alles weg. Man sollte aber seine Anwendung nicht so aufbauen, dass man bei der täglichen Arbeit oder der Fehlersuche ein paar Sekunden/Minuten spart, sondern dass man in der Zukunft, wenn man umfassende Erweiterungen oder Anpassungen vornehmen will, nicht die ganze Anwendung umbauen muss. UnitTests Mal außen vor gelassen ...

Aber ja, die Factory bzw. ihr Sinn steht auf einem anderen Blatt, ich würde mir immer gut überlegen, ob ich eine Factory brauche, denn sie bedeutet zwangsläufig Mehraufwand, der für z.B. UnitTests nicht zwingend notwendig ist.
Wenn man mit einem IoC-Framework arbeitet, braucht man die gar nicht, da man von der Factory sowieso nichts sehen würde, man würde nur die erzeugte Instanz injiziert bekommen. Braucht man doch eine Factory, dann lässt sie sich sehr leicht ergänzen, die meisten (wenn nicht sogar alle) IoC-Frameworks können damit umgehen und der vorhandene Code ändert sich nur beim Aufbau des IoC-Containers - und da nicht Mal sehr umfangreich.

Man sollte nicht pauschal jeder Klasse ein Interface spendieren. Man sollte stattdessen überlegen, welche Detail-Komponenten es gibt und die bekommen dann ein Interface zur Abstraktion. Die tatsächliche Implementierung bzw. die Klassen, die sie nutzt, brauchen dann keine Interfaces mehr - solange sie auch nur von dieser einen Komponente genutzt werden.
Ich arbeite daher ganz gerne mit einzelnen Projekten/DLLs pro Komponente und mach alles internal, was nur von den abstrahierten Klassen genutzt werden soll. Sollte sich die Wahl als falsch herausstellen, fällt das wenigstens schmerzhaft auf und man kann die Struktur nochmal überdenken.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

16.830 Beiträge seit 2008
vor 4 Jahren

Selbst das .NET Framework packt nicht alles in Interfaces ( z.Bsp.: File, Exception, EventArgs )

Nein, es ist kein Relikt.
Gerade File ist ein großes Problem, denn Datei-Operationen kann man nicht ohne eigene Wrapper oder Shims testen.

Überall Interfaces zu nutzen war nicht meine Aussage: sondern an geeigneten Stellen.
Vererbung, wie es EventArgs architektonisch nutzt, hat ebenso seine Berechtigung wie Interfaces.

Wenn ich jeden Code immer so anlegen würde, bekäme ich jedes mal ganz schnell einen unübersichtlichen Moloch durch den sich man ziemlich schlecht navigieren kann.

Das war auch nicht meine Aussage.

Ein Interface zwischegeschaltet sorgt leider auch dafür, dass sich die Implementierung zum teil nicht so leicht finden lässt.

Das ist ein Problem der Organisation bzw. der Arbeitsweise.
Nicht vom Quellcode oder von der Architektur. Davon abgesehen gibt es Hilfsmittel in Visual Studio / anderen IDEs.

Der Rest bzw. der Factory Diskussion:

Da gibts prinzipiell auch nur die Antwort: kommt auf die Gesamtsituation an, was man eher macht.

S
Saftsack Themenstarter:in
12 Beiträge seit 2018
vor 4 Jahren

Vielen Dank für deine ausführliche Antwort. - Ich habe heute den ganzen Tag damit verbracht deine Vorschläge und Anmerkungen nachzuvollziehen. Ich glaube ich muss mein Klassendesign komplett überdenken, da ich zunächst den Widerspruch auflösen muss, dass meine Klasse <PipeRequest> das Interface IPipeRequest gar nicht implementiert .