12/08/2018, 13:32

Traits in PHP and Laravel

Bài viết này được dịch từ nguồn What are PHP Traits? có thêm phần chém gió của tác giả bài dịch hy vọng có thể truyền tải cho các bạn chút hiểu biết về Trait trong PHP (bow). Tôi (tác giả bài dịch) biết được đến Trait thông qua một dự án thử việc tại Framgia Vietnam vào tháng 3/2015 (yaoming). ...

Bài viết này được dịch từ nguồn What are PHP Traits? có thêm phần chém gió của tác giả bài dịch hy vọng có thể truyền tải cho các bạn chút hiểu biết về Trait trong PHP (bow). Tôi (tác giả bài dịch) biết được đến Trait thông qua một dự án thử việc tại Framgia Vietnam vào tháng 3/2015 (yaoming).

Trong nội dung bài viết này chúng ta sẽ cùng tìm hiểu thế nào là Trait? Nó làm việc như thế nào? Nó khác Abstract ClassInterface ra sao? Tại sao chúng ta nên dùng, đồng thời đưa ra những case study cụ thể sử dụng.

Giới thiệu

Khi làm việc với PHP, một trong những vấn đề có thể chúng ta đã từng thắc mắc, đã từng gặp phải, đó là việc ta chỉ có thể kế thừa (extends) từ một class cha mà thôi.

Tuy nhiên, nhiều lúc việc kế thừa từ nhiều class lại rất có ích. Chúng ta có thể sử dụng lại các phương thức ở các class khác nhau để tránh việc lặp code. Ví dụ tôi có một class A mang phương thức X, đồng thời tôi lại có class B mang phương thức Y. Tôi phải thiết kế một class C mang phương thức Z đồng thời muốn sử dụng lại phương thức XY nói ở trên. Tôi phải làm thế nào trong khi tôi là PHP Coder mà ông PHP lại không hỗ trợ tôi đa kế thừa @@

Một giải pháp "chày cối" là cho class C kế thừa từ class B (tôi sử dụng được Y) rồi lại cho class B kế thừa từ class A (tôi sử dụng được X). Tuy nhiên đây mới là ví dụ đơn giản với hai class A và B, trường hợp mà yêu cầu cần dùng nhiều phương thức ở nhiều class khác nhau hơn thì việc kế thừa như tôi nói ở trên lại trở thành một thảm họa (huhuhu).

Nhận ra sự bất cập này, trong bảng PHP 5.4 đã đưa ra một khái niệm, và đó chính là Traits. Về cơ bản thì chúng ta có thể tưởng tượng Trait giống với Mixin, cho phép chúng ta nhúng các Trait class vào các class khác. Do đó tránh được việc lặp code, có thể tái sử dụng trong khi tránh được vấn đề như tôi nói ở trên ở việc kế thừa nhiều tầng.

Traits là gì?

Trait có thể hiểu như một class, là nơi tập hợp một nhóm phương thức (method) mà chúng ta muốn sử dụng trong class khác. Cũng giống như Abstract Class, chúng ta không thể khởi tạo một đối tượng từ Trait.

Hãy cùng thử cài đặt một ví dụ đơn giản bằng việc thiết kế một Trait:

trait Sharable {

  public function share($item)
  {
    return 'share this item';
  }

}

Và sử dụng trong các class khác:

class Post {

  use Sharable;

}

class Comment {

  use Sharable;

}

Bạn có thể tưởng tượng việc sử dụng Trait như trên giống như việc chúng ta viết phương thức share() cho cả class PostComment. Cả hai class đều sở hữu phương thức này, và do vậy ta có thể sử dụng rất đơn giản như sau:

