• Jetzt anmelden. Es dauert nur 2 Minuten und ist kostenlos!

Singleton-Pattern vererben

vit_o

Neues Mitglied
Hallo,
ich würde gerne eine Basisklasse haben, die eine statische Methode (z.B. getInstance()) vererbt. Diese Methode soll dabei eine Implementierung des Singleton-Patterns darstellen. Auf diese Weise sollten alle abgeleiteten Klassen das Singleton-Pattern implementieren. Im Prinzip müsste in jeder Kindsklasse folgender Code stehen:
Code:
class Kindesklasse extends Basisklasse
{
   protected static $instance=null;

   public static function getInstance()
   {
        if(self::$_instance===null)
         {
             self::$_instance    = new self;
         }
         
         return self::$_instance;
   }
}
Nur würde ich gerne den Code nicht tausend Mal kopieren, sondern lieber zentral in der Basisklasse implementieren. Wenn ich aber den Code so in die Basisklasse schreiben würde, würde ich selbst wenn ich 2 Kindsklassen hätte nur ein Objekt erstellen können, weil über self::$_instance die statische Eigenschaft der Basisklasse angesprochen würde und nicht die der Kindsklasse. Gibt es eine Möglichkeit die Kindsklasse aus Elternklasse anzusprechen, auch wenn die Eigenschaft $_instance als protected deklariert wurde? Oder fällt jemandem vielleicht eine völlig andere Lösung für das Problem ein?
 
Werbung:
Meine Basisklasse ist eig nur eine abstrakte Klasse, die alle Gemeinsamkeiten der Kindsklassen enthält, ansonsten haben die Kindsklassen unterschiedliche Methoden. Ein Factory-Pattern wäre doch eher dann angebracht, wenn ich ein Objekt benötigen würde, das austauschbar ist. Ich will um nicht zu viel Speicher zu verbrauchen nur ein Objekt von jeder Kindsklasse haben und auf dieses von überall zugreifen können. Aber warum gilt es Singleton-Patterns zu vermeiden?

Vielen Dank für den Tipp mit "static", aber ich verstehe die Verwendung noch nicht so ganz. Wenn ich einfach alles "self"s durch "static"s ersetze gibt das nur 'ne Fehlermeldung. Und die Variable $_instance bleibt doch auch immernoch nur zentral in der Basisklasse vorhanden, oder?
 
Werbung:
Die Factory-Klasse erzeugt Objekte, mehr nicht. Außerdem kann diese die Objekte konfigurieren.
In OOP erzeugt man laufend neue Objekte; Singletons erlauben dies im Gegensatz nicht. Man muss sie also sparsam einsetzen.

Und ja „static“ hilft dir hier nicht weiter, da hier auch nur auf die obere Variable zugegriffen wird. D.h. jede Klasse wird ihr eignen Variable haben müssen.
 
Darüber ob man mit dem Singleton Pattern sparsam umgehen sollte oder nicht, kann man sich sicherlich streiten. In meinem Fall ist es glaub ich sinnvoll.
Ich habe aber gerade festgestellt, dass du mit den Late Static Bindings doch recht hattest. Ich habe wohl irgend einen anderen Fehler gemacht. Folgendes funktioniert reibungslos:
Code:
abstract class Basisklasse
{
    //Es wird an dieser Stelle keine statische Eigenschaft $_instance benötigt!
    
    public function __construct()
    {
        //Ist schon Ein Objekt abgeleitet worden?
        if(!is_null(static::$_instance))
        {
            throw new Exception('Es wurde schon ein Objekt dieser Klasse erzeugt.');
        }
        else
        {
            //Die Instanz hier und nicht in der Methode getInstance zuweisen, da sonst über den Kontruktor
            //beliebig viele Objekte erzeugt werden könnten.
            static::$_instance    = $this;
        }
    }
    
    public static function getInstance()
    {
        //falls noch nicht geschehen ein Objekt dieser Klasse initialisieren
        if(static::$_instance===null)
        {
            new static;
        }
        
        return static::$_instance;
    }
}

class Kindsklasse1 extends Basisklasse
{
    protected static $_instance    = null;
    
}
class Kindsklasse2 extends Basisklasse
{
    protected static $_instance    = null;
    
}

$a    = Kindsklasse1::getInstance();
$b    = Kindsklasse2::getInstance();
//folgende Zeile löst einen Fehler aus:
$a2    = new Kindsklasse1;
//folgende Zeile löst auch einen Fehler aus:
$b2    = new Kindsklasse2;
Vielen Dank für die Idee!
 
Zuletzt bearbeitet:
Darüber kann man sich nicht streiten:
Es besteht die große Gefahr, durch exzessive Verwendung von Singletons quasi ein Äquivalent zu globalen Variablen zu implementieren und damit dann prozedural anstatt objektorientiert zu programmieren[1].
Singleton (Entwurfsmuster)

Z.B. wird und vorallem wurde dem Zend Framework immer vorgeworfen zu „Versingletont“ zu sein. Deshalb wurden immer mehr Sinlgetons entfernt, bis nur noch Zend_Controller_Front und Zend_Registry übrig geblieben sind. Vorher waren es vielleicht maximal zehn Klassen. Du hast schon drei.
 
