Bàn về Active Record

Nay cong đít lên viết, ko có thời gian mở đầu dông dài nữa =)) thôi vào đề chính luôn.

Active Record Pattern là một design pattern hết sức phổ biến. Có thể bạn ít khi nghe nhắc đến khái niệm này ( hoặc nếu bạn làm RoR thì bạn đã nghe, và nhìn title bạn có thể nghĩ ngay đến Active Record model được sử dụng trong RoR ), tuy nhiên, rất có khả năng bạn đã từng sử dụng nó rồi đấy. Ví dụ như trong PHP, các framework phổ biến có thể kể đến như Laravel , Yii, Symphony, CakePHP, ... etc tất cả đều implement active record pattern. Về mặt khái niệm, có thể nói một cách đơn giản thì active record pattern là một cách tiếp cận mà trong đó, các row trong (relational) database của ta, được map 1-1 với các object. Đến đây chắc hẳn các bạn đã thấy quen thuộc hơn, bởi lẽ như đã nó ở trên, active record pattern rất phổ biến và được rất nhiều các framework sử dụng. Lợi điểm mà nó mang lại là quá rõ ràng : với việc map 1-1 giữa object và row dữ liệu, các công việc truy xuất hay thêm sửa xóa dữ liệu của ta trở nên 'uniform' và tiện lợi hơn rất nhiều. Tuy nhiên, cách làm này cũng ko hẳn đã là hoàn hảo. Và trước khi đi tìm hiểu sâu hơn một chút về nó, trước hết ta hãy cùng nhìn lại hai khái niệm hết sức cơ bản : data structure và object.

Data Structure và Object

Hai khái niệm nghe khá quen thuộc đúng không, nhưng liệu bạn có dễ dàng phân biệt được chúng ? Trong buổi những ngày đầu học về OOP, có thể đôi lần bạn đã nhắc nhở, rằng data của một object nên được ẩn đi, và cái nên để lộ ra phía public nên là các method. Ví dụ như ta có một object Car, các thuộc tính ví dụ như màu sắc, chủng loại ... của nó, ta ko nên để public ra ngoài , nghĩa là ko thể gọi trực tiếp $car->color hay $car->brand . Cái mà phía public có thể gọi, đó là các method, các behavior của cái xe, ví dụ như $car->start(), hay $car->stop(). Trong trường hợp ta muốn lấy các thuộc tính của chiếc xe, ta cũng lấy qua các method, ví dụ như $car->getColor() hay $car->getBrand(). Túm lại, đối với object hidden data, exposed behavior.

Còn về data structure thì sao. Với những người làm PHP, khái niệm này có lẽ hơi khó hơn một chút, vì trong PHP, array có lẽ là kiểu data struct duy nhất thường được sử dụng, nên có lẽ ta cần nhớ lại một chút về kiểu struct trong C++ mà ta được học ở trường. Với data structure thì ngược lại, các thuộc tính (data) của nó hoàn toàn được public ra bên ngoài, nhưng data structure không hề có các behavior. Nếu bên trong có các phương thức public , thì các method này cũng hoàn toàn chỉ mang tính điều hướng, chứ không hề chứa các logic business. Tóm lại, với data structure exposed data, no behavior

Vậy là ta đã phân biệt được giữa 2 khái niệm này rồi đúng không, giờ hãy cùng ngẫm về điểm cộng và điểm trừ của từng loại nhé. Giờ ta thử đặt ra một tình huống, ta có 2 loại user là member và admin phân biệt với nhau bằng thuộc tính type. Mỗi loại user này có trang homepage khác nhau, và bây giờ ta đang muốn viết hàm lấy về trang homepage của một user . Làm theo như định nghĩa ở trên, ta thử xem hai trường hợp, sử dụng một Object User và sử dụng một data structure user ( mình code PHP nên xin tạm lấy PHP làm ví dụ, và vì thế data structure ở đây xin dùng một array )

Dùng Object
abstract class User
{
    private $type;
    
    public function getType()
    {
        return $this->type;
    }
    abstract public function getHomePage() {}
}
class Member extends User
{
    public function getHomepage() {
        return view('member.home');
    }
}
class Admin extends User
{
    public function getHomepage() {
        return view('admin.home');
    }
}

class UsersController
{
    public function home($user)
    {
        return $user->getHomepage();
    }
}
Dùng Data Structure
class UsersController
{
    const USER_TYPE_ADMIN = 1;
    $user = [
        'type' => self::USER_TYPE_ADMIN
    ];
    
    public function home($user)
    {
        switch($user['type']) {
            case self::USER_TYPE_ADMIN:
                return $this->getAdminHomepage();
            case self::USER_TYPE_MEMBER:
                return $this->getMemberHomepage();
            case default:
                break;
        }
    }
    
    private function getAdminHomepage()
    {
        return view('admin.home');
    }
    
    private function getMemberHomepage()
    {
        return view('member.home');
    }
}

Ví dụ hơi củ chuối, nhưng tạm thế đã. Đúng theo nguyên tắc như trên, ta có object với các thuộc tính được hidden, và expose ra bên ngoài các method của nó. Ta có data structure ( ở đây là array, có các thuộc tính expose ra ngoài ,nhưng ko có behavior nào được định nghĩa cho nó ). Trong trường hợp, giả sử như có thêm một type khác , moderator chẳng hạn, ta chỉ cần thay đổi trong model User là đủ, các đoạn code logic sử dụng object User đều không bị ảnh hưởng. Ngược lại, nếu như có thêm một type , các đoạn code logic sử dụng data structure $user đều sẽ bị ảnh hưởng, bạn sẽ phải thêm case vào trong vòng switch,và logic tương ứng với nó. ( tất nhiên là trong thực tế, chuyện này có thể tránh một cách dễ dàng bằng cách tổ chức lại code - viết một class service, trong đó có hàm getHomepage() nhận đầu vào là một array, sau đó code logic gọi đến service này - là một cách. Ở ví dụ này, ta đang muốn so sánh việc sử dụng object và sử dụng data struct ) . Code của ta lúc này sẽ trở thành

// Thêm
class Moderator extends User
{
    public function getHomepage() {
        return view('moderator.home');
    }
}

// Code logic không đổi
class UsersController
{
    public function home($user)
    {
        return $user->getHomepage();
    }
}
class UsersController
{
    // Code logic bị ảnh hưởng
    public function home($user)
    {
        switch($user['type']) {
            case self::USER_TYPE_ADMIN:
                return $this->getAdminHomepage();
            case self::USER_TYPE_MEMBER:
                return $this->getMemberHomepage();
            case self::USER_TYPE_MODERATOR:
                return $this->getModeratorHomepage();
            case default:
                break;
        }
    }
    
    // Thêm
    private function getModeratorHomepage()
    {
        return view('moderator.home');
    }
}

Trong một tình huống khác, giả sử, ta muốn thêm một behavior cho User, chẳng hạn getProfile chẳng hạn. Lúc đó, code của ta sẽ có dạng

Dùng Object
abstract class User
{
    private $type;
    
    public function getType()
    {
        return $this->type;
    }
    abstract public function getHomePage() {}
    abstract public function getProfilePage() {}
}
class Member extends User
{
    public function getHomepage() {
        return view('member.home');
    }
    public function getProfilePage() {
        return view('member.profile');
    }
}
class Admin extends User
{
    public function getHomepage() {
        return view('admin.home');
    }
    public function getProfilePage() {
        return view('admin.profile');
    }
}

class UsersController
{
    public function home($user)
    {
        return $user->getHomepage();
    }
}
Dùng Data Structure
class UsersController
{
    const USER_TYPE_ADMIN = 1;
    $user = [
        'type' => self::USER_TYPE_ADMIN
    ];
    
    public function home($user)
    {
        switch($user['type']) {
            case self::USER_TYPE_ADMIN:
                return $this->getAdminHomepage();
            case self::USER_TYPE_MEMBER:
                return $this->getMemberHomepage();
            case default:
                break;
        }
    }
    
    public function profile($user)
    {
        switch($user['type']) {
            case self::USER_TYPE_ADMIN:
                return $this->getAdminProfile();
            case self::USER_TYPE_MEMBER:
                return $this->getMemberProfile();
            case default:
                break;
        }
    }
    
    private function getAdminHomepage()
    {
        return view('admin.home');
    }
    
    private function getMemberHomepage()
    {
        return view('member.home');
    }
    
    private function getAdminProfile()
    {
        return view('admin.profile');
    }
    
    private function getMemberProfile()
    {
        return view('member.profile');
    }
}

Ở đây, nếu ta dùng object, khi có sự thay đổi về mặt function xảy ra, object và các object con của nó đều bị ảnh hưởng, qua đó các đoạn code logic sử dụng object đều cần phaỉ được kiểm tra lại ( rebuild, retest, redeploy ). Trong khi đó, nếu sử dụng structre, các đoạn code cũ của ta không hề bị ảnh hưởng ( hàm getHomepage() đảm bảo chạy ổn định )

Tóm lại, object và data structure như hai thái cực đối lập, cover những mảng khác nhau và bổ sung lẫn nhau:

  • Object : hidden data, exposed behavior. Không bị ảnh hưởng khi có sự thay đổi về mặt type, nhưng dễ bị ảnh hưởng khi có sự thay đổi về mặt function.
  • Data Structure : exposed data, no behavior. Dễ bị ảnh hưởng khi có thay đổi về type, nhưng khi có thay đổi về mặt function, code logic cũ sử dụng data structure không bị ảnh hưởng.

Active Record là gì

Như phần trên, ta cùng nhớ lại 2 dạng biểu thị dữ liệu, object và data structure. Ta cũng đã thấy, mỗi loại có một đặc tính riêng, và điểm mạnh yếu riêng. Trên lí thuyết, ứng dụng của ta nên áp dụng cả 2 loại dữ liệu này. Với những phần mà logic đã ổn định, nghĩa là ít có khả năng xuất hiện thêm các function mới, thay vào đó, nó có khả năng xuất hiện những type mới, ta nên thiết kế code logic ở đây dựa trên object. Và ngược lại, những phần mà ít có khả năng xuất hiện type mới, nhưng dễ xuất hiện logic mới, ta nên sử dụng data structure. Thế nhưng, câu hỏi đặt ra là, Active record pattern, các model của nó thuộc loại gì, nó giống object hơn, hay giống data structure hơn ? Câu trả lời, có lẽ là cả hai : các framework áp dụng active record pattern đã cố hòa 2 loại khái niệm này vào làm một. Lấy ví dụ như EloquentModel của Laravel. Về mặt bản chất, đã theo active record pattern, nghĩa là có sự mapping 1-1 giữa object với data row trong cơ sở dữ liệu, nên các model theo pattern này đều giống với data structure. Tuy nhiên, cũng nhờ sự mapping này, các code logic tương tác với database của các model trở nên đồng bộ hơn rất nhiều. Qua đó, ta hoàn toàn có thể viết code logic loại này vào trong base model, và có thể áp dụng chung cho tất cả các bảng dữ liệu. EloquentModel ( và các loại model tương tự theo active record pattern ) ra đời như vậy, một kiểu data structure được gắn thêm code xử lí logic. Ưu điểm của nó thì khỏi phải bàn cãi, tốc độ viết code cho các logic CRUD cơ bản chắc phải nhanh hơn gấp trăm lần, nhưng liệu với cách làm hybrid này, nó có tránh được hoàn toàn các nhược điểm của data structure ?