Lập trình hướng đối tượng trong PHP – Phần 2: Tính kế thừa

Loạt bài viết về Lập trình hướng đối tượng trong PHP gồm 4 phần, hiện tại bạn đang ở trong phần 2.

1. Mở đầu

Trong phần một chúng ta đã được làm quen với các thuật ngữ cơ bản như class, object, tính mô đun hóa và tính trìu tượng trong OOP. Trong bài viết tiếp theo này, bạn sẽ cùng chúng tôi khám phá những vấn đề cốt lõi của OOP là tính kế thừa, tính đóng gói, cách ghi đè phương thức trong kế thừa và cuối cùng là phạm vi truy cập các thành phần trong lớp.

Sự thừa kế trong class là một class có nguồn gốc từ một class khác, cũng giống như con người, con cái được thừa hưởng những phẩm chất từ cha mẹ. Trong thế giới hướng đối tượng, các phẩm chất này là các thuộc tính và phương thức. Thông qua kế thừa, một class được “sinh ra” với cùng thuộc tính và phương thức của class mà nó kế thừa, lớp kế thừa thậm chí còn có những phẩm chất riêng mà cha mẹ không có.

Kế thừa trong lập trình hướng đối tượng

Trong trường hợp này, lớp ChildrenClass kế thừa ParentClass mà không có những phẩm chất riêng nào. Trong sơ đồ UML tiếp theo, các class con là ChildrenClass1 và ChildrenClass2 đã kế thừa và phát huy, nó đã có những “phẩm chất” riêng.

Kế thừa và có phẩm chất riêng trong lập trình hướng đối tượng

Sự kế thừa có thể diễn ra nhiều “đời” trong lập trình hướng đối tượng, không giới hạn số lần kế thừa. Chúng ta cùng xem sơ đồ UML tiếp theo, lớp ChildrenClass kế thừa từ ParentClass, lớp GrandChildClass lại kế thừa từ ChildrenClass.

Kế thừa nhiều đời trong lập trình hướng đối tượng

Khi bạn định nghĩa một class bằng cách kế thừa một class khác, sẽ không mất thời gian để thiết kế lại class. Bạn có thể thêm các thuộc tính và phương thức mới, tuy nhiên chúng ta có thể thay đổi hành vi các phương thức được kế thừa từ lớp cha? Nếu thay đổi định nghĩa lớp cha sẽ dẫn đến các lớp kế thừa khác bị ảnh hưởng. Thay vào đó, chúng ta có thể ghi đè phương thức được kế thừa từ lớp cha trong lớp con. Đây chính là tính đa hình trong OOP, nơi mà gọi cùng một phương thức sẽ cho các kết quả khác nhau phụ thuộc vào loại đối tượng. Các thành phần class có thể truy nhập và thay đổi một class khác, điều này đôi khi không tốt và trong lập trình hướng đối tượng khái niệm phạm vi truy cập (visibility) xuất hiện, nó có đưa ra các phạm vi thay đổi các thành phần của class.

1. Sự kế thừa class

Một trong những cách để lập trình nhanh là khả năng sử dụng lại các định nghĩa class, quá trình này chính là kế thừa. Trở lại với ví dụ về class User, class này có các thuộc tính username, userId, email, password và có các phương thức là login, logout. Bạn có thể tạo ra một class khác là Admin, nó được mở rộng từ User. Một đối tượng Admin có thể có thêm thuộc tính accessLevel và phương thức editUser.

Class Admin kế thừa từ class User

Như vậy giữa hai class có một mối quan hệ “is a”, Admin là một dạng của User. Để tạo một class con từ class cha, sử dụng từ khóa extends. Nếu bạn đã định nghĩa lớp ClassName ở bài trước, bạn có thể tạo một lớp con như sau:

class ChildClass extends ClassName { 

}

Class ChildClass sẽ sở hữu các thành phần của lớp cha ClassName, bạn có thể thay đổi ChildClass để đáp ứng các yêu cầu riêng mà không thay đổi gì lớp cha ClassName.

Chú ý: Để kiểm tra xem một đối tượng có phải là một thực thể của class nào đó không sử dụng từ khóa instanceof

