Ciri-ciri dalam PHP - ada contoh dunia nyata / praktik terbaik? [Tutup]

148

Ciri - ciri telah menjadi salah satu tambahan terbesar untuk PHP 5.4. Saya tahu sintaks dan memahami ide di balik ciri-ciri, seperti kode horisontal digunakan kembali untuk hal-hal umum seperti logging, keamanan, caching dll.

Namun, saya masih tidak tahu bagaimana saya bisa menggunakan ciri-ciri dalam proyek saya.

Apakah ada proyek open source yang sudah menggunakan sifat? Adakah artikel / bahan bacaan yang bagus tentang bagaimana menyusun arsitektur menggunakan ciri-ciri?

Maks
sumber
8
Inilah pendapat saya: posting blog tentang topik yang saya tulis pada topik. TL; DR: Pada dasarnya, saya khawatir meskipun mereka kuat dan dapat digunakan untuk kebaikan, sebagian besar kegunaan yang akan kita lihat akan menjadi anti-pola yang lengkap dan menyebabkan rasa sakit yang jauh lebih banyak daripada yang mereka pecahkan ...
ircmaxell
1
Lihatlah perpustakaan standar scala dan Anda akan menemukan banyak contoh ciri yang berguna.
dmitry

Jawaban:

89

Pendapat pribadi saya adalah bahwa sebenarnya ada sangat sedikit aplikasi untuk ciri-ciri saat menulis kode bersih.

Daripada menggunakan ciri untuk meretas kode ke kelas, lebih baik untuk meneruskan dependensi melalui konstruktor atau melalui setter:

class ClassName {
    protected $logger;

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

Alasan utama mengapa saya menemukan bahwa lebih baik daripada menggunakan sifat adalah bahwa kode Anda jauh lebih fleksibel dengan menghapus kopling keras ke suatu sifat. Sebagai contoh, Anda dapat dengan mudah melewati kelas logger yang berbeda sekarang. Ini membuat kode Anda dapat digunakan kembali dan diuji.

NikiC
sumber
4
Menggunakan ciri-ciri, Anda juga dapat menggunakan kelas logger lain, kan? Cukup edit sifat tersebut, dan semua kelas yang menggunakan sifat tersebut akan diperbarui. Koreksi saya jika saya salah
rickchristie
14
@rickchristie Tentu, Anda bisa melakukannya. Tetapi Anda perlu mengedit kode sumber dari sifat tersebut. Jadi Anda akan mengubahnya untuk setiap kelas yang menggunakannya, bukan hanya kelas tertentu yang Anda inginkan untuk pencatat yang berbeda. Dan bagaimana jika Anda ingin menggunakan kelas yang sama tetapi dengan dua penebang yang berbeda? Atau jika Anda ingin melewati mock-logger saat pengujian? Anda tidak bisa, jika Anda menggunakan sifat, Anda bisa, jika Anda menggunakan injeksi ketergantungan.
NikiC
2
Saya bisa mengerti maksud Anda, saya juga merenungkan apakah sifat itu layak atau tidak. Maksud saya, dalam kerangka kerja modern seperti Symfony 2 Anda memiliki injeksi ketergantungan di semua tempat yang tampaknya superioir atas sifat dalam sebagian besar kasus. Saat ini saya melihat ciri-ciri sebagai tidak lebih dari "compiler assisted copy & paste". ;)
Maks
11
Saat ini saya melihat ciri-ciri sebagai tidak lebih dari "compiler assisted copy & paste". ;) : @Max: Itulah sifat yang dirancang untuk menjadi, jadi itu sepenuhnya benar. Itu membuatnya lebih "dipertahankan", karena hanya ada satu definisi, tetapi pada dasarnya hanya c & p ...
ircmaxell
29
NikiC kehilangan poin: menggunakan suatu sifat tidak mencegah penggunaan Dependency Injection. Dalam kasus ini, suatu sifat hanya akan membiarkan setiap kelas yang mengimplementasikan logging tidak harus menduplikasi metode setLogger () dan pembuatan properti $ logger. Sifat itu akan memberi mereka. setLogger () akan mengetikkan petunjuk tentang LoggerInterface seperti contohnya, sehingga semua jenis logger dapat diteruskan. Gagasan ini mirip dengan jawaban Gordon di bawah ini (hanya sepertinya ia mengetik mengisyaratkan kelas super Logger daripada antarmuka Logger ).
Ethan
205

Saya kira kita harus melihat ke dalam bahasa yang memiliki Ciri-ciri untuk beberapa waktu sekarang untuk mempelajari praktik-praktik Baik / Terbaik yang diterima. Pendapat saya saat ini tentang Trait adalah bahwa Anda hanya harus menggunakannya untuk kode yang harus Anda duplikasi di kelas lain yang memiliki fungsi yang sama.

Contoh untuk sifat Logger:

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;
}

Dan kemudian Anda lakukan ( demo )

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

Saya kira hal penting yang perlu dipertimbangkan ketika menggunakan ciri-ciri adalah bahwa mereka benar-benar hanya potongan kode yang dapat disalin ke dalam kelas. Ini dapat dengan mudah menyebabkan konflik, misalnya, ketika Anda mencoba mengubah visibilitas metode, misalnya

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

Di atas akan menghasilkan kesalahan ( demo ). Demikian juga, metode apa pun yang dideklarasikan dalam sifat yang juga sudah dideklarasikan di kelas penggunaan tidak akan disalin ke dalam kelas, misalnya

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

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

akan mencetak 2 ( demo ). Ini adalah hal-hal yang Anda ingin hindari karena mereka membuat kesalahan sulit ditemukan. Anda juga ingin menghindari menempatkan hal-hal ke dalam sifat-sifat yang beroperasi pada properti atau metode kelas yang menggunakannya, misalnya

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();

bekerja ( demo ) tetapi sekarang sifatnya sangat erat digabungkan ke A dan seluruh gagasan penggunaan kembali horisontal hilang.

Ketika Anda mengikuti Prinsip Segregasi Antarmuka, Anda akan memiliki banyak kelas dan antarmuka kecil. Itu membuat Ciri-Ciri calon yang ideal untuk hal-hal yang Anda sebutkan, misalnya keprihatinan lintas sektor , tetapi tidak untuk membuat objek (dalam arti struktural). Dalam contoh Logger kami di atas, sifat tersebut sepenuhnya terisolasi. Itu tidak memiliki ketergantungan pada kelas-kelas konkret.

Kita dapat menggunakan agregasi / komposisi (seperti yang ditunjukkan di tempat lain di halaman ini) untuk mencapai kelas hasil yang sama, tetapi kelemahan menggunakan agregasi / komposisi adalah bahwa kita harus menambahkan metode proxy / delegator secara manual ke masing-masing dan setiap kelas maka yang seharusnya dapat login. Ciri-ciri menyelesaikan ini dengan baik dengan memungkinkan saya untuk menjaga boilerplate di satu tempat dan secara selektif menerapkannya di tempat yang diperlukan.

Catatan: mengingat sifat-sifat adalah konsep baru dalam PHP, semua pendapat yang diungkapkan di atas dapat berubah. Saya belum punya banyak waktu untuk mengevaluasi konsep itu sendiri. Tapi saya harap cukup baik untuk memberi Anda sesuatu untuk dipikirkan.

Gordon
sumber
41
Itulah use case yang menarik: gunakan antarmuka yang mendefinisikan kontrak, gunakan sifat untuk memenuhi kontrak itu. Bagus
Maks
13
Saya suka programmer seperti ini, yang mengusulkan contoh kerja nyata dengan deskripsi singkat untuk masing-masing Thx
Arthur Kushman
1
Bagaimana jika seseorang menggunakan kelas abstrak saja? Mengganti antarmuka dan sifat, seseorang dapat membuat kelas abstrak. Juga jika antarmuka sangat diperlukan untuk aplikasi, kelas abstrak juga dapat mengimplementasikan antarmuka dan mendefinisikan metode seperti sifat itu. Jadi bisakah Anda menjelaskan mengapa kita masih membutuhkan sifat?
sumanchalki
12
Kelas abstrak @sumanchalki mengikuti aturan Inheritance. Bagaimana jika Anda membutuhkan kelas yang mengimplementasikan Loggable dan Cacheable? Anda perlu kelas untuk memperpanjang AbstractLogger yang perlu memperpanjang AbstractCache. Tapi itu berarti semua Loggables adalah Cache. Itu adalah kopling yang tidak Anda inginkan. Ini membatasi penggunaan kembali dan mengacaukan grafik warisan Anda.
Gordon
1
Saya pikir tautan demo sudah mati
Pmpr
19

:) Saya tidak suka berteori dan berdebat tentang apa yang harus dilakukan dengan sesuatu. Dalam hal ini sifat-sifatnya. Saya akan menunjukkan kepada Anda apa yang menurut saya berguna untuk Anda dan Anda bisa belajar darinya, atau mengabaikannya.

Ciri - mereka hebat untuk menerapkan strategi . Pola desain strategi, singkatnya, berguna ketika Anda ingin data yang sama ditangani (difilter, disortir, dll) secara berbeda.

Misalnya, Anda memiliki daftar produk yang ingin Anda saring berdasarkan beberapa kriteria (merek, spesifikasi, apa pun), atau diurutkan dengan cara yang berbeda (harga, label, apa pun). Anda dapat membuat sifat penyortiran yang berisi fungsi yang berbeda untuk tipe penyortiran yang berbeda (numerik, string, tanggal, dll). Anda kemudian dapat menggunakan sifat ini tidak hanya di kelas produk Anda (seperti yang diberikan dalam contoh), tetapi juga di kelas lain yang membutuhkan strategi serupa (untuk menerapkan jenis numerik ke beberapa data, dll).

Cobalah:

<?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>';
?>

Sebagai catatan penutup, saya berpikir tentang ciri-ciri seperti aksesori (yang dapat saya gunakan untuk mengubah data saya). Metode dan properti serupa yang dapat dipotong dari kelas saya dan ditempatkan di satu tempat, untuk perawatan yang mudah, kode yang lebih pendek dan lebih bersih.

D. Marti
sumber
1
Meskipun ini menjaga antarmuka publik tetap bersih, antarmuka internal mungkin menjadi sangat kompleks dengan ini, terutama jika Anda memperluas ini ke hal-hal lain, seperti warna misalnya. Saya pikir fungsi sederhana atau metode statis lebih baik di sini.
Sebastian Mach
Saya suka istilah itu strategies.
Rannie Ollit
4

Saya senang untuk Ciri karena mereka memecahkan masalah umum ketika mengembangkan ekstensi untuk platform e-commerce Magento. Masalahnya terjadi ketika ekstensi menambahkan fungsionalitas ke kelas inti (seperti katakanlah model Pengguna) dengan memperluasnya. Ini dilakukan dengan mengarahkan autoloader Zend (melalui file konfigurasi XML) untuk menggunakan model Pengguna dari ekstensi, dan meminta model baru itu memperpanjang model inti. ( contoh ) Tetapi bagaimana jika dua ekstensi menimpa model yang sama? Anda mendapatkan "kondisi balapan" dan hanya satu yang dimuat.

Solusinya sekarang adalah mengedit ekstensi sehingga salah satu memperluas model yang lain menimpa kelas dalam sebuah rantai, dan kemudian mengatur konfigurasi ekstensi untuk memuatnya dalam urutan yang benar sehingga rantai warisan bekerja.

Sistem ini sering menyebabkan kesalahan, dan ketika menginstal ekstensi baru perlu untuk memeriksa konflik dan mengedit ekstensi. Ini menyakitkan, dan merusak proses peningkatan.

Saya pikir menggunakan Ciri akan menjadi cara yang baik untuk mencapai hal yang sama tanpa model yang mengganggu ini menimpa "kondisi ras". Memang masih bisa ada konflik jika beberapa Ciri menerapkan metode dengan nama yang sama, tapi saya akan membayangkan sesuatu seperti konvensi namespace sederhana yang bisa menyelesaikan ini sebagian besar.

TL; DR Saya pikir Ciri-ciri bisa berguna untuk membuat ekstensi / modul / plugin untuk paket perangkat lunak PHP besar seperti Magento.

thaddeusmt
sumber
0

Anda dapat memiliki sifat untuk objek hanya baca seperti ini:

  trait ReadOnly{  
      protected $readonly = false;

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

Anda dapat mendeteksi apakah sifat itu digunakan dan menentukan apakah Anda harus menulis objek itu dalam database, file, dll.

Nico
sumber
Jadi kelas yang useakan memanggil sifat ini kemudian akan memanggil if($this -> getReadonly($value)); tetapi ini akan menghasilkan kesalahan jika Anda tidak memiliki usesifat ini. Karenanya contoh ini cacat.
Luceos
Nah, Anda perlu memeriksa apakah sifat tersebut digunakan terlebih dahulu. Jika sifat ReadOnly didefinisikan pada objek, Anda dapat memeriksa apakah itu hanya dibaca atau tidak.
Nico
Saya melakukan bukti generik konsep untuk sifat seperti itu di gist.github.com/gooh/4960073
Gordon
3
Anda harus mendeklarasikan antarmuka untuk ReadOnly untuk tujuan itu
Michael Tsang