+8

PHP Traits là gì?

Mở đầu

Một trong những vấn đề của PHP như một ngôn ngữ lập trình đó là chỉ có thể đơn kế thừa (single inheritance). Điều đó có nghĩa là một class chỉ có thể kế thừa được một class khác.
Tuy vậy, rất nhiều trường hợp việc kế thừa từ nhiều class là việc có ích lợi. Ví dụ, chúng ta mong muốn một class kế thừa từ nhiều class khác nhau tránh việc lặp code.
Vấn đề này có thể dẫn tới việc một class có một lịch sử gia đình kéo dài mà đôi khi không cần thiết.
Trong PHP 5.4 có một tính năng mới được gọi là Traits. Một Trait giống với Mixin ở chỗ  nó cho phép kết hợp các Trait classes vào một class hiện có. Điều này có nghĩa là bạn có thể làm giảm sự trùng lặp code và có được những lợi ích trong khi tránh các vấn đề về đa kế thừa.
Trong bài viết này tôi sẽ khám phá đặc điểm của Traits và cho thấy cách bạn có thể sử dụng chúng trong các dự án của bạn như thế nào.

Traits là gì?

Traits hiểu đơn giản là một nhóm các methods mà bạn muốn include nó trong một class khác. Một Trait giống với abstract class không thể khởi tạo trên chính nó.

Một ví dụ của Trait có thể là:

trait Sharable {

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

Bạn có thể include Trait này trong các class khác như sau:

class Post {
    use Sharable;
}

class Comment {
    use Sharable;
}

Bây giờ, nếu bạn tạo một đối tượng từ một trong hai class ở trên, bạn sẽ thấy được chúng đều có một method share() có sẵn:

$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ư những gì bạn thấy ở ví dụ bên trên, cả hai đối tượng PostComment đều có sẵn method share() mặc dù nó không được định nghĩa.

Một Trait đơn giản chỉ là cách copy và paste code trong thời gian chạy.

Điều này có nghĩa là một Trait được sao chép vào class PostCommment bởi vậy khi khởi tạo một đối tượng mới từ class đó, method share() đã có sẵn.

Traits khác Abstract classes như thế nào?

Traits khác với Abstract class bởi vì chúng không phụ thuộc vào sự kế thừa.

Thử tưởng tượng nếu PostComment class kế thừa từ một Abstract class AbstractSocial. Chúng ta có thể muốn nhiều hơn việc chỉ share các posts hay comments trên web mạng xã hội, bởi vậy có lẽ chúng ta có lẽ sẽ sử dụng cây kế thừa phức tạp như sau:

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

Việc kế thừa phức tạp như trên là khá khó chịu, nhưng nó cũng thêm sự phiền phức khi một đối tượng đơn giản không có cấu trúc kế thừa tương tự. Ví dụ, nếu chúng ta có một đối tượng message không muốn share trên mạng xã hội vì đó đối tượng này sẽ đòi hỏi một cấu trúc thừa kế hơi khác.

Traits khác với Interfaces ra sao?

Traits có đặc điểm khá giống với Interfaces. Cả Traits và Interfaces đều luôn luôn đơn giản, ngắn gọn và không sử dụng nhiều nếu không có một class thực tế được implements.

Tuy nhiên sự khác biệt giữa chúng là điểm quan trọng. Một Interface là một giao ước rằng đối tượng này có thể thực hiện điều này, trong khi Trait cho phép một đối tượng có khả năng thực hiện cái gì đó. Ví dụ:

// 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()
    {
        //
    }

}

Trong ví dụ này chúng ta có một Interface Sociable tuyên bố rằng một đối tượng Post có thể likeshare. Trait Sharable triển khai (implements) phương thức share() phương thức like() được triển khai trong lớp Post.

Vì vậy bạn có thể thấy chúng ta có thể gõ gợi ý đối tượng Post để thấy rằng nếu nó là sociable (nó được implements Sociable interface), trong khi các Trait định nghĩa một phương pháp tái sử dụng mà chúng ta có thể kết hợp trong các classes tương tự khác:

$post = new Post;

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

Vậy Traits có những lợi ích nào?

Lợi ích của việc sử dụng Trait đó là bạn giảm việc trùng lặp code trong khi không phải sử dụng đa kế thừa phức tạp mà không có ý nghĩa trong bối cảnh ứng dụng của bạn.

Điều này cho phép bạn xác định Traits đơn giản rõ ràng xúc tích và kết hợp nó trong những chức năng thích hợp.

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

Traits tạo ra việc bạn có thể dễ viết các classes cồng kềnh và có quá nhiều chức năng. Một Trait bản chất là một cách để copy và paste code giữa các classes. Bởi có một cách đơn giản để thêm một nhóm các methods vào một class. Nó rất dễ dàng xa rời single responsibility principle .

Một điểm trừ khác nữa khi sử dụng Traits đó là không thể xem xét tất cả methods của một class khi nhìn vào source code cũng như method bị xung đột hay lặp logic.