Werbung:
Ein singleton zu vererben, widerspricht sich. Ein singleton ist ein einmaliges Objekt, d.h. "es kann nur einen geben", wenn du es vererben willst, hast du plötzlich ein zweites Objekt, auch wenn es vererbt ist, von diesem "singleton" (das dann keines mehr ist).

Überhaupt ist das Argument "nicht zu viel Speicher zu verbrauchen " in dem Fall Unsinn. Objekte "brauchen" keinen Speicher. Objekte sind i.d.R. Referenzen, also Zeiger, d.h. verbrauchen nur wenige Byte.

Speicher wird erst verbraucht, wenn du Daten hast, vor allem Zeichenketten. Aber lass dich nicht dazu verleiten zu denken, je mehr Objekte, um so mehr Speicher brauchst du. Das ist grundsätzlich so nicht richtig.

(Ich weiß, dass diese Aussagen so nicht 100% korrekt sind, aber es geht mir darum zu zeigen, dass dieses Denken, man könnte Speicher sparen, wenn man singletons verwendet, unsinnig ist)
 
Werbung:
Das sicher auch. Aber in dem Fall geht es ja darum, dass in einem OO Ansatz versucht wird Resourcenschonend zu programmieren, was ja nicht verkehrt ist. Aber eben nicht dadurch, dass ich mal ein Objekt spare.
 
Vllt erkläre ich mal die Architektur meiner Applikation. Ich nutze das Zend Framework. Deshalb ist meine Anwendung grundsätzlich schon mal in Model, View und Controller aufgeteilt. Es gibt ja unterschiedliche Auffassung darüber was das Model und was der Controller zu tun hat. Bei mir übernimmt das Model nicht nur die Datenspeicherung, sondern enthält die komplette Business-Logik. Das Model habe ich allerdings erneut in mehrere Teile aufgeteilt. Jedes Model hat dabei einen oder mehrere Mapper, die wiederum auf eine oder mehrere Datenbanktabellen mappen (Es könnten theoretisch auch andere persistente Medien genutzt werden). Alle Datenbanktabellen, die direkt über foreign-keys miteinander verknüpft sind gehören zu einem Mapper. Der Mapper erzeugt aus den Daten, die die Daten(bank)abfrage geliefert hat, ein Objekt einer Record-Klasse. Diese Record-Objekte sind beim Einsatz von Datenbanken immer einzelne Zeilen, wobei diese über den Mapper zu anderen Datensätzen verlinkt werden. Diese Records werden an das Modelobjekt zurückgegeben. Dort ist dann die eigentliche Applikationslogik implementiert. Die Daten werden dort also irgendwie weiterverarbeitet. Das Resultat wird dann über den Controller an die View-Schicht weitergeleitet.

Grundsätzlich ist es nun aber so, dass alle Mapper einen Konsens haben, sprich Methoden und Eigenschaften, die jeder Mapper braucht. Deshalb habe ich eine abstrakte Klasse implementiert. Ein Mapper wird manchmal nicht nur von einem Model, sondern auch ab und zu von zwei Models benutzt. Da der Mapper aber immer dasselbe macht, braucht man keine zwei Objekte. Außerdem implementiert der Mapper einen Zwischenspeicher. Daten die ein Model abgefragt hat, brauchen deshalb nicht nochmal von einem anderen aus der Datenbank gelesen werden. Dies wiederum geht nur, wenn beide Models dasselbe Objekt verwenden. Also warum kein Singleton für alle Mapper? Besteht die Gefahr ein Äquivalent zu globalen Variable zu schaffen? Wieso sind globale Variable überhaupt ein Problem? Doch wahrscheinlich aus demselben Grund, warum man Namensräume eingerichtet hat, oder? Man kommt durcheinander und weist Variablen, die für andere Dinge gedacht waren, Werte zu, die woanders gespeichert werden sollten. Ich nutze aber, wie das Zend Framework, eine "User at will"-Struktur, die mir die Klassen lädt, wenn ich sie brauche. Aus dem Klassennamen kann man also die genaue Position der entsprechenden Datei ablesen. Eine Klasse ist bei mir immer Mindestens zwei Ordnerebenen tief, weshalb jeder Klassenname mindestens so lang ist: Default_Ordnername_NamederDatei. Dabei entsteht wirklich keinerlei Gefahr irgendetwas durcheinander zu bringen. Ein Mapper besitzt also immer folgendes Präfix: Default_Model_Mapper_.

Dass die Unittests schwierig zu implementieren sind, wusste ich noch nicht. Das könnte natürlich ein Argument sein. Das ist aber vllt. unter Berücksichtigung der Absicherung zu rechtfertigen.

Das ist jetzt natürlich nur ein kurzer Abriss von der Struktur. Falls es jemand noch genauer wissen will, schick' ich ihm einfach einen Auszug aus meiner vorläufigen Dokumentation. Falls sich jemand fragt, warum ich so ein Aufwand betreibe, im Prinzip soll eine hohe Flexibilität gewährleistet werden. Eine Anwendung mit dieser Struktur ist z.B. auf beliebig viele Server verteilbar;-)
 
