Ankündigung

Einklappen
Keine Ankündigung bisher.

Klassen-Design

Einklappen

Neue Werbung 2019

Einklappen
X
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

  • Klassen-Design

    Hallo zusammen,

    zum Einstieg ins Forum habe ich gleich eine Frage zum Umgang mit Klassen in PHP, bei deren Thema ich im Moment keinen Ansatz finde.

    Die Ableitungen von "Service" im unten gezeigten Code nutzen dabei immer ein Objekt, welches von "Data" abgeleitet wurde. Eine Klasse kann dabei von verschiedenen Service-Implementierungen genutzt werden. setDataForPerson() befindet sich daher in "Service".

    Via Typehint erzwingt setData() ein Objekt vom Typ Data. Bei getData() weiß man also in der Folge eigentlich auch nur, dass ein Objekt diesen Typs (oder null) zurück kommt, entsprechend kennzeichnet dies auch @return.

    Folgerichtig beschwert sich dann meine IDE (PhpStorm) darüber, dass in setDataForPerson() das Feld "field_person_name" beim Resultat von "getData()" undefiniert sei. Zwar führt PHP das Programm schmerzfrei so aus, wie es "gedacht" ist, aber es handelt sich hier wohl um keine wirklich saubere Lösung.

    Ich habe den Beispiel-Code stark vereinfacht von einem konkreten Fall hergeleitet, hoffe aber, dass man das grundsätzliche Problem erkennen kann.

    Hat vielleicht jemand einen Tipp zur Hand, wie man die Klassen günstiger strukturieren kann?

    Eine Klasse zwischen Service und Service1/Service2 zu hängen, die setData()/getData() mit dem konkreten Typ implementiert und setDataForPerson() dort unterzubringen, ist im konkreten Fall keine Option, da abseits des Beispiel-Codes mehrere Ableitungen von Data vorhanden sind, bei denen sich Datenfelder überschneiden, sprich: in Service1 würde bspw. setDataForPerson() und setDataForCar() (im Code nicht gezeigt) aufgerufen, während bspw. in Service4 setDataForPerson() und setDataForXY() aufgerufen werden könnten. Es würde jeweils in den genannten Setter-Methoden (die jeweils nicht nur eines, sondern eine Anzahl Felder setzen) ein entsprechend passendes Data-Objekt notwendig sein.

    Problematisch ist hierbei, dass die Data-Klassen (bislang) als flache Klassen mit einer handvoll Feldern implementiert sind, was gemacht wurde, damit diese als einfache Datenhaltungs-Klassen an anderer Stelle zur Verarbeitung der enthaltenen Daten übergeben werden können.

    Eine Option wäre vielleicht, setDataForPerson() in die entsprechende(n) Implementierung(en) von Data umzuziehen, in Data eine abstrakte Methode der Art setDataForAllFields() vorzugeben, die dann in den konkreten Data-Implementierungen alle notwendigen Methoden wie setDataForPerson() aufruft. Problematisch dabei fände ich allerdings, dass damit die Data-Klassen wiederum zu viel Wissen über die eingelieferten Daten haben müssen. An der Stelle wo derzeit setDataForPerson('test') aufgerufen wird, müsste dann die gesamte Quell-Objektstruktur aus der die Daten (hier nur String 'test') stammen an setDataForAllFields() übergeben werden und in der Data-Implementierung die Logik vorhanden sein, um die Daten zu erhalten.

    Vielen Dank im Voraus
    mdo


    PHP-Code:
    <?php
    error_reporting
    (E_STRICT);

    abstract class 
    Service
    {
        private 
    $data;

        
    /**
         * @param Data $data
         * @return void
         */
        
    public function setData(Data $data)
        {
            
    $this->data $data;
        }

        
    /**
         * @return Data
         */
        
    public function getData()
        {
            return 
    $this->data;
        }

        
    /**
         * @param string $value
         * @return void
         */
        
    public function setDataForPerson($value)
        {
            
    $this->getData()->field_person_name $value;
        }

    }

    class 
    Service1 extends Service
    {
        public function 
    __construct()
        {
            
    $this->setData(new Data1());
        }

        public function 
    doit()
        {
            
    $this->setDataForPerson('test');
        }
    }

    class 
    Service2 extends Service
    {
        public function 
    __construct()
        {
            
    $this->setData(new Data1());
        }

        public function 
    doit()
        {
            
    $this->setDataForPerson('test2');
        }
    }

    class 
    Service3 extends Service
    {
        public function 
    __construct()
        {
            
    $this->setData(new Data2());
        }

        public function 
    doit()
        {
            
    // do some other stuff
        
    }
    }

    abstract class 
    Data
    {
        public 
    $field_data;
    }

    class 
    Data1 extends Data
    {
        public 
    $field_person_name;
    }

    class 
    Data1a extends Data1
    {
        public 
    $field_person_birth;
    }

    class 
    Data2 extends Data
    {
        public 
    $field_data2;
    }


    $x = new Service1();
    $x->doit();

    var_dump($x);

  • #2
    Service, Service2, Service3 scheint mir schon grober Quatsch zu sein. Warum nicht eine Klasse Service?

    Kommentar


    • #3
      Zitat von lcrash Beitrag anzeigen
      Service, Service2, Service3 scheint mir schon grober Quatsch zu sein. Warum nicht eine Klasse Service?
      Weil die Implementierungen von doit() die umfangreiche Funktionalität einer Art Webservice umsetzen und dort nicht nur wie in dem vereinfachten Beispiel gezeigt ein Setter aufgerufen wird.

      Kommentar


      • #4
        Die Anweisung $this->getData()->field_person_name = $value gehört in eine Klasse, deren getData-Methode ein Objekt vom Typ Data1 liefert.
        Meinungen, die ich geäußert habe, sind nicht notwendigerweise meine eigenen. Abweichungen von der deutschen Rechtschreibung unterliegen dem Urheberrecht, dürfen aber unter den Bedingungen von verwendet werden

        Kommentar


        • #5
          Ich bin mir sicher dass das besser geht aber für Tipps zum grundlegenden Klassendesign ist mir das zu abstrakt und du scheinst ja auch schon recht festgelegt zu sein.

          Deshalb ist folgender Vorschlag auch eher Symptombehandlung, löst aber oberflächlich dein Problem
          PHP-Code:
              /**
               * @param string $value
               * @return void
               */
              
          public function setDataForPerson($value)
              {
                  
          $data $this->getData();
                  if (
          $data instanceof Data1))
                  {
                          
          // hier sollte dann auch deine IDE Bescheid wissen
                          
          $data->field_person_name $value;
                  }
                  else
                  {
                          throw 
          BadMethodCallException('Data is no instance of Person (Data1)');
                  }
              } 
          [IMG]https://g.twimg.com/twitter-bird-16x16.png[/IMG][URL="https://twitter.com/fschmengler"]@fschmengler[/URL] - [IMG]https://i.stack.imgur.com/qh235.png[/IMG][URL="https://stackoverflow.com/users/664108/fschmengler"]@fschmengler[/URL] - [IMG]http://i.imgur.com/ZEqflLv.png[/IMG] [URL="https://github.com/schmengler/"]@schmengler[/URL]
          [URL="http://www.schmengler-se.de/"]PHP Blog[/URL] - [URL="http://www.schmengler-se.de/magento-entwicklung/"]Magento Entwicklung[/URL] - [URL="http://www.css3d.net/"]CSS Ribbon Generator[/URL]

          Kommentar


          • #6
            Zitat von mimomamu Beitrag anzeigen
            Die Anweisung $this->getData()->field_person_name = $value gehört in eine Klasse, deren getData-Methode ein Objekt vom Typ Data1 liefert.
            Eigentlich war mir das natürlich vorher schon klar und so wie ich schrieb, suchte ich nach einem Weg das sauber aufzulösen. Aber Du hast natürlich Recht, man sollte es ganz simpel so herum anpacken, die Anweisung entsprechend verschieben und dann schauen, wie man die Dinge zusammen bringt ...


            Zitat von fab Beitrag anzeigen
            Ich bin mir sicher dass das besser geht aber für Tipps zum grundlegenden Klassendesign ist mir das zu abstrakt und du scheinst ja auch schon recht festgelegt zu sein.

            Deshalb ist folgender Vorschlag auch eher Symptombehandlung, löst aber oberflächlich dein Problem
            PHP-Code:
                /**
                 * @param string $value
                 * @return void
                 */
                
            public function setDataForPerson($value)
                {
                    
            $data $this->getData();
                    if (
            $data instanceof Data1))
                    {
                            
            // hier sollte dann auch deine IDE Bescheid wissen
                            
            $data->field_person_name $value;
                    }
                    else
                    {
                            throw 
            BadMethodCallException('Data is no instance of Person (Data1)');
                    }
                } 

            Wie Du schon schriebst eher eine Symptombehandlung und die wollte ich tatsächlich vermeiden.

            Ich habe es nun vorerst so gelöst, wie ich schon angedeutet hatte. Übertragen auf mein Code-Beispiel in Stichpunkten:

            * setDataForPerson() und Konsorten werden aus den Service-Klassen in die Data-Klassen geschoben, wo auch die zugehörigen Daten-Felder bearbeitet werden (das verringert dann auch den Data-Class-Smell an dieser Stelle)
            * in Service gibt es eine Methode setData() die eine Delegierung an setDataForAllFields(MyClass $myClass) in den Data-Implementierungen vornimmt und Exceptions handhabt
            * im Ganzen sorgt das noch für weitere Entkopplung, da bislang das Exception-Handling, das Einfluss auf den Status des Webservice-Aufrufs nimmt, unmittelbar bei setDataForPerson() untergebracht war
            * dafür wurde noch eine passende Exception-Klasse nötig, um entsprechende Fehler aus Data an Service zurück zu kommunizieren
            * einziger Nachteil ist soweit jetzt, dass setDataForAllFields() Kenntnisse über die Klasse MyClass haben muss, um die Daten zu extrahieren

            So bleibt zwar vorerst eine unschöne Stelle, aber zumindest ist die Unsicherheit bezüglich des Objekt-Typs eliminiert.

            Kommentar


            • #7
              Schau Dir mal das Strategie-Pattern an, vielleicht bringt Dich das weiter. Kann Dein Problem gerade nicht nachvollziehen.
              [COLOR="#F5F5FF"]--[/COLOR]
              [COLOR="Gray"][SIZE="6"][FONT="Georgia"][B]^^ O.O[/B][/FONT] [/SIZE]
              „Emoticons machen einen Beitrag etwas freundlicher. Deine wirken zwar fachlich richtig sein, aber meist ziemlich uninteressant.
              [URL="http://www.php.de/javascript-ajax-und-mehr/107400-draggable-sorttable-setattribute.html#post788799"][B]Wenn man nur Text sieht, haben viele junge Entwickler keine interesse, diese stumpfen Texte zu lesen.“[/B][/URL][/COLOR]
              [COLOR="#F5F5FF"]
              --[/COLOR]

              Kommentar


              • #8
                Prinzipiell ist es so, dass du auf die Eigenschaft an dieser Stelle gar nicht zugreifen dürftest. Wenn du dort ein Data-Objekt erwartest, darfst du auch nur auf Eigenschaften und Methoden von Data zugreifen. Data1 ist tabu. Sonst fliegt dir nämlich dein Programm um die Ohren, wenn du die Methode auf einer Data2 Instanz aufrufst (was ja legitim wäre).

                Eine Lösungsmöglichkeit zu nennen fällt schwer ohne die konkrete Umgebung und den Sinn und Zweck zu kennen.

                Die simpelste Lösung wäre wohl etwas wie
                PHP-Code:
                $data->setField('person_name') = $value 
                Davon ist aber im Allgemeinen abzuraten.

                Kommentar


                • #9
                  Kannst du mir mal kurz einen Abriss geben, was du genau mit dem Klassendesign bezweckst? Ich würde gerne vermeiden über if's und instanceof's diskutieren zu müssen, wenn es einfach durch Inversion der Abhängigkeiten oder Null-Pattern zu lösen ist - vielleicht auch durch Strategy. Wobei ich vor der Anwendung von letzterem eher noch über DI auf Basis eines Service-Interfaces nachdenken würde.
                  Viele Grüße,
                  Dr.E.

                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                  1. Think about software design [B]before[/B] you start to write code!
                  2. Discuss and review it together with [B]experts[/B]!
                  3. Choose [B]good[/B] tools (-> [URL="http://adventure-php-framework.org/Seite/088-Why-APF"]Adventure PHP Framework (APF)[/URL][URL="http://adventure-php-framework.org"][/URL])!
                  4. Write [I][B]clean and reusable[/B][/I] software only!
                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                  Kommentar


                  • #10
                    Zitat von dr.e. Beitrag anzeigen
                    Kannst du mir mal kurz einen Abriss geben, was du genau mit dem Klassendesign bezweckst? Ich würde gerne vermeiden über if's und instanceof's diskutieren zu müssen, wenn es einfach durch Inversion der Abhängigkeiten oder Null-Pattern zu lösen ist - vielleicht auch durch Strategy. Wobei ich vor der Anwendung von letzterem eher noch über DI auf Basis eines Service-Interfaces nachdenken würde.

                    Im Grunde geht es um folgendes Szenario. Ich habe einen Webservice, der einen Proxy zu einem Backend-Dienst darstellt. Der WebService implementiert verschiedene Services (Service1, Service2, ...). Diese rufen beim Backend-Dienst wiederum Services auf, die mit sich ähnelden XML-Nachrichten angesprochen werden.

                    Die Data-Klassen dienen dazu, die Daten aufzunehmen, die der Webservice erhält, damit eine XML-Factory-Klasse den entsprechenden Request bauen kann. Die Data-Implementierungen spiegeln also die XML-Stuktur der Backend-Requests als Objekt wieder. "Data" an sich enthält dabei gemeinsame Elemente, die allen erbenden Data-Klassen gemein sind. In der tatsächlichen Implementierung handelt es sich nicht um primitive Felder, sondern die einzelnen Data-Klassen-Felder sind wiederum Objekte, die auch Ihre Inhalte validieren können.

                    Ein Übergeben der Eingabedaten ohne den Umweg der Data-Klassen halten ich für wenig sinnvoll, da zum einen halt die Data-Klassen die Request-Formate beim Backend widerspiegeln und zum anderen spätestens in der XML-Factory zu viel Logik wäre, die sich um die Beschaffung der Daten kümmern müsste.

                    Die XML-Factory orientiert sich beim Zusammenstellen des XML-Requests an der vorhandenen Struktur des jeweiligen Data-Objekts.

                    Übertragen auf mein Code-Beispiel bedeutet dies (in meiner bereits geänderten Code-Fassung) nun, dass eine Service-Klasse in doit() zuerst via setDataForAllFields() anstößt, dass das konkrete Data-Objekt "sich selbst" mit Daten füllt. Weiter übergibt doit() (bzw. eine weitere Methode) dann das gefüllte Data-Objekt an die XML-Factory um mit dem Ergebnis den Backend-Dienst anzusprechen.

                    Kommentar

                    Lädt...
                    X