if ($obj instanceof "ClassName") {
    // $obj là một thực thể của lớp ClassName
}

Chúng ta cùng thực hành lý thuyết trên với một ví dụ cụ thể, tạo file inheritance.php trong thư mục C:/xampp/htdocs/OOP (Xem phần 1 để cài đặt môi trường thực hành cho loạt bài Lập trình hướng đối tượng trong PHP).

<!doctype html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>Ví dụ về kế thừa trong lập trình hướng đối tượng</title>
   <link rel="stylesheet" href="style.css">
</head>
<body>
    <?php
    #------------ ĐỊNH NGHĨA CLASS ----------------------#

    /* Class Pet.
     * Thuộc tính: name.
     * Phương thức:
     * - __construct()
     * - eat()
     * - sleep()
     */
    class Pet {
        public $name;

        // Hàm khởi tạo thiết lập tên cho vật nuôi
        function __construct($pet_name) {
            $this->name = $pet_name;
        }
        // Vật nuôi có thể ăn
        function eat() {
            echo "<p>$this->name đang ăn.</p>";
        }
        // Vật nuôi có thể ngủ
        function sleep() {
            echo "<p>$this->name đang ngủ.</p>";
        }
    } // End of Pet class.

    /* Class Mèo kế thừa class Vật nuôi
     * Mèo có thêm phương thức: climb().
     */
    class Cat extends Pet {
        // Mèo có thể trèo tường
        function climb() {
            echo "<p>$this->name đang trèo tường.</p>";
        }
    } // End of Cat class.

    /* Class chó kế thừa class Vật nuôi
     * Chó có thêm phương thức: fetch().
     */
    class Dog extends Pet {
        // Chó có thể lấy đồ vật về.
        function fetch() {
            echo "<p>$this->name đang lấy đồ vật cho chủ.</p>";
        }
    } // End of Dog class.

    #------------ KẾT THÚC ĐỊNH NGHĨA CLASS ----------------------#

    // Tạo một con chó tên Jonh
    $dog = new Dog('Jonh');

    // Tạo một con mèo tên Mina
    $cat = new Cat('Mina');

    // Cho hai con này ăn
    $dog->eat();
    $cat->eat();

    // Cho hai con vật này ngủ
    $dog->sleep();
    $cat->sleep();

    // Chó thì bắt lấy đồ vật còn mèo bắt trèo tường
    $dog->fetch();
    $cat->climb();

    // Xóa đối tượng khi không dùng đến
    unset($dog, $cat);
    ?>
</body>
</html>

Chúng ta cùng xem sơ đồ UML các class trong ví dụ trên để thấy rõ hơn:

UML trong ví dụ inheritance

Trong ví dụ này cả hai class Chó và Mèo đều được mở rộng từ class Vật nuôi, do vậy hai lớp này sở hữu các thành phần trong class Vật nuôi. Class Chó có thêm phương thức chạy lấy đồ cho chủ (fetch) còn Mèo có thêm phương thức trèo trường. Như vậy tuy cùng được sinh ra bởi lớp cha là Vật nuôi nhưng các lớp Mèo, Chó có những bản sắc riêng. Thực hiện chạy ví dụ xem thế nào http://oop.dev/inheritance.php

Ví dụ kế thừa trong lập trình hướng đối tượng

2. Phương thức khởi tạo và phương thức hủy trong kế thừa

Trong ví dụ về vật nuôi ở trên, chúng ta đã tạo ra một class (Pet) và từ đó sinh ra các class khác (Dog và Cat). Trong các class này có các phương thức riêng của nó như climb(), fetch(). Như trong bài trước, có hai phương thức thông dụng cho các class là phương thức khởi tạo __contstruct và phương thức hủy __destruct. Class Pet có phương thức khởi tạo và không cần phương thức hủy. Chuyện gì sẽ xảy ra nếu các class Dog và Cat cũng có phương thức khởi tạo. Mặc định, phương thức khởi tạo có tên là __construct, vậy PHP làm cách nào để xác định được nó chạy hàm _construct của lớp cha hay lớp con?

Để xử lý vấn đề này, PHP sẽ luôn gọi phương thức khởi tạo của lớp kế thừa, nguyên tắc này cũng áp dụng cho phương thức hủy. Không giống như các ngôn ngữ lập trình hướng đối tượng khác, trong PHP, khi tạo ra một đối tượng của lớp con, hàm khởi tạo của lớp cha không được tự động thực hiện.

Quay lại ví dụ class về hình chữ nhật Rectangle trong Phần 1, chúng ta sẽ tạo ra file square.php trong thư mục OOP để thực hiện

<!doctype html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>Ví dụ về hàm khởi tạo và hàm hủy trong kế thừa</title>
   <link rel="stylesheet" href="style.css">
</head>
<body>
    <?php

    require('Rectangle.php');

    // Tạo class Square và đưa vào hàm khởi tạo riêng
    class Square extends Rectangle {
        // Hàm khởi tạo với 1 tham số là một cạnh của hình vuông
        function __construct($side = 0) {
            $this->width = $side;
            $this->height = $side;
        }

    } // End of Square class.

    // Tính toán với hình chữ nhật
    $width = 21;
    $height = 98;

    echo "<h2>Hình chữ nhật có chiều rộng $width và chiều cao $height</h2>";

    $rect = new Rectangle($width, $height);

    echo '<p>Chu vi hình chữ nhật: ' . $rect->getPerimeter() . '</p>';
    echo '<p>Diện tích hình chữ nhật: ' . $rect->getArea() . '</p>';

    // Tính toán với hình vuông
    $side = 60;

    echo "<h2>Hình vuông với mỗi cạnh là $side</h2>";

    $squr = new Square($side);

    echo '<p>Chu vi hình vuông: ' . $squr->getPerimeter() . '</p>';
    echo '<p>Diện tích hình vuông: ' . $squr->getArea() . '</p>';

    // xóa đối tượng sau khi sử dụng xong
    unset($rect, $squr);
    ?>
</body>
</html>

Class Square được mở rộng từ class Rectangle, tuy nhiên với hình vuông thì chỉ cần 1 cạnh là đã xác định do đó không có lý do gì để truyền cả chiều rộng và chiều cao khi tạo ra đối tượng hình vuông. Bởi vậy, một hàm khởi tạo cho class Square được định nghĩa với chỉ duy nhất một tham số. Chúng ta cùng xem kết quả http://oop.dev/square.php

Ví dụ hàm khởi tạo và hàm hủy trong kế thừa

3. Ghi đè một phương thức trong kế thừa

Trong phần trên chúng ta đã biết một class có thể kế thừa từ một class khác và cách các class con có những phương thức riêng. Trong ví dụ thực hành, các lớp con có thể định nghĩa hàm khởi tạo riêng. Giống như cách thức này, tạo một định nghĩa phương thức để thay thế trong class con có thể được áp dụng với tên gọi là ghi đè phương thức.

Để ghi đè một phương thức trong PHP, lớp con cần phải định nghĩa phương thức với tên và số tham số giống với phương thức lớp cha.

Chúng ta cùng quay trở lại với ví dụ ở trong phần kế thừa, trong class Pet chúng ta sẽ tạo ra thêm phương thức play() và nó thực hiện khác nhau ở mỗi class con. Tạo file override_method.php trong thư mục OOP với nội dung sau:

<!doctype html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>Ví dụ về ghi đè phương thức trong kế thừa</title>
   <link rel="stylesheet" href="style.css">
</head>
<body>
    <?php
    #------------ ĐỊNH NGHĨA CLASS ----------------------#

    /* Class Pet.
     * Thuộc tính: name.
     * Phương thức:
     * - __construct()
     * - eat()
     * - sleep()
     */
    class Pet {
        public $name;

        // Hàm khởi tạo thiết lập tên cho vật nuôi
        function __construct($pet_name) {
            $this->name = $pet_name;
        }
        // Vật nuôi có thể ăn
        function eat() {
            echo "<p>$this->name đang ăn.</p>";
        }
        // Vật nuôi có thể ngủ
        function sleep() {
            echo "<p>$this->name đang ngủ.</p>";
        }
        // Vật nuôi có chơi
        function play() {
            echo "<p>$this->name đang chơi.</p>";
        }
    } // End of Pet class.

    /* Class Mèo kế thừa class Vật nuôi
     * Mèo có thêm phương thức: climb().
     */
    class Cat extends Pet {
        // Mèo có thể chơi trèo tường
        function play() {
            echo "<p>$this->name đang trèo tường.</p>";
        }
    } // End of Cat class.

    /* Class chó kế thừa class Vật nuôi
     * Chó có thêm phương thức: fetch().
     */
    class Dog extends Pet {
        // Chó có thể chơi lấy đồ vật.
        function play() {
            echo "<p>$this->name đang chơi lấy đồ.</p>";
        }
    } // End of Dog class.

    #------------ KẾT THÚC ĐỊNH NGHĨA CLASS ----------------------#

    // Tạo một con chó tên Jonh
    $dog = new Dog('Jonh');

    // Tạo một con mèo tên Mina
    $cat = new Cat('Mina');

    // Tạo môt vật nuôi chưa biết loài gì tên là Rob
    $pet = new Pet('Rob');

    // Cho các con vật này ăn
    $dog->eat();
    $cat->eat();
    $pet->eat();

    // Cho các con vật này ngủ
    $dog->sleep();
    $cat->sleep();
    $pet->sleep();

    // Cho các con vật này chơi
    $dog->play();
    $cat->play();
    $pet->play();

    // Xóa đối tượng khi không dùng đến
    unset($dog, $cat, $pet);
    ?>
</body>
</html>

Trong ví dụ này, chúng ta có thể thấy các class Dog, Cat và Pet đều có phương thức play() nhưng khi thực hiện nó sẽ cho các kết quả khác nhau do đã ghi đè trong từng class cụ thể.

Ví dụ ghi đè phương thức trong kế thừa

4. Phạm vi truy nhập các thành phần trong class

Có ba mức độ trong phạm vi truy cập thuộc tính và phương thức là public, protected và private. Để thiết lập các phạm vi này, chúng ta đưa 1 trong 3 từ khóa trên vào trước thuộc tính.

class ClassName {
  public $var1 = 'Hello';
  private $var2 = 'world';
  protected $var3 = 42;
}

Thiết lập phạm vi là bắt buộc trong PHP do đó mọi thuộc tính chúng ta dùng trước đây đều có khai báo là public. Tương tự với phương thức, cũng có thể sử dụng các từ khóa này.

class ClassName {
  public function myFunction() {
    // Function code.
  }
}

Phạm vi truy cập thành phần của lớp

Để mô tả rõ hơn phạm vi truy cập các thành phần trong class chúng ta sử dụng hình ảnh trên với phạm vi của public là rộng nhất và phạm vi của private là hẹp nhất:

  • Các thành phần public có thể được truy cập trong bất kỳ class nào cũng như các đoạn code trong PHP.
  • Thành phần protected chỉ có thể truy cập trong lớp đó và các lớp có nguồn gốc từ lớp đó hay được mở rộng từ lớp đó.
  • Thành phần private chỉ có thể truy cập trong bản thân lớp đó.

Cùng quay lại với ví dụ về class Pet, vì thuộc tính $name của class Pet là public do đó, bạn có thể truy xuất đến thuộc tính $name:

$pet = new Pet('Jonh');
$pet->name = 'Nick';

Vì $name là public do đó trong các class Dog, Cat bạn cũng nhìn thấy và thay đổi được.

Các thuộc tính và phương thức với phạm vi truy cập có vẻ kỳ lạ nhưng với OOP thì nó liên quan đến một khái niệm quan trọng, tính bao đóng. Đơn giản hơn, tính bao đóng của OOP sẽ đóng gói và ẩn các thông tin mà các thông tin này không cần thiết phải biết đến ở ngoài class. Một class được sử dụng mà không cần biết nó hoạt động như thế nào bên trong. Ví dụ, một class về database cần một kết nối database được thiết lập trong nó nhưng kết nối này không cần thiết phải được truy nhập từ ngoài lớp. Quay trở lại ví dụ về class Pet, thuộc tính name nên có phạm vi là protected, có nghĩa là class Pet và các class mở rộng từ nó là Dog và Cat có thể truy cập vào $name nhưng không thể thay đổi từ bên ngoài. Với ví dụ class Rectangle cũng vậy, các thuộc tính $width và $height cũng nên có phạm vi là protected.

Chúng ta sẽ cùng nhau thực hiện một ví dụ để hiểu hơn về phạm vi truy cập và cách sử dụng nó trong lập trình hướng đối tượng.

<!doctype html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>Phạm vi truy cập các thành phần của lớp</title>
   <link rel="stylesheet" href="style.css">
</head>
<body>
    <?php 
    #------------ ĐỊNH NGHĨA CLASS ----------------------#
    /* Class Test.
     * Class này chứa 3 thuộc tính có các phạm vi truy cập khác nhau
     * - public $public
     * - protected $protected
     * - private $_private
     * Class có 1 phương thức: printVar().
     */
    class Test {
       public $public = 'public';
       protected $protected = 'protected';
       private $_private = 'private';

       // In giá trị được truyền vào
       function printVar($var) {
          echo "<p>Trong class Test, \$$var: '{$this->$var}'.</p>";
       }
    } // End of Test class.

    /* LittleTest class mở rộng từ Test.
     * LittleTest ghi đè phương thức printVar().
     */
    class ChildTest extends Test {
       function printVar($var) {
          echo "<p>Trong class ChildTest, \$$var: '{$this->$var}'.</p>";
       }
    } // End of ChildTest class.
    #------------ KẾT THÚC ĐỊNH NGHĨA CLASS ----------------------#

    $parent = new Test();
    $child = new ChildTest();
    // Phần 1: In ra màn hình giá trị hiện tại của $public ---------------------------------

    echo '<h1>Public</h1>';
    echo '<h2>Initially...</h2>';
    $parent->printVar('public');
    $child->printVar('public');
    // Thay đổi $public và in lại ra màn hình
    echo '<h2>Thử thay đổi $parent->public...</h2>';
    $parent->public = 'modified';
    $parent->printVar('public');
    $child->printVar('public');

    // Phần 2: In ra màn hình giá trị hiện tại của $protected -----------------------------

    echo '<hr><h1>Protected</h1>';
    echo '<h2>Initially...</h2>';
    $parent->printVar('protected');
    $child->printVar('protected');
    // Thử thay đổi giá trị $protected và in lại ra màn hình
    echo '<h2>Thử thay đổi $parent->protected...</h2>';
    $parent->protected = 'modified';
    $parent->printVar('protected');
    $child->printVar('protected');

    // Phần 3: In ra màn hình giá trị hiện tại của $_private ------------------------------
    echo '<hr><h1>Private</h1>';
    echo '<h2>Initially...</h2>';
    $parent->printVar('_private');
    $child->printVar('_private');
    // Thử thay đổi giá trị $_private và in lại ra màn hình
    echo '<h2>Thử thay đổi $parent->_private...</h2>';
    $parent->_private = 'modified';
    $parent->printVar('_private');
    $child->printVar('_private');

    // Xóa các đối tượng sau khi sử dụng
    unset($parent, $child);
    ?>
</body>
</html>

Để thấy được từng phạm vi hoạt động như thế nào, đầu tiên chúng ta thực hiện comment phần 2 và phần 3, khi đó chạy http://oop.dev/visibility.php chúng ta thấy kết quả như sau:

Phạm vi public

Vì $public có phạm vi truy cập là public do đó nó có thể truy nhập ở bất kỳ đâu: trong class con, trong class cha và ngoài các class này.

Tiếp theo chúng ta comment hai phần 1 và 3, kết quả như sau:

Phạm vi protected

$protected có phạm vi truy cập là protected nghĩa là chỉ truy cập trong class cha và các class con được mở rộng từ class cha, do đó khi truy cập từ bên ngoài class chúng ta thấy báo lỗi ngay:

$parent->protected = 'modified';

Tiếp theo chúng ta comment phần 1 và 2, kết quả như sau:

Phạm vi private

$_private có phạm vi là private nghĩa là nó chỉ có thể truy cập bên trong class đó, như vậy:

$child->printVar('_private');

Sẽ phát sinh lỗi Undefined property do class ChildTest không có thuộc tính $_private do nó không thể kế thừa từ lớp cha hoặc tự bản thân nó không định nghĩa. Tiếp theo đó:

$parent->_private = 'modified';

sẽ phát sinh lỗi không thể truy nhập được $_private do thuộc tính này có phạm vi là private, chỉ được truy nhập trong bản thân class nó.

5. Toán tử phạm vi

OOP có một số các toán tử đặc biệt như -> để truy xuất vào các thành phần của đối tượng, một toán tử đặc biệt khác là :: để truy xuất các thành phần thông qua class chứ không phải là đối tượng.

ClassName::methodName();
ClassName::propertyName;

Có hai chỗ có thể sử dụng toán tử này:

  • Trong class, để tránh nhầm lẫn khi các lớp kế thừa có cùng thuộc tính và phương thức.
  • Ngoài class, để truy xuất các thành phần mà không cần phải tạo ra đối tượng

Bên ngoài class bạn cần sử dụng ClassName, bên trong class sử dụng từ khóa self để tham chiếu đến class hiện tại (giống như $this để tham chiếu đến đối tượng hiện tại).

class SomeClass {
  function _ _construct() {
    self::doThis();
  }
  protected function doThis() {
    echo 'done!';
  }
}

Để tham chiếu đến một thành phần của lớp cha, từ khóa parent được sử dụng:

class SomeOtherClass extends SomeClass{
  function _ _construct() {
    parent::doThis();
  }
}

Chúng ta cùng thực hành các từ khóa và toán tử :: trong ví dụ về class Pet ở đầu bài viết, tạo ra file scope_resolution.php:

<!doctype html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>Scope resolution operator</title>
   <link rel="stylesheet" href="style.css">
</head>
<body>
    <?php 
    #------------ ĐỊNH NGHĨA CLASS ----------------------#
    /* Class Pet.
     * Thuộc tính: name.
     * Phương thức:
     * - __construct()
     * - eat()
     * - sleep()
     */
    class Pet {
        public $name;

        // Hàm khởi tạo thiết lập tên cho vật nuôi
        function __construct($pet_name) {
            $this->name = $pet_name;
            self::sleep();
        }
        // Vật nuôi có thể ăn
        function eat() {
            echo "<p>$this->name đang ăn.</p>";
        }
        // Vật nuôi có thể ngủ
        function sleep() {
            echo "<p>$this->name đang ngủ.</p>";
        }
        // Vật nuôi có chơi
        function play() {
            echo "<p>$this->name đang chơi.</p>";
        }
    } // End of Pet class.

    /* Class Mèo kế thừa class Vật nuôi
     * Mèo có thêm phương thức: climb().
     */
    class Cat extends Pet {
        // Mèo có thể chơi trèo tường
        function play() {
            parent::play();
            echo "<p>$this->name đang trèo tường.</p>";
        }
    } // End of Cat class.

    /* Class chó kế thừa class Vật nuôi
     * Chó có thêm phương thức: fetch().
     */
    class Dog extends Pet {
        // Chó có thể chơi lấy đồ vật.
        function play() {
            parent::play();
            echo "<p>$this->name đang chơi lấy đồ.</p>";
        }
    } // End of Dog class.

    #------------ KẾT THÚC ĐỊNH NGHĨA CLASS ----------------------#

    // Tạo một con chó tên Jonh
    $dog = new Dog('Jonh');

    // Tạo một con mèo tên Mina
    $cat = new Cat('Mina');

    // Tạo môt vật nuôi chưa biết loài gì tên là Rob
    $pet = new Pet('Rob');

    // Cho các con vật này ăn
    $dog->eat();
    $cat->eat();
    $pet->eat();

    // Cho các con vật này ngủ
    $dog->sleep();
    $cat->sleep();
    $pet->sleep();

    // Cho các con vật này chơi
    $dog->play();
    $cat->play();
    $pet->play();

    // Xóa đối tượng khi không dùng đến
    unset($dog, $cat, $pet);
    ?>
</body>
</html>

Thực hiện ví dụ tại đường dẫn http://oop.dev/scope_resolution.php chúng ta được kết quả như sau:

Scope resolution operator

6. Thành phần static

Như chúng ta đã biết một biến static sẽ nhớ giá trị của nó mỗi lần một hàm được gọi, ví dụ:

function test() {  
  static $n = 1;
  echo "$n<br>";
  $n++;
}
test();
test();
test();
/* Kết quả
1
2
3
*/

Khái niệm này cũng được đưa vào khi định nghĩa các thành phần của class, các giá trị này sẽ được nhớ qua mỗi lần một đối tượng của class được tạo ra. Để khai báo một thành phần của lớp là static chúng ta sử dụng từ khóa static:

class SomeClass {
  public static $var = 'value';
}

biến static không thể truy xuất thông qua đối tượng mà bắt buộc phải truy xuất thông qua toán tử :: với tên class hoặc từ khóa seft tùy vào truy xuất bên trong hay ngoài class.

Quay lại ví dụ về Pet, tạo ra file static_member.php với nội dung như sau:

<!doctype html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>Static</title>
   <link rel="stylesheet" href="style.css">
</head>
<body>
    <?php 
    #------------ ĐỊNH NGHĨA CLASS ----------------------#
    /* Class Pet.
     * Thuộc tính: name.
     * Phương thức:
     * - __construct()
     * - eat()
     * - sleep()
     */
    class Pet {
        protected $name;
        private static $_count = 0;

        // Hàm khởi tạo thiết lập tên cho vật nuôi
        function __construct($pet_name) {
            $this->name = $pet_name;
            // Tăng số đối tượng được tạo
            self::$_count++;
        }
        // Hủy và giảm bộ đếm số đối tượng được tạo
        function __destruct() {
            self::$_count--;
        }

        // Phương thức static trả về số đối tượng được tạo
        public static function getCount() {
            return self::$_count;
        }
    } // End of Pet class.

    class Cat extends Pet {
    }
    
    class Dog extends Pet {
    }

    class Ferret extends Pet {
    }

    class PygmyMarmoset extends Pet {
    }
    #------------ KẾT THÚC ĐỊNH NGHĨA CLASS ----------------------#

    $dog = new Dog('Old Yeller');
    echo '<p>After creating a Dog, I now have ' . Pet::getCount() . ' pet(s).</p>';
    
    $cat = new Cat('Bucky');
    echo '<p>After creating a Cat, I now vhave ' . Pet::getCount() . ' pet(s).</p>';

    $ferret = new Ferret('Fungo');
    echo '<p>After creating a Ferret, I now have ' . Pet::getCount() . ' pet(s).</p>';

    unset($dog);
    echo '<p>After tragedy strikes, I now have ' . Pet::getCount() . ' pet(s).</p>';

    $pygmymarmoset = new PygmyMarmoset('Toodles');
    echo '<p>After creating a Pygmy Marmoset, I now have ' . Pet::getCount() . ' pet(s).</p>';

    unset($cat, $ferret, $pygmymarmoset);
    ?>
</body>
</html>

Chạy http://oop.dev/static_member.php chúng ta được kết quả như sau:

static member

Mỗi lần một đối tượng được tạo ra biến static $count sẽ được tăng thêm một và mỗi khi hủy đối tượng, nó lại giảm đi một.

7. Lời kết

Như vậy, chúng ta đã lượt qua các phần trọng tâm nhất của lập trình hướng đối tượng trong PHP. Có thể đúc kết ngắn gọn là các kiến thức về class và kế thừa class là những gì trọng tâm nhất trong cách giải quyết vấn đề theo hướng đối tượng. Loạt bài về OOP sẽ kết thúc với phần 3 là một phần không kém phần lý thú, PHP đã sử dụng một số khái niệm khác nữa để tăng sự đa dạng trong kế thừa như lớp trìu tượng, interface, trait…

Add Comment