Tôi nghĩ rằng khi Traits được sử dụng đúng cách thì nó sẽ là một công cụ tuyệt vời cho việc xử lý của chúng ta. Tuy vậy, Traits cũng có thể trở thành vật chống lưng cho lập trình lười biếng (lazy programming). Bạn có thể giải quyết vấn đề ngay lập tức chỉ với việc thêm một trait. Thường thì Composition là cách tiếp cận tốt hơn kế thừa hoặc sử dụng một Trait.

Các tình huống điển hình nên dùng Traits

Vâng, tôi nghĩ rằng Traits là một cách tuyệt vời để tái sử dụng một đoạn code giữa một tập các class tương tự mà không cần kế thừa từ cùng một abstract class.

Sử dụng ví dụ về socialtrước đó, tưởng tượng rằng chúng ta có các đối tượng Post, Photo, Note, MessageLink. Hầu hết, các đối tượng trên có thể hoán đổi trong hệ thống như chúng được tạo ra và tương tác giữa các users với nhau.

Tuy vậy, Post, Photo, NoteLink là các đối tượng công khai, có thể chia sẻ giữa các users. Trong khi đối tượng Message là tin nhắn riêng không được công khai. Do vậy, Post, Photo, NoteLink được implements bởi Shareable interface:

interface Shareable {
    public function share();
}

Liệu việc lặp lại method share() có cần thiết trong mỗi classes đã implements Shareable interface?

Liệu việc sử dụng AbstractShare class extend mà các đối tượng được implements bởi Shareable interface có ý nghĩa?

Liệu việc chúng ta có method share() được implements như một phần của class AbstractEntity nhưng sau đó lại chặn method đó với đối tượng Message là có ý nghĩa?

Câu trả lời đều là không.

Liệu nó có ý nghĩa để implements một ShareableTrait thỏa mãn các interfaces contract và có thể dễ dàng thêm vào các đối tượng chỉ cần nó?

Câu trả lời là có.

Ví dụ thực tế của việc sử dụng Traits

Việc xác định khi nào cần sử dụng Traits hoặc xác định một trong những phương án giải quyết vấn đề của bạn đôi khi rất khó khăn.

Khi bạn đối mặt với vấn đề này, ý tưởng không tồi là xem xét các open source trên thế giới để biết được cách mà họ áp dụng kỹ thuật này như thế nào.

Theo ý kiến cá nhân của tôi, một Open Source project tận dụng tốt các đặc điểm của Traits là Laravel package Cashier.

Cashier package thêm chức năng cho Eloquent model giúp nó thực sự dễ dàng trong việc quản lý subscriptions trong các ứng dụng Saas (Software as a service).

Tuy nhiên để thêm chức năng cho các models đã tồn tại, bạn sẽ đối mặt với một số khó khăn.

Lựa chọn đầu tiên là chỉ cần implement đơn giản trong các methods của bạn bằng cách copy các method ví dụ từ tài liệu của Cashier. Đây không phải là giải pháp tốt vì bạn có thể gặp các lỗi như copy code sai (sad) hay bất cứ khi nào Cashier update bạn phải duyệt qua tât cả các models và cập nhật lại code (dead).

Lựa chọn thứ hai là extend đối tượng Cashier để kế thừa các methods mà bạn cần. Đây cũng là giải pháp tồi bởi vì điều gì sẽ xảy ra khi ban muốn thêm các methods từ một package để validate cho models của bạn? Bạn sẽ phải viết code một dòng kế thừa phức tạp và khó hiểu. Để rồi một ngày kia đồng nghiệp của bạn hoặc chính bạn khi đọc lại đoạn code này phải thốt lên một câu thằng nào code cái đống sh*t này đây.

Thay vào đó, Cashier package cung cấp một trait cho phép bạn thêm chức năng cho bất kỳ models nào mà không làm lặp code của chính nó hoặc có một dòng kế thừa khó chịu:

use Laravel\Cashier\BillableTrait;
use Laravel\Cashier\BillableInterface;

class User extends Eloquent implements BillableInterface {

    use BillableTrait;

    protected $dates = ['trial_ends_at', 'subscription_ends_at'];

}

Kết luận

  • Traits cung cấp một giải pháp thực sự tốt tránh những khó chịu và lộn xộn của kế thừa có thể nảy sinh trong các ngôn ngữ đơn kế thừa như PHP.
  • Traits cho phép bạn thêm các chắc năng cho các classes của bạn theo chiều ngang mà không quá phức tạp hay lặp code.
  • Traits không phải là câu trả cho tất cả các vấn đề. Sử dụng Traits sai tình huống chắc chắn là một quyết định tồi.
  • Một trait có thể giải quyết tình hình trước mắt của bạn, nhưng sử dụng composition có thể là câu trả lời thực sự cho tình trạng khó khăn của bạn. Hiểu khi nào và ở đâu để sử dụng trait là cách tốt nhất để tăng skill của chính bạn.

Bài viết gốc

What are PHP Traits?


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí