Những đặc điểm trong PHP - bất kỳ ví dụ thực tế / thực tiễn tốt nhất nào? [đóng cửa]


148

Các đặc điểm là một trong những bổ sung lớn nhất cho PHP 5.4. Tôi biết cú pháp và hiểu ý tưởng đằng sau các đặc điểm, như sử dụng lại mã ngang cho các nội dung phổ biến như đăng nhập, bảo mật, lưu trữ, v.v.

Tuy nhiên, tôi vẫn không biết làm thế nào tôi sẽ sử dụng các đặc điểm trong các dự án của mình.

Có bất kỳ dự án nguồn mở nào đã sử dụng các đặc điểm không? Bất kỳ bài viết tốt / tài liệu đọc về cách cấu trúc kiến ​​trúc bằng cách sử dụng các đặc điểm?


8
Đây là ý kiến ​​của tôi: một bài viết trên blog về chủ đề mà tôi đã viết về chủ đề này. TL; DR: Về cơ bản, tôi sợ rằng trong khi chúng mạnh mẽ và có thể được sử dụng tốt, thì phần lớn các ứng dụng chúng ta sẽ thấy sẽ hoàn toàn chống lại mô hình và gây ra nhiều đau đớn hơn chúng giải quyết ...
ircmaxell

1
Hãy xem thư viện tiêu chuẩn scala và bạn sẽ tìm ra nhiều ví dụ hữu ích về đặc điểm.
dmitry

Câu trả lời:


89

Ý kiến ​​cá nhân của tôi là thực sự có rất ít ứng dụng cho các đặc điểm khi viết mã sạch.

Thay vì sử dụng các đặc điểm để hack mã vào một lớp, tốt hơn là chuyển các phần phụ thuộc thông qua hàm tạo hoặc qua setters:

class ClassName {
    protected $logger;

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

Lý do chính tại sao tôi thấy rằng tốt hơn so với việc sử dụng các đặc điểm là mã của bạn linh hoạt hơn nhiều bằng cách loại bỏ khớp nối cứng thành một đặc điểm. Ví dụ, bạn có thể chỉ cần vượt qua một lớp logger khác. Điều này làm cho mã của bạn có thể tái sử dụng và kiểm tra được.


4
Sử dụng đặc điểm, bạn cũng có thể sử dụng một lớp logger khác phải không? Chỉ cần chỉnh sửa tính trạng và tất cả các lớp sử dụng tính trạng được cập nhật. Sửa lỗi cho tôi nếu tôi sai
rickchristie

14
@rickchristie Chắc chắn, bạn có thể làm điều đó. Nhưng bạn sẽ cần phải chỉnh sửa mã nguồn của đặc điểm. Vì vậy, bạn sẽ thay đổi nó cho mọi lớp sử dụng nó, không chỉ một lớp cụ thể mà bạn muốn có một logger khác. Và nếu bạn muốn sử dụng cùng một lớp nhưng với hai logger khác nhau thì sao? Hoặc nếu bạn muốn vượt qua một mock-logger trong khi thử nghiệm? Bạn không thể, nếu bạn sử dụng đặc điểm, bạn có thể, nếu bạn sử dụng tiêm phụ thuộc.
NikiC

2
Tôi có thể thấy quan điểm của bạn, tôi cũng đang suy nghĩ xem những đặc điểm đó có đáng hay không. Ý tôi là, trong các khuôn khổ hiện đại như Symfony 2, bạn có sự phụ thuộc vào tất cả mọi nơi có vẻ như siêu năng lực trong các đặc điểm trong hầu hết các trường hợp. Hiện tại tôi thấy các đặc điểm không còn nhiều nữa là "trình biên dịch hỗ trợ sao chép & dán". ;)
Tối đa

11
Hiện tại tôi thấy các đặc điểm không còn nhiều nữa là "trình biên dịch hỗ trợ sao chép & dán". ;) : @Max: Đó chính xác là những đặc điểm được thiết kế, vì vậy điều đó hoàn toàn chính xác. Nó làm cho nó nhiều hơn "duy trì", vì chỉ có một định nghĩa, nhưng nó thực chất chỉ c & p ...
ircmaxell

29
NikiC's đang thiếu điểm: sử dụng một đặc điểm không ngăn cản sử dụng Dependency Injection. Trong trường hợp này, một đặc điểm sẽ chỉ cho phép mọi lớp thực hiện ghi nhật ký không phải sao chép phương thức setLogger () và tạo thuộc tính $ logger. Các đặc điểm sẽ cung cấp cho họ. tập hợp ).
Ethan

205

Tôi đoán người ta sẽ phải xem xét các ngôn ngữ có Đặc điểm trong một thời gian để tìm hiểu các thực tiễn Tốt / Tốt nhất được chấp nhận. Ý kiến ​​hiện tại của tôi về Trait là bạn chỉ nên sử dụng chúng cho mã mà bạn sẽ phải sao chép trong các lớp khác có cùng chức năng.

Ví dụ cho một đặc điểm của 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;
}

Và sau đó bạn làm ( bản demo )

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

Tôi đoán điều quan trọng cần xem xét khi sử dụng các đặc điểm là chúng thực sự chỉ là những đoạn mã được sao chép vào lớp. Điều này có thể dễ dàng dẫn đến xung đột, ví dụ, khi bạn cố gắng thay đổi mức độ hiển thị của các phương thức, ví dụ:

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

Ở trên sẽ dẫn đến một lỗi ( bản demo ). Tương tự, bất kỳ phương thức nào được khai báo trong đặc điểm cũng đã được khai báo trong lớp sử dụng sẽ không được sao chép vào lớp, ví dụ:

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

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

sẽ in 2 ( bản demo ). Đây là những điều bạn sẽ muốn tránh vì chúng khó tìm ra lỗi. Bạn cũng sẽ muốn tránh đưa mọi thứ vào các đặc điểm hoạt động trên các thuộc tính hoặc phương thức của lớp sử dụng nó, ví dụ

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

hoạt động ( bản demo ) nhưng bây giờ tính trạng được kết hợp mật thiết với A và toàn bộ ý tưởng tái sử dụng ngang bị mất.

Khi bạn tuân theo Nguyên tắc phân chia giao diện, bạn sẽ có nhiều lớp và giao diện nhỏ. Điều đó làm cho Traits trở thành một ứng cử viên lý tưởng cho những điều bạn đã đề cập, ví dụ như mối quan tâm xuyên suốt , nhưng không sáng tác các đối tượng (theo nghĩa cấu trúc). Trong ví dụ Logger của chúng tôi ở trên, đặc điểm này hoàn toàn bị cô lập. Nó không phụ thuộc vào các lớp cụ thể.

Chúng ta có thể sử dụng tập hợp / thành phần (như được hiển thị ở nơi khác trên trang này) để đạt được cùng một lớp kết quả, nhưng nhược điểm của việc sử dụng tập hợp / thành phần là chúng ta sẽ phải thêm các phương thức proxy / ủy nhiệm theo cách thủ công cho mỗi và sau đó nên có thể đăng nhập. Các đặc điểm giải quyết điều này một cách độc đáo bằng cách cho phép tôi giữ nồi hơi ở một nơi và áp dụng có chọn lọc khi cần thiết.

Lưu ý: cho rằng các đặc điểm là một khái niệm mới trong PHP, tất cả các ý kiến ​​được trình bày ở trên có thể thay đổi. Tôi chưa có nhiều thời gian để tự đánh giá khái niệm này. Nhưng tôi hy vọng nó là đủ tốt để cung cấp cho bạn một cái gì đó để suy nghĩ.


41
Đó là một trường hợp sử dụng thú vị: sử dụng một giao diện xác định hợp đồng, sử dụng đặc điểm để đáp ứng hợp đồng đó. Tốt một.
Tối đa

13
Tôi thích loại lập trình viên thực thụ này, người đề xuất một ví dụ làm việc thực tế với mô tả ngắn cho mỗi người. Thx
Arthur Kushman

1
Nếu ai đó sử dụng một lớp trừu tượng thay thế thì sao? Thay thế giao diện và đặc điểm, người ta có thể tạo ra một lớp trừu tượng. Ngoài ra nếu giao diện rất cần thiết cho ứng dụng, lớp trừu tượng cũng có thể thực hiện giao diện và định nghĩa các phương thức như đặc điểm đã làm. Vì vậy, bạn có thể giải thích tại sao chúng ta vẫn cần những đặc điểm?
sumanchalki

12
@sumanchalki Lớp trừu tượng tuân theo các quy tắc của Thừa kế. Điều gì nếu bạn cần một lớp thực hiện Loggable và Cache? Bạn sẽ cần lớp để mở rộng AbstractLogger cần mở rộng AbstractCache. Nhưng điều đó có nghĩa là tất cả các Loggables là Caches. Đó là một khớp nối mà bạn không muốn. Nó giới hạn việc tái sử dụng và làm rối đồ thị thừa kế của bạn.
Gordon

1
Tôi nghĩ các liên kết demo đã chết
Pmpr

19

:) Tôi không muốn đưa ra giả thuyết và tranh luận về những gì nên làm với một cái gì đó. Trong trường hợp này đặc điểm. Tôi sẽ cho bạn thấy những gì tôi thấy những đặc điểm hữu ích và bạn có thể học hỏi từ nó hoặc bỏ qua nó.

Đặc điểm - chúng là tuyệt vời để áp dụng các chiến lược . Tóm lại, các mẫu thiết kế chiến lược rất hữu ích khi bạn muốn xử lý cùng một dữ liệu (được lọc, sắp xếp, v.v.).

Ví dụ: bạn có một danh sách các sản phẩm mà bạn muốn lọc dựa trên một số tiêu chí (nhãn hiệu, thông số kỹ thuật, bất cứ thứ gì) hoặc được sắp xếp theo các phương tiện khác nhau (giá, nhãn, bất cứ thứ gì). Bạn có thể tạo một đặc điểm sắp xếp có chứa các hàm khác nhau cho các loại sắp xếp khác nhau (số, chuỗi, ngày, v.v.). Sau đó, bạn có thể sử dụng đặc điểm này không chỉ trong lớp sản phẩm của mình (như được nêu trong ví dụ), mà còn trong các lớp khác cần các chiến lược tương tự (để áp dụng sắp xếp số cho một số dữ liệu, v.v.).

Thử nó:

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

Là một lưu ý cuối cùng, tôi nghĩ về những đặc điểm như phụ kiện (mà tôi có thể sử dụng để thay đổi dữ liệu của mình). Các phương thức và thuộc tính tương tự có thể được cắt ra khỏi các lớp của tôi và được đặt vào một nơi duy nhất, để dễ bảo trì, mã ngắn hơn và sạch hơn.


1
Trong khi điều này giữ cho giao diện công cộng sạch sẽ, giao diện bên trong có thể trở nên thực sự phức tạp với điều này, đặc biệt nếu bạn mở rộng giao diện này sang những thứ khác, ví dụ như màu sắc. Tôi nghĩ rằng các hàm đơn giản hoặc phương thức tĩnh để tốt hơn ở đây.
Sebastian Mach

Tôi thích thuật ngữ này strategies.
Rannie Ollit

4

Tôi rất hào hứng với Traits vì chúng giải quyết một vấn đề phổ biến khi phát triển các tiện ích mở rộng cho nền tảng thương mại điện tử Magento. Sự cố xảy ra khi tiện ích mở rộng thêm chức năng vào lớp lõi (như mô hình Người dùng) bằng cách mở rộng nó. Điều này được thực hiện bằng cách trỏ trình tải tự động Zend (thông qua tệp cấu hình XML) để sử dụng mô hình Người dùng từ tiện ích mở rộng và mô hình mới đó mở rộng mô hình lõi. ( ví dụ ) Nhưng nếu hai phần mở rộng ghi đè lên cùng một mô hình thì sao? Bạn nhận được một "điều kiện cuộc đua" và chỉ có một được tải.

Giải pháp ngay bây giờ là chỉnh sửa các phần mở rộng để người ta mở rộng lớp ghi đè mô hình của người kia trong một chuỗi, sau đó đặt cấu hình tiện ích mở rộng để tải chúng theo đúng thứ tự để chuỗi kế thừa hoạt động.

Hệ thống này thường xuyên gây ra lỗi và khi cài đặt tiện ích mở rộng mới, cần kiểm tra xung đột và chỉnh sửa tiện ích mở rộng. Đây là một nỗi đau, và phá vỡ quá trình nâng cấp.

Tôi nghĩ rằng sử dụng Đặc điểm sẽ là một cách tốt để thực hiện điều tương tự mà không có mô hình gây phiền nhiễu này ghi đè lên "điều kiện chủng tộc". Cấp vẫn có thể có xung đột nếu nhiều phương thức thực hiện các phương thức có cùng tên, nhưng tôi sẽ tưởng tượng một cái gì đó giống như một quy ước không gian tên đơn giản có thể giải quyết phần lớn điều này.

TL; DR Tôi nghĩ rằng Traits có thể hữu ích cho việc tạo các phần mở rộng / mô-đun / plugin cho các gói phần mềm PHP lớn như Magento.


0

Bạn có thể có một đặc điểm cho đối tượng chỉ đọc như thế này:

  trait ReadOnly{  
      protected $readonly = false;

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

Bạn có thể phát hiện nếu đặc điểm đó được sử dụng và xác định wheter hay không, bạn nên viết đối tượng đó trong cơ sở dữ liệu, tệp, v.v.


Vì vậy, lớp mà useđặc điểm này sau đó sẽ gọi if($this -> getReadonly($value)); nhưng điều này sẽ tạo ra một lỗi nếu bạn không có useđặc điểm này. Do đó, ví dụ này là thiếu sót.
Luceos

Chà, bạn cần kiểm tra xem tính trạng này có được sử dụng trước không. Nếu đặc điểm ReadOnly được xác định trên một đối tượng, thì bạn có thể kiểm tra xem nó có chỉ đọc hay không.
Nico

Tôi đã làm một bằng chứng chung về khái niệm cho một đặc điểm như vậy trong gist.github.com/gooh/4960073
Gordon

3
Bạn nên khai báo một giao diện cho ReadOnly cho mục đích đó
Michael Tsang
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.