Eigenschaften in PHP - Beispiele / Best Practices aus der Praxis? [geschlossen]

148

Eigenschaften waren eine der größten Ergänzungen für PHP 5.4. Ich kenne die Syntax und verstehe die Idee hinter Merkmalen wie der Wiederverwendung von horizontalem Code für allgemeine Dinge wie Protokollierung, Sicherheit, Caching usw.

Ich weiß jedoch immer noch nicht, wie ich Merkmale in meinen Projekten nutzen würde.

Gibt es Open Source-Projekte, die bereits Merkmale verwenden? Gibt es gute Artikel / Lesematerial zum Strukturieren von Architekturen mithilfe von Merkmalen?

Max
quelle
8
Hier ist meine Meinung: Ein Blog-Beitrag zu dem Thema, das ich zu diesem Thema geschrieben habe. TL; DR: Grundsätzlich befürchte ich, dass die meisten Anwendungen, die wir sehen werden, zwar vollständig sind und für immer verwendet werden können, aber vollständige Anti-Muster sind und weitaus mehr Schmerzen verursachen als sie lösen ...
ircmaxell
1
Werfen Sie einen Blick auf die Scala-Standardbibliothek und Sie werden viele nützliche Beispiele für Merkmale finden.
dmitry

Antworten:

89

Meine persönliche Meinung ist, dass es beim Schreiben von sauberem Code tatsächlich nur sehr wenige Anwendungen für Merkmale gibt.

Anstatt Merkmale zu verwenden, um Code in eine Klasse zu hacken, ist es besser, die Abhängigkeiten über den Konstruktor oder über Setter zu übergeben:

class ClassName {
    protected $logger;

    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }
    // or
    public function setLogger(LoggerInterface $logger) {
        $this->logger = $logger;
    }
}

Der Hauptgrund, warum ich das besser finde als die Verwendung von Merkmalen, ist, dass Ihr Code viel flexibler ist, indem die harte Kopplung an ein Merkmal entfernt wird. Zum Beispiel könnten Sie jetzt einfach eine andere Logger-Klasse übergeben. Dies macht Ihren Code wiederverwendbar und testbar.

NikiC
quelle
4
Mit Merkmalen können Sie auch eine andere Logger-Klasse verwenden, oder? Bearbeiten Sie einfach das Merkmal, und alle Klassen, die das Merkmal verwenden, werden aktualisiert. Korrigieren Sie mich, wenn ich falsch
liege
14
@rickchristie Klar, das könntest du machen. Sie müssten jedoch den Quellcode des Merkmals bearbeiten. Sie würden es also für jede Klasse ändern, die es verwendet, nicht nur für die bestimmte Klasse, für die Sie einen anderen Logger wünschen. Und was ist, wenn Sie dieselbe Klasse mit zwei verschiedenen Loggern verwenden möchten? Oder wenn Sie beim Testen einen Mock-Logger übergeben möchten? Sie können nicht, wenn Sie Merkmale verwenden, können Sie, wenn Sie Abhängigkeitsinjektion verwenden.
NikiC
2
Ich kann Ihren Standpunkt sehen, ich überlege auch, ob Eigenschaften es wert sind oder nicht. Ich meine, in modernen Frameworks wie Symfony 2 gibt es überall eine Abhängigkeitsinjektion, die in den meisten Fällen über den Merkmalen zu liegen scheint. Im Moment sehe ich Merkmale als nicht viel mehr als "Compiler Assisted Copy & Paste". ;)
Max
11
Im Moment sehe ich Merkmale als nicht viel mehr als "Compiler Assisted Copy & Paste". ;) : @Max: Genau dafür wurden Eigenschaften entwickelt, das ist also völlig richtig. Es macht es "wartbarer", da es nur eine Definition gibt, aber es ist im Grunde nur c & p ...
ircmaxell
29
Bei NikiC fehlt der Punkt: Die Verwendung eines Merkmals verhindert nicht die Verwendung von Dependency Injection. In diesem Fall würde ein Merkmal nur zulassen, dass jede Klasse, die die Protokollierung implementiert, die setLogger () -Methode und die Erstellung der $ logger-Eigenschaft nicht duplizieren muss. Das Merkmal würde sie liefern. setLogger () würde wie im Beispiel einen Hinweis auf LoggerInterface eingeben, damit jeder Logger-Typ übergeben werden kann. Diese Idee ähnelt Gordons Antwort unten (nur sieht es so aus, als würde er einen Hinweis auf eine Logger-Superklasse und nicht auf eine Logger-Schnittstelle geben ).
Ethan
205