Zuletzt bearbeitet:
Wo ist denn jetzt genau der Vorteil von:
PHP:
$mapper = Default_Model_Mapper_Foobar::getInstance()
gegenüber von:
PHP:
$mapper = new Default_Model_Mapper_Foobar();
wenn man mal von den ganzen Nachteilen absieht?

Globale Variablen gelten deshalb als Anti-Pattern, da man
a) Seiteneffekte nicht verhindern kann, wenn zich Funktionen auf diese zugreifen
b) wird’s unübersichtlich, so dass man gerne mal lokale mit globaler Variable verwechselt
 
Werbung:
vit_o schrieb:
Da der Mapper aber immer dasselbe macht, braucht man keine zwei Objekte. Außerdem implementiert der Mapper einen Zwischenspeicher. Daten die ein Model abgefragt hat, brauchen deshalb nicht nochmal von einem anderen aus der Datenbank gelesen werden. Dies wiederum geht nur, wenn beide Models dasselbe Objekt verwenden.

Du könntest den Zwischenspeicher einfach statisch implementieren.

PHP:
<?php

class Cache
{
    protected $data = array();

    public function set($key, $value)
    {
        $this->data[$key] = $value;
    }

    public function get($key)
    {
        if (!array_key_exists($key, $this->data)) {
            throw new Exception(sprintf('Key "%2" does not exist in cache',
                    $key));
        }

        return $this->data[$key];
    }

    public function hit($key)
    {
        return (array_key_exists($key, $this->data));
    }
}

class MyMapper
{
    protected static $cache = null;

    public function __construct()
    {
        if (self::$cache === null) {
            self::$cache = new Cache();
        }
    }

    public function getSomethingById($id)
    {
        $data = null;

        try {
            $data = self::$cache->get('something-' . $id);
        } catch (Exception $e) {
            $data = $this->doGetSomethingById($id);
            self::$cache->set('something-' . $id, $data);
        }

        return $data;
    }

    protected function doGetSomethingById($id)
    {
        printf("DEBUG: Query for id %d\n", $id);

        return (object) array('id' => $id);
    }
}


header('Content-Type: text/plain');


$mapper1 = new MyMapper();
var_dump($mapper1->getSomethingById(178));

$mapper2 = new MyMapper();
var_dump($mapper2->getSomethingById(178));

$mapper1->getSomethingById(254);
$mapper2->getSomethingById(254);
 
Der einzige Vorteil den ich habe wenn ich mit getInstance() arbeite ist der, dass ich ein bisschen Speicher spare. Mehr nicht. Aber es ging ja eigentlich darum, warum ich nicht die Variante mit getInstance() wählen soll.
Globale Variablen könnte man wirklich mit lokalen verwechseln, aber ich kann in PHP keine Klassennamen mit lokalen Variablen verwechseln. Was für Seiteneffekte können denn auftreten, wenn von zich Stellen auf die Methode getInstance zugegriffen wird?
Ich hoffe du bekommst nicht den Eindruck, dass ich mich nicht beraten lassen will, ich würde nur gern die Gründe für die Vermeidung dieser Struktur verstehen.
Vielen Dank für eure Geduld.
 
Werbung:
Mein Beispiel hat aus Unit-Testing-Sicht zwei große Probleme.

  1. Statische Variablen können zwischen Tests zu Seiteneffekten führen, was das Prinzip der Unabhängigkeit zweier Tests zerstört.
    PHP:
    class C
    {
        public static $a = 'foo';
    }
    
    function testModifiedValue()
    {
        $c = new C();
    
        $c::$a .= 'bar';
    
        if ($c::$a !== 'foobar') {
            throw new Exception('!== foobar');
        }
    }
    
    function testInitialValue()
    {
        $c = new C();
    
        if ($c::$a !== 'foo') {
            throw new Exception('!== foo');
        }
    }
    Der Aufruf von
    PHP:
    testInitialValue();
    testModifiedValue();
    läuft sauber durch, der Aufruf von
    PHP:
    testModifiedValue();
    testInitialValue();
    jedoch nicht, da die Klasse C zwischen den Tests nicht aus dem Speicher entfernt wird.
  2. Die Klasse MyMapper ist nicht unabhängig von Cache testbar. Cache kann nicht „gemockt“ werden, da die entsprechende Instanz nicht von Außen injizierbar ist (der new-Operator steht innerhalb der MyMapper-Klasse).

    Besser:
    PHP:
    class MyMapper
    {
    
        // ...
    
        public function __construct(Cache $cache)
        {
            $this->cache = $cache;
        }
    
        // ...
    }

Das dürfte alles ebenfalls auf dein Singleton zutreffen.

Siehe dazu auch folgende Artikel von Miško Hevery:

- Singletons are Pathological Liars
- How to Think About the “new” Operator with Respect to Unit Testing
 
Zurück
Oben