$post = new Post;
echo $post->share('); // 'share this item'

$comment = new Comment;
echo $comment->share('); // 'share this item'

Traits hoạt động như thế nào?

Như bạn đã thấy ở bên trên thì cả hai object của class Post và Comment đều có phương thức share() mặc dù phương thức này không hề được định nghĩa trong class.

Trait được đưa ra đơn giản chỉ là một cách để bạn copy and paste code giữa các class. Đó chính là lý do tại sao phương thức share() có thể được sử dụng khi khởi tạo đối tượng cũng như trong chính bản thân class Post hay Comment. Cụ thể như sau:

class Post {

  use Sharable;

  public function shareWithFacebook() {
    echo $this->share(');
    echo 'Facebook shared!';
  }
}

Traits khác với Abstract Class thế nào?

Trait khác với Abstract Class vì nó không dựa trên sự thừa kế. Tưởng tượng rằng nếu class Post và class Comment phải kế thừa từ một AbstractSocial class. Chúng ta dường như muốn nhiều hơn là chỉ share post và comment lên mạng xã hội. Tuy nhiên việc sử dụng abstract class khiến chúng ta phải xây dựng một mô hình kế thừa hết sức phức tạp như sau:

class AbstractValidate extends AbstractCache {}
class AbstractSocial extends AbstractValidate {}
class Post extends AbstractSocial {}

Traits khác với Interfaces thế nào?

Có thể nói về cơ bản thì Trait và Interface khá giống nhau về tính chất sử dụng. Cả hai đều không thể sử dụng nếu không có một class được implement cụ thể. Tuy nhiên chúng cũng có những sự khác nhau hết sức rõ rệt.

Interface có thể hiểu như một bản "hợp đồng" (nếu sử dụng) chỉ ra rằng: "đối tượng có thể làm việc này", do vậy bạn phải implement nó thì mới sử dụng được. Trong khi đó Trait chỉ nói : "đối tượng có khả năng làm việc này".

Ta hãy cùng đi vào một ví dụ cụ thể như sau:

// Interface
interface Sociable {

  public function like();
  public function share();

}

// Trait
trait Sharable {

  public function share($item)
  {
    // share this item
  }

}

// Class
class Post implements Sociable {

  use Sharable;

  public function like()
  {
    //
  }

}

Chúng ta có interface Sociable chỉ ra rằng đối tượng Post có thể like() và share(). Trong khi đó Sharable Trait implement phương thức share() và phương thức like() lại được implement bởi chính class Post.

Bạn có thể thấy chúng ta có thể type hint đối tượng Post để kiểm tra xem nó có được implement từ Sociable interface hay không, đồng thời ta có thể thấy phương thức share() triển khai trong Trait không những sử dụng cho việc implement từ interface mà còn có thể mang đi sử dụng cho các class tương tự khác:

$post = new Post;

if($post instanceOf Sociable)
{
  $post->share('hello world');
}

Dùng Traits có lợi thế nào?

  • Giảm việc lặp code.
  • Tránh được việc kế thừa nhiều tầng nhiều lớp khá phức tạp trong tổng thể hệ thống, sẽ khó maintain sau này.
  • Định nghĩa ngắn gọn, sau đó có thể đặt sử dụng ở những nơi cần thiết, sử dụng được ở nhiều class cùng lúc.

Nhược điểm của Traits

  • Trait có thể tạo ra các class mang quá nhiều trách nhiệm (responsibility). Trait được tạo ra chủ yếu dựa trên tư tưởng "copy and paste" code giữa các class. Chúng ta có thể dễ dành thêm một tập hợp các phương thức vào class thông qua việc sử dụng Trait. Điều này vi phạm nguyên tắc Single Responsibility Principle.
  • Sử dụng Trait khiến chúng ta khó khăn trong việc xem tất cả các phương thức của một class, do vậy khó để có thể phát hiện được một phương thức bất kỳ có bị trùng lặp hay không.
  • Trait cảm giác như công cụ hữu ích cho những kẻ lười, khi muốn thêm vào để giải quyết vấn đề ngay lập tức. Thường thì Composition là một kiến trúc ổn hơn cho việc kế thừa hay sử dụng Trait.

Những tình huống cụ thể sử dụng Traits

Ta sẽ tự đặt ra câu hỏi là trong tình huống nào sử dụng Trait sẽ là giải pháp hay?

Tôi nghĩ Traits là một cách thức tuyệt vời khi chúng ta muốn tái sử dụng code giữa các class có tính chất tương tự nhau nhưng không kế thừa từ một abstract class cụ thể nào.

Trong các ứng dụng mạng xã hội, tưởng tượng rằng chúng ta có các đối tượng Post, Photo, Note, Message và Link. Những đối tượng này tương tác với nhau trong hệ thống của chúng ta và chúng được tạo ra và tương tác giữa các người dùng hệ thống.

Tuy nhiên, Post, Photo, Note và Link có thể chia sẻ public lẫn nhau giữa các user, trong khi đó thì đối tượng Message lại là các private message không thể để public được.

Đối tượng Post, Photo, Note, và Link đều nên được implement từ một interface Shareable:

interface Shareable {

  public function share();

}

Các câu hỏi đặt ra cho chúng ta như sau:

  1. Chúng ta có nên viết phương thức share() ở trong tất cả các class implement từ Shareable interface? Câu trả lời là: KHÔNG.
  2. Chúng ta có nên tạo ra một AbstractShare class để các class của chúng ta kế thừa, và đồng thời các class đó implement Shareable interface? Câu trả lời cũng là: KHÔNG.

Giải pháp của chúng ta nên thực hiện đó là implement phương thức share() trong một ShareableTrait, đồng thời chúng ta có thể thêm vào những class cần thiết sử dụng.

Ví dụ cụ thể về việc sử dụng Trait

Trong bài viết của tác giả có lấy ví dụ về package Cashier trong Laravel, tuy nhiên tôi xin phép không phân tích về package này vì có thể có người chưa từng sử dụng nó (bản thân tác giả của bài viết này cũng chưa sử dụng).

Tôi xin giới thiệu với các bạn về một tính năng mà vốn Laravel đã có từ những phiên bản 4.x trong đó có sử dụng Trait. Hẳn là nếu bạn từng làm việc với Laravel, cụ thể hơn là với Eloquent, chắc các bạn đến khái niệm Soft Delete mà Laravel đưa ra. Tưởng tượng đơn giản thì thay bằng việc delete vật lý bản ghi trong Database, chúng ta chỉ delete logic thôi. Bản ghi đó vẫn còn nhưng đang ở trạng thái deleted.

Về Soft Delete thì bạn có thể tưởng tượng như chức năng thùng rác trên hệ điều hành của chúng ta. Khi chúng ta xóa, dữ liệu sẽ vào thùng rác. Bạn hoàn toàn có thể recover dữ liệu đó hoặc vào thùng rác xóa hẳn dữ liệu khỏi ổ cứng.

Như vậy là ta đã có cái nhìn tổng quan về Soft Delete rồi, giờ ta sẽ đi sâu vào xem Laravel họ xử lý vấn đề này thế nào? Sử dụng Trait ra sao? Tôi sẽ dùng tài liệu tham chiếu của phiên bản Laravel 5.2. Ta sẽ tìm hiểu về cách cài đặt Soft Deleting:

namespace App;

use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentSoftDeletes;

class Flight extends Model
{
    use SoftDeletes;

    /**
     * The attributes that should be mutated to dates.
     *
     * @var array
     */
    protected $dates = ['deleted_at'];
}

Đây là một Model Flight mới được tạo ra:

  • use IlluminateDatabaseEloquentModel; để khai báo sử dụng base model mà Laravel đã dựng lên, tất cả các Model của chúng ta đều được kế thừa từ Base Model này.
  • use IlluminateDatabaseEloquentSoftDeletes; chính là khai báo sử dụng Trait SoftDeletes.
  • use SoftDeletes; cách gọi này giống với những ví dụ ở trên, để ta có thể sử dụng Trait trong class Flight.

Flow sử dụng Soft Delete hết sức đơn giản:

  • Trong bảng của chúng ta ngoài created_at và updated_at sẽ có thêm trường deleted_at để tracking lại trạng thái đã xóa hay chưa? Dữ liệu NULL sẽ là trạng thái chưa xóa và có giá trị datetime sẽ lại thời điểm xóa bản ghi.
  • Khi chỉ muốn delete logic (soft delete) ta sẽ sử dụng hàm delete() (VD: $flight->delete()), dữ liệu trường deleted_at sẽ được cập nhật.
  • Khi muốn delete vật lý (xóa hẳn khỏi Database) ta sẽ sử dụng hàm forceDelete() (VD: $flight->forceDelete()), bản ghi sẽ biến mất hỏi Database mà không còn một dấu vết nào             </div>
            
            <div class=
0