Ich denke, man müsste sich schon seit einiger Zeit mit Sprachen befassen, die Eigenschaften haben, um die akzeptierten Good / Best Practices zu lernen. Meine aktuelle Meinung zu Trait ist, dass Sie sie nur für Code verwenden sollten, den Sie in anderen Klassen duplizieren müssten, die dieselbe Funktionalität haben.

Beispiel für ein Logger-Merkmal:

interface Logger
{
    public function log($message, $level);    
}

class DemoLogger implements Logger
{
    public function log($message, $level)
    {
        echo "Logged message: $message with level $level", PHP_EOL; 
    }
}

trait Loggable // implements Logger
{
    protected $logger;
    public function setLogger(Logger $logger)
    {
        $this->logger = $logger;
    }
    public function log($message, $level)
    {
        $this->logger->log($message, $level);
    }
}

class Foo implements Logger
{
    use Loggable;
}

Und dann machst du ( Demo )

$foo = new Foo;
$foo->setLogger(new DemoLogger);
$foo->log('It works', 1);

Ich denke, das Wichtigste bei der Verwendung von Merkmalen ist, dass es sich wirklich nur um Codeteile handelt, die in die Klasse kopiert werden. Dies kann leicht zu Konflikten führen, wenn Sie beispielsweise versuchen, die Sichtbarkeit von Methoden zu ändern, z

trait T {
    protected function foo() {}
}
class A { 
    public function foo() {}
}
class B extends A
{
    use T;
}

Das Obige führt zu einem Fehler ( Demo ). Ebenso werden im Merkmal deklarierte Methoden, die auch bereits in der using-Klasse deklariert sind, nicht in die Klasse kopiert, z

trait T {
    public function foo() {
    return 1;
}
}
class A { 
    use T;
    public function foo() {
    return 2;
}
}

$a = new A;
echo $a->foo();

druckt 2 ( Demo ). Dies sind Dinge, die Sie vermeiden möchten, da sie das Auffinden von Fehlern erschweren. Sie sollten auch vermeiden, Dinge in Merkmale zu setzen, die auf Eigenschaften oder Methoden der Klasse wirken, die sie verwendet, z

class A
{
    use T;
    protected $prop = 1;
    protected function getProp() {
        return $this->prop;
    }
}

trait T
{
    public function foo()
    {
        return $this->getProp();
    }
}

$a = new A;
echo $a->foo();

funktioniert ( Demo ), aber jetzt ist das Merkmal eng mit A verbunden und die gesamte Idee der horizontalen Wiederverwendung geht verloren.

Wenn Sie dem Prinzip der Schnittstellentrennung folgen, haben Sie viele kleine Klassen und Schnittstellen. Das macht Traits zu einem idealen Kandidaten für die Dinge, die Sie erwähnt haben, z. B. Querschnittsthemen , aber keine Objekte (im strukturellen Sinne) zu komponieren. In unserem obigen Logger-Beispiel ist das Merkmal vollständig isoliert. Es gibt keine Abhängigkeiten von konkreten Klassen.

Wir könnten Aggregation / Komposition verwenden (wie an anderer Stelle auf dieser Seite gezeigt), um dieselbe resultierende Klasse zu erzielen, aber der Nachteil der Verwendung von Aggregation / Komposition besteht darin, dass wir die Proxy- / Delegator-Methoden manuell zu jeder Klasse hinzufügen müssen, die dies sollte in der Lage sein zu protokollieren. Eigenschaften lösen dieses Problem gut, indem ich die Kesselplatte an einem Ort aufbewahren und bei Bedarf selektiv anwenden kann.

Hinweis: Da Merkmale in PHP ein neues Konzept sind, können sich alle oben geäußerten Meinungen ändern. Ich hatte noch nicht viel Zeit, das Konzept selbst zu bewerten. Aber ich hoffe, es ist gut genug, um Ihnen etwas zum Nachdenken zu geben.

Gordon
quelle
41
Das ist ein interessanter Anwendungsfall: Verwenden Sie eine Schnittstelle, die den Vertrag definiert, und verwenden Sie das Merkmal, um diesen Vertrag zu erfüllen. Gut.
Max
13
Ich mag diese Art von echten Programmierern, die echte Arbeitsbeispiele mit kurzen Beschreibungen für jeden vorschlagen. Thx
Arthur Kushman
1
Was ist, wenn jemand stattdessen eine abstrakte Klasse verwendet? Durch Ersetzen der Schnittstelle und des Merkmals kann eine abstrakte Klasse erstellt werden. Auch wenn die Schnittstelle für die Anwendung so erforderlich ist, kann die abstrakte Klasse die Schnittstelle implementieren und die Methoden definieren, wie es das Merkmal getan hat. Können Sie also erklären, warum wir noch Eigenschaften brauchen?
Sumanchalki
12
@sumanchalki Abstrakte Klasse folgt den Regeln der Vererbung. Was ist, wenn Sie eine Klasse benötigen, die Loggable und Cacheable implementiert? Sie benötigen die Klasse, um AbstractLogger zu erweitern, die dann AbstractCache erweitern muss. Das bedeutet aber, dass alle Loggables Caches sind. Das ist eine Kupplung, die Sie nicht wollen. Es schränkt die Wiederverwendung ein und bringt Ihr Vererbungsdiagramm durcheinander.
Gordon
1
Ich denke, Demo-Links sind tot
Pmpr
19

:) Ich mag es nicht zu theoretisieren und darüber zu debattieren, was mit etwas gemacht werden soll. In diesem Fall Eigenschaften. Ich werde Ihnen zeigen, wofür ich Eigenschaften nützlich finde, und Sie können entweder daraus lernen oder sie ignorieren.

Eigenschaften - sie sind großartig, um Strategien anzuwenden . Kurz gesagt, Strategiedesignmuster sind nützlich, wenn dieselben Daten unterschiedlich behandelt (gefiltert, sortiert usw.) werden sollen.

Beispielsweise haben Sie eine Liste von Produkten, die Sie nach bestimmten Kriterien (Marken, Spezifikationen, was auch immer) herausfiltern oder nach verschiedenen Mitteln sortieren möchten (Preis, Etikett, was auch immer). Sie können ein Sortiermerkmal erstellen, das verschiedene Funktionen für verschiedene Sortiertypen (numerisch, Zeichenfolge, Datum usw.) enthält. Sie können dieses Merkmal dann nicht nur in Ihrer Produktklasse (wie im Beispiel angegeben) verwenden, sondern auch in anderen Klassen, die ähnliche Strategien benötigen (um eine numerische Sortierung auf einige Daten usw. anzuwenden).

Versuch es:

<?php
trait SortStrategy {
    private $sort_field = null;
    private function string_asc($item1, $item2) {
        return strnatcmp($item1[$this->sort_field], $item2[$this->sort_field]);
    }
    private function string_desc($item1, $item2) {
        return strnatcmp($item2[$this->sort_field], $item1[$this->sort_field]);
    }
    private function num_asc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] < $item2[$this->sort_field] ? -1 : 1 );
    }
    private function num_desc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] > $item2[$this->sort_field] ? -1 : 1 );
    }
    private function date_asc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 < $date2 ? -1 : 1 );
    }
    private function date_desc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 > $date2 ? -1 : 1 );
    }
}

class Product {
    public $data = array();

    use SortStrategy;

    public function get() {
        // do something to get the data, for this ex. I just included an array
        $this->data = array(
            101222 => array('label' => 'Awesome product', 'price' => 10.50, 'date_added' => '2012-02-01'),
            101232 => array('label' => 'Not so awesome product', 'price' => 5.20, 'date_added' => '2012-03-20'),
            101241 => array('label' => 'Pretty neat product', 'price' => 9.65, 'date_added' => '2012-04-15'),
            101256 => array('label' => 'Freakishly cool product', 'price' => 12.55, 'date_added' => '2012-01-11'),
            101219 => array('label' => 'Meh product', 'price' => 3.69, 'date_added' => '2012-06-11'),
        );
    }

    public function sort_by($by = 'price', $type = 'asc') {
        if (!preg_match('/^(asc|desc)$/', $type)) $type = 'asc';
        switch ($by) {
            case 'name':
                $this->sort_field = 'label';
                uasort($this->data, array('Product', 'string_'.$type));
            break;
            case 'date':
                $this->sort_field = 'date_added';
                uasort($this->data, array('Product', 'date_'.$type));
            break;
            default:
                $this->sort_field = 'price';
                uasort($this->data, array('Product', 'num_'.$type));
        }
    }
}

$product = new Product();
$product->get();
$product->sort_by('name');
echo '<pre>'.print_r($product->data, true).'</pre>';
?>

Abschließend denke ich über Merkmale wie Zubehör nach (mit denen ich meine Daten ändern kann). Ähnliche Methoden und Eigenschaften, die aus meinen Klassen herausgeschnitten und an einem einzigen Ort abgelegt werden können, für eine einfache Wartung, kürzeren und saubereren Code.

D. Marti
quelle
1
Während dies die öffentliche Schnittstelle sauber hält, kann die interne Schnittstelle dadurch sehr komplex werden, insbesondere wenn Sie dies auf andere Dinge ausweiten, wie z. B. Farben. Ich denke, einfache Funktionen oder statische Methoden verbessern sich hier.
Sebastian Mach
Ich mag den Begriff strategies.
Rannie Ollit
4

Ich freue mich über Traits, da sie ein häufiges Problem bei der Entwicklung von Erweiterungen für die Magento-E-Commerce-Plattform lösen . Das Problem tritt auf, wenn Erweiterungen einer Kernklasse (z. B. dem Benutzermodell) Funktionen hinzufügen, indem sie diese erweitern. Dazu zeigen Sie dem Zend-Autoloader (über eine XML-Konfigurationsdatei), dass er das Benutzermodell aus der Erweiterung verwenden soll, und lassen Sie dieses neue Modell das Kernmodell erweitern. ( Beispiel ) Aber was ist, wenn zwei Erweiterungen dasselbe Modell überschreiben? Sie erhalten eine "Rennbedingung" und nur eine wird geladen.

Die derzeitige Lösung besteht darin, die Erweiterungen so zu bearbeiten, dass eine die Modellüberschreibungsklasse des anderen in einer Kette erweitert, und dann die Erweiterungskonfiguration so festzulegen, dass sie in der richtigen Reihenfolge geladen werden, damit die Vererbungskette funktioniert.

Dieses System verursacht häufig Fehler. Bei der Installation neuer Erweiterungen müssen Sie nach Konflikten suchen und Erweiterungen bearbeiten. Dies ist ein Schmerz und unterbricht den Upgrade-Prozess.

Ich denke, die Verwendung von Traits wäre ein guter Weg, um dasselbe zu erreichen, ohne dass dieses nervige Modell die "Rennbedingung" außer Kraft setzt. Zugegeben, es könnte immer noch Konflikte geben, wenn mehrere Traits Methoden mit demselben Namen implementieren, aber ich würde mir vorstellen, dass eine einfache Namespace-Konvention dies größtenteils lösen könnte.

TL; DR Ich denke, Traits könnten nützlich sein, um Erweiterungen / Module / Plugins für große PHP-Softwarepakete wie Magento zu erstellen.

thaddeusmt
quelle
0

Sie könnten eine Eigenschaft für schreibgeschützte Objekte wie diese haben:

  trait ReadOnly{  
      protected $readonly = false;

      public function setReadonly($value){ $this->readonly = (bool)$value; }
      public function getReadonly($value){ return $this->readonly; }
  }

Sie können feststellen, ob dieses Merkmal verwendet wird, und bestimmen, ob Sie dieses Objekt in eine Datenbank, Datei usw. schreiben sollen oder nicht.

Nico
quelle
Also würde die Klasse, die usedieses Merkmal dann nennen würde if($this -> getReadonly($value)); Dies würde jedoch einen Fehler erzeugen, wenn Sie usedieses Merkmal nicht verwenden würden. Daher ist dieses Beispiel fehlerhaft.
Luceos
Nun, Sie müssen zuerst überprüfen, ob das Merkmal verwendet wird. Wenn das ReadOnly-Merkmal für ein Objekt definiert ist, können Sie überprüfen, ob es schreibgeschützt ist oder nicht.
Nico
Ich habe einen allgemeinen Proof of Concept für ein solches Merkmal in gist.github.com/gooh/4960073
Gordon
3
Sie sollten eine Schnittstelle für ReadOnly zu diesem Zweck deklarieren
Michael Tsang