Lớp cuối cùng có nghĩa là gì trong PHP?

Hợp đồng rõ ràng, tác dụng phụ riêng biệt, khả năng kiểm tra, độ phức tạp và tải nhận thức thấp, tính linh hoạt của mã và sự tự tin vào bản thân

Động lực

Giảm khả năng hiển thị phạm vi xuống mức tối thiểu

Khi bạn nhìn thấy một tiền tố lớp với cuối cùng, bạn sẽ ngăn một lớp cụ thể được mở rộng bởi bất kỳ lớp nào khác, điều này không chỉ làm cho nó dễ đọc hơn mà còn đảm bảo rằng phạm vi logic mà bạn đang ở bị giới hạn ở lớp cụ thể đó

Khuyến khích tâm lý “sáng tác hơn kế thừa”

Nguyên tắc Đóng-Mở phát biểu. mở để mở rộng nhưng đóng để sửa đổi

Nếu vì bất kỳ lý do gì, một lý do tốt mà bạn hoàn toàn nên biết, bạn quyết định tạo một tài sản thừa kế ở đó, thì chỉ cần bỏ từ khóa cuối cùng và bạn đã sẵn sàng để tiếp tục

Khi bạn “theo mặc định” không thể mở rộng từ một lớp (vì nó là lớp cuối cùng), bạn sẽ tự giúp mình bằng cách nghĩ đến việc sử dụng thành phần thay vì kế thừa

Tại sao lớp học này không phải là cuối cùng?

Nếu chúng ta đặt mục tiêu tổng hợp hơn là kế thừa, thì chúng ta nên cố gắng tránh kế thừa càng nhiều càng tốt và chỉ sử dụng nó khi thực sự cần thiết. Kế thừa thường bị lạm dụng trong OOP

quan niệm sai lầm

Khi chúng tôi dạy OOP lần đầu tiên, chúng tôi thường giới thiệu ví dụ kế thừa cổ điển

Tuy nhiên, khi Alan Kay tạo ra Smalltalk, tính kế thừa chưa bao giờ là khái niệm chính của nó. Khái niệm chính là gửi tin nhắn, nghĩa là bạn có thể gửi tin nhắn đến các đối tượng và chúng đóng gói dữ liệu và logic trong đó, đồng thời bạn có thể thay đổi hành vi của chúng bằng cách sử dụng các đối tượng khác nhau, thực chất là thành phần. Nhưng khái niệm kế thừa quá phổ biến cuối cùng làm lu mờ thành phần

Những lợi ích

  • Hợp đồng rõ ràng. Sử dụng giao diện sẽ buộc bạn phải suy nghĩ về giao tiếp giữa các đối tượng
  • Đơn vị mã độc lập, không có tác dụng phụ. Chỉ chèn các giao diện dưới dạng phụ thuộc sẽ loại bỏ mọi tác dụng phụ khó chịu xung quanh mã bạn đang làm việc
  • khả năng kiểm tra. Mocking phụ thuộc cực kỳ dễ dàng khi chúng là giao diện
  • Độ phức tạp thấp, có thể quản lý được. Vì mọi thứ đều bị cô lập, bạn sẽ không cần phải lo lắng về những thay đổi gợn sóng. Điều này làm giảm đáng kể độ phức tạp của mã của bạn
  • Tải nhận thức thấp. Với sự phức tạp giảm đi, bộ não của bạn sẽ được tự do tập trung vào những gì quan trọng
  • tính lưu động của mã. Bằng cách loại bỏ bất kỳ khớp nối không cần thiết nào, bạn sẽ có thể di chuyển mọi thứ dễ dàng hơn trước đây
  • Tự tin vào bản thân. Việc có thể kiểm tra mã của bạn một cách cô lập tốt như vậy sẽ mang lại cho bạn cảm giác tự tin tuyệt vời khi thay đổi nó

Thành phần trên thừa kế

Nếu bạn cảm thấy cần phải cấu hình lại một đối tượng, thay đổi các phần của thuật toán hoặc viết lại một phần của quá trình triển khai, hãy xem xét việc tạo một lớp mới thay vì ghi đè lên một lớp hiện có. Nếu bạn cần biểu diễn một hệ thống phân cấp của các lớp, trong đó các lớp con là lớp thay thế thích hợp cho lớp cha của chúng. Đây sẽ là tình huống cổ điển mà bạn vẫn có thể xem xét thừa kế. Tuy nhiên, kết quả thậm chí có thể tốt hơn nếu bạn không kế thừa từ các lớp cha cụ thể mà từ các giao diện trừu tượng

Gần đây tôi đã viết về thời điểm thêm giao diện vào một lớp. Sau khi giải thích những lý do chính đáng để thêm giao diện, tôi khẳng định rằng nếu không có lý do nào trong số đó áp dụng cho trường hợp của bạn, thì bạn chỉ nên sử dụng một lớp và tuyên bố nó là "cuối cùng"

PHP 5 giới thiệu từ khóa cuối cùng, từ khóa này ngăn các lớp con ghi đè một phương thức bằng cách thêm tiền tố vào định nghĩa bằng từ khóa cuối cùng. Nếu bản thân lớp đang được định nghĩa cuối cùng thì nó không thể được mở rộng

— Hướng dẫn sử dụng PHP, "Từ khóa cuối cùng"

Một minh họa về cú pháp final để khai báo lớp

final class CanNotBeExtended
{
    // ...
}

class CanStillBeExtended
{
     // ...
}

class Subclass extends CanStillBeExtended
{
    // override methods here
}

// Produces a Fatal error:

class Subclass extends CanNotBeExtended
{
    // ...
}

Trong vài năm nay, tôi đã sử dụng từ khóa final ở mọi nơi (cảm ơn Marco Pivetta đã giúp tôi đi đúng hướng. ). Khi tôi nhìn thấy một lớp không phải là cuối cùng, tôi cảm thấy như đó là một lớp rất dễ bị tổn thương. Nội bộ của nó được đưa ra ngoài trời;

Tuy nhiên, tôi cũng nhớ sự phản đối ban đầu của mình đối với việc thêm final vào mọi định nghĩa về lớp học và tôi thường phải tự bảo vệ mình trong các cuộc hội thảo, vì vậy tôi nghĩ sẽ hữu ích nếu tôi giải thích tất cả về nó ở đây

Thay thế. lớp học không cuối cùng

Bỏ qua từ khóa final là tiêu chuẩn đối với nhiều nhà phát triển, họ cho rằng họ nên cho phép những người khác sử dụng lại lớp và chỉ thay đổi một phần hành vi của nó bằng cách mở rộng nó. Lúc đầu, điều này có vẻ như là một việc rất cân nhắc, nhưng có một số nhược điểm của nó

Bạn là người thiết kế ban đầu của lớp và bạn đã nghĩ đến một trường hợp sử dụng cụ thể khi tạo lớp đó. Sử dụng lớp cho mục đích khác bằng cách ghi đè một phần hành vi của lớp có thể gây nguy hiểm cho tính toàn vẹn của lớp. Trạng thái của đối tượng mở rộng có thể trở nên khó đoán hoặc không phù hợp với trạng thái của đối tượng ban đầu. Hoặc hành vi có thể thay đổi theo cách mà lớp con không còn được coi là sự thay thế thích hợp cho lớp cơ sở

Hơn nữa, nhà phát triển tạo một lớp con của lớp bạn để ghi đè hành vi của nó, có thể không cần tất cả các phần bên trong của lớp cha. Tuy nhiên, nó kế thừa những phần bên trong đó (cấu trúc dữ liệu, phương thức riêng tư) và sẽ sớm đi đến điểm mà nó phải hoạt động xung quanh chúng

Về sự phát triển trong tương lai, có một vấn đề khác. lớp hiện đã được phân lớp, vẫn có một cuộc sống riêng. Khách hàng của nó có thể có những nhu cầu khác nhau theo thời gian, vì vậy những thay đổi sẽ được thực hiện đối với nó. Những thay đổi này cũng có thể ảnh hưởng đến các lớp con, vì chúng có thể dựa vào một chi tiết triển khai cụ thể của lớp cha. Khi chi tiết này thay đổi, phân lớp sẽ bị phá vỡ

Từ tất cả những vấn đề này, chúng tôi phải kết luận rằng bằng cách cho phép phân lớp, chúng tôi sẽ dẫn đến một tình huống không mong muốn. Tương lai của lớp con và lớp cha sẽ trở nên gắn bó với nhau. Tái cấu trúc lớp cha sẽ khá khó khăn, bởi vì ai biết lớp nào đang dựa vào một chi tiết triển khai cụ thể. Và vì lớp con không cần tất cả dữ liệu và hành vi của lớp cha, nên chúng có thể hành động như thể chúng là cùng một thứ, nhưng trên thực tế, chúng không phải vậy.

Thay thế tốt hơn ghi đè

Vì vậy, việc thay đổi hành vi của một lớp không nên được thực hiện bằng cách phân lớp lớp đó và ghi đè các phương thức. Nhưng chúng ta có thể làm gì khác? . Hoặc là do lớp được sử dụng và dựa vào nơi khác trong dự án hoặc về mặt vật lý, nó không phải là một tùy chọn để sửa đổi mã của nó vì nó là một phần của gói nhà cung cấp

Tôi thậm chí muốn nói rằng thay đổi mã không phải là cách ưa thích để thay đổi hành vi ngay từ đầu. Nếu bạn có thể, hãy thay đổi hành vi của một đối tượng bằng cách cấu hình lại nó, nghĩa là bằng cách tráo đổi các đối số của hàm tạo, bạn nên làm điều đó

Điều này nhắc chúng ta về nguyên tắc Đảo ngược phụ thuộc, theo đó chúng ta nên phụ thuộc vào sự trừu tượng và nguyên tắc Mở/đóng, giúp chúng ta thiết kế các đối tượng có thể cấu hình lại mà không cần chạm vào mã của nó

Còn mẫu Phương thức mẫu thì sao?

Có một cách tiếp cận khác để thay đổi hành vi của một lớp. Đó là một mẫu hành vi được mô tả trong "Mẫu thiết kế" cổ điển. Các yếu tố của phần mềm hướng đối tượng tái sử dụng". Mẫu được gọi là "Phương thức mẫu"

Xác định khung của một thuật toán trong một hoạt động, trì hoãn một số bước để phân lớp. Phương thức mẫu cho phép các lớp con xác định lại các bước nhất định của thuật toán mà không thay đổi cấu trúc của thuật toán

Giải pháp này tốt hơn một chút so với việc cho phép các lớp con ghi đè hành vi của lớp cha, vì nó giới hạn những gì có thể thay đổi. Nó làm cho lớp cha trở nên vô dụng bằng cách đánh dấu nó là abstract. Điều này ngăn nó có một cuộc sống của riêng mình. Trong thành phần Bảo mật Symfony, chúng tôi tìm thấy một ví dụ tuyệt vời về mẫu Phương thức Mẫu trong lớp trừu tượng Voter. Nó giúp người dùng triển khai cử tri ủy quyền của riêng họ, bằng cách cung cấp phần lớn thuật toán được cho là giống nhau cho tất cả cử tri. Người dùng chỉ cần triển khai

abstract class Voter implements VoterInterface
{
    public function vote(TokenInterface $token, $subject, array $attributes)
    {
        $vote = self::ACCESS_ABSTAIN;

        foreach ($attributes as $attribute) {
            if (!$this->supports($attribute, $subject)) {
                continue;
            }

            $vote = self::ACCESS_DENIED;
            if ($this->voteOnAttribute($attribute, $subject, $token)) {
                return self::ACCESS_GRANTED;
            }
        }

        return $vote;
    }

    abstract protected function supports($attribute, $subject);

    abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
}
0 và
abstract class Voter implements VoterInterface
{
    public function vote(TokenInterface $token, $subject, array $attributes)
    {
        $vote = self::ACCESS_ABSTAIN;

        foreach ($attributes as $attribute) {
            if (!$this->supports($attribute, $subject)) {
                continue;
            }

            $vote = self::ACCESS_DENIED;
            if ($this->voteOnAttribute($attribute, $subject, $token)) {
                return self::ACCESS_GRANTED;
            }
        }

        return $vote;
    }

    abstract protected function supports($attribute, $subject);

    abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
}
1. đó là cả hai phần nhỏ hơn của thuật toán tổng thể. Symfony sẽ không thể đoán được những triển khai này, vì chúng dành riêng cho dự án

abstract class Voter implements VoterInterface
{
    public function vote(TokenInterface $token, $subject, array $attributes)
    {
        $vote = self::ACCESS_ABSTAIN;

        foreach ($attributes as $attribute) {
            if (!$this->supports($attribute, $subject)) {
                continue;
            }

            $vote = self::ACCESS_DENIED;
            if ($this->voteOnAttribute($attribute, $subject, $token)) {
                return self::ACCESS_GRANTED;
            }
        }

        return $vote;
    }

    abstract protected function supports($attribute, $subject);

    abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
}

Nó thực sự rất hay, tuy nhiên, không có gì ở đây không thể giải quyết bằng thành phần thay vì kế thừa. Một giải pháp tương đương sẽ là

// The user needs to implement this interface, instead of extending from Voter:

interface AttributeVoter
{
    public function supports($attribute, $subject);

    public function voteOnAttribute($attribute, $subject, TokenInterface $token);
}

// The Security package provides this standard Voter implementation:

final class Voter implements VoterInterface
{
    private $attributeVoter;

    public function __construct(AttributeVoter $attributeVoter)
    {
        $this->attributeVoter = $attributeVoter;
    }

    public function vote(TokenInterface $token, $subject, array $attributes)
    {
        $vote = self::ACCESS_ABSTAIN;

        foreach ($attributes as $attribute) {
            // We delegate calls to the injected AttributeVoter

            if (!$this->attributeVoter->supports($attribute, $subject)) {
                continue;
            }

            $vote = self::ACCESS_DENIED;
            if ($this->attributeVoter->voteOnAttribute($attribute, $subject, $token)) {
                return self::ACCESS_GRANTED;
            }
        }

        return $vote;
    }
}

Theo lý do từ "Khi nào nên thêm một giao diện vào một lớp", chúng ta thậm chí có thể xem xét lại sự hiện diện của một

abstract class Voter implements VoterInterface
{
    public function vote(TokenInterface $token, $subject, array $attributes)
    {
        $vote = self::ACCESS_ABSTAIN;

        foreach ($attributes as $attribute) {
            if (!$this->supports($attribute, $subject)) {
                continue;
            }

            $vote = self::ACCESS_DENIED;
            if ($this->voteOnAttribute($attribute, $subject, $token)) {
                return self::ACCESS_GRANTED;
            }
        }

        return $vote;
    }

    abstract protected function supports($attribute, $subject);

    abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
}
2 trong ví dụ trên. Nếu tất cả việc bỏ phiếu được thực hiện theo cùng một cách, nghĩa là thuật toán bỏ phiếu giống nhau trong mọi tình huống, chúng tôi thực sự không muốn người dùng viết triển khai của riêng họ về
abstract class Voter implements VoterInterface
{
    public function vote(TokenInterface $token, $subject, array $attributes)
    {
        $vote = self::ACCESS_ABSTAIN;

        foreach ($attributes as $attribute) {
            if (!$this->supports($attribute, $subject)) {
                continue;
            }

            $vote = self::ACCESS_DENIED;
            if ($this->voteOnAttribute($attribute, $subject, $token)) {
                return self::ACCESS_GRANTED;
            }
        }

        return $vote;
    }

    abstract protected function supports($attribute, $subject);

    abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
}
2. Vì vậy, chúng tôi cũng có thể chỉ cung cấp lớp Voter chứ không phải giao diện (tôi thực sự không chắc đó có phải là trường hợp của thành phần Bảo mật Symfony hay không, nhưng đó là một lựa chọn thiết kế tốt để xem xét trong các dự án của riêng bạn)

Giải pháp đề xuất không liên quan đến thừa kế nữa. Nó để lại phần bên trong của tất cả các lớp liên quan cho chính họ. Nghĩa là, tất cả các lớp này đều có thể là final và có thể được tái cấu trúc một cách an toàn ngay bây giờ. Một ưu điểm khác là chúng ta có thể sử dụng lại từng lớp trong một kịch bản khác nhau. Các lớp cuối cùng giống như các khối xây dựng đã sẵn sàng để sử dụng, thay vì các mẫu vẫn cần được làm cụ thể

Thành phần trên thừa kế

Bạn có thể đã nghe cụm từ "Thành phần hơn thừa kế" trước đây; . Trong hầu hết các trường hợp, bạn nên ưu tiên giải pháp liên quan đến thành phần hơn là giải pháp liên quan đến kế thừa. Cụ thể hơn, nếu bạn cảm thấy cần phải cấu hình lại một đối tượng, thay đổi các phần của thuật toán, viết lại một phần của quá trình triển khai, hãy xem xét việc tạo một lớp mới thay vì ghi đè lên một lớp hiện có. Nếu bạn cần biểu diễn một hệ thống phân cấp của các lớp, trong đó các lớp con là lớp thay thế thích hợp cho lớp cha của chúng, thì đây sẽ là tình huống kinh điển mà bạn vẫn có thể xem xét tính kế thừa. Tuy nhiên, kết quả vẫn có thể tốt hơn nếu bạn không kế thừa từ các lớp cha cụ thể mà từ các giao diện trừu tượng. Tuy nhiên, điều này rất khó để hiểu đúng (cá nhân tôi rất tiếc về phần lớn công việc trước đây của tôi liên quan đến thừa kế), vì vậy nếu có thể, bạn vẫn nên bố cục hơn

Tiện ích mở rộng phải là một trường hợp sử dụng riêng biệt

Với tư cách là người thiết kế lớp, bạn tạo lớp theo cách sao cho nó cung cấp một số điều hữu ích mà người dùng có thể thực hiện với lớp đó. Ví dụ, họ có thể

  • khởi tạo nó,
  • gọi một phương thức trên đó,
  • nhận được một số thông tin từ nó,
  • hoặc thay đổi một cái gì đó về nó

"Cấu hình lại nó để thay đổi một phần hành vi của nó" cũng nên có trong danh sách này, dưới dạng một mục riêng biệt. Và do đó, nên "cho phép nó được mở rộng để ghi đè lên một phần hành vi của nó". Đó phải là một quyết định có chủ ý để cho phép người dùng làm điều đó với lớp của bạn. Việc cho phép một lớp được mở rộng có (thường hạn chế) hậu quả đối với thiết kế của nó và tương lai của nó. Vì vậy, ít nhất bạn không nên cho phép nó theo mặc định

"Cuối cùng" đẩy mọi người đi đúng hướng

Vì vậy, nếu cho phép người dùng phân lớp một lớp không phải là tiêu chuẩn, thì việc không cho phép nó phải là. Nói cách khác. thêm final vào khai báo lớp nên là mặc định của bạn. Thủ thuật đơn giản này sẽ dẫn mọi người đi đúng hướng. hướng tới các lớp nhỏ hơn, có ít vấn đề bảo trì hơn, dễ tái cấu trúc hơn và hoạt động giống như các khối xây dựng có thể được sử dụng lại trong các phần khác nhau của dự án

"Bạn luôn có thể xóa bản cuối cùng nếu muốn"

Tôi thường nghe một lập luận về việc sử dụng final mà tôi muốn giải quyết ở đây. "Bạn luôn có thể loại bỏ cuối cùng nếu bạn muốn. " Theo một nghĩa nào đó, điều này đúng; bạn luôn có thể cho phép mở rộng một lớp bất cứ khi nào bạn muốn. Nhưng điều này dường như cũng đúng với việc thêm "cuối cùng" - luôn luôn chỉ cần một vài lần nhấn phím. Như đã thảo luận trong bài viết này, tôi không nghĩ đó là thái độ đúng đắn; . Mở ra sau khi bị đóng là hỏi hết rắc rối thừa kế đã trình bày ở trên. Đảm bảo rằng thay vì loại bỏ "cuối cùng" khỏi khai báo lớp, bạn sẽ luôn nhắm đến một giải pháp thay thế một phần của giải pháp hiện có và sử dụng thành phần để cho phép cấu hình lại

"Công cụ mô phỏng của tôi không hoạt động với các lớp cuối cùng"

Một phản đối đối với các lớp học cuối cùng mà tôi thường nghe là. "Tôi thích những gì bạn nói, nhưng công cụ chế giễu của tôi không hoạt động với các lớp cuối cùng". Thật vậy, hầu hết không. Nó có ý nghĩa, bởi vì thử nghiệm nhân đôi mà một công cụ như vậy tạo ra thường trông giống như thế này

final class CanNotBeExtended
{
}

// This produces a Fatal error:

class TestDoubleForCanNotBeExtended32789473246324369823903 extends CanNotBeExtended
{
    // override implementations of parent class here...
}

Thật không may, nhưng các công cụ không thể đổ lỗi. Họ sẽ phải thực hiện một số việc lén lút như móc vào trình tải lớp để biến lớp không phải là cuối cùng (thực tế, điều đó hoàn toàn có thể thực hiện được). Nhưng họ thì không, vì vậy sự thật bị tát vào mặt chúng ta. Tất cả những gì công cụ nói là không thể mở rộng lớp cuối cùng - không có vấn đề gì về lỗi nghiêm trọng đó. Thay vào đó, bất cứ khi nào chúng tôi cảm thấy cần phải thay thế một lớp cuối cùng bằng một bài kiểm tra kép, chúng tôi nên xem xét hai tùy chọn

  1. Có lẽ chúng ta không nên muốn thay thế đồ thật, tôi. e. lớp
  2. Có lẽ chúng ta nên giới thiệu một cái gì đó chúng ta có thể thay thế, tôi. e. một giao diện

Tùy chọn 1 thường hữu ích khi bạn đang cố chế nhạo những thứ như thực thể và đối tượng giá trị. Chỉ cần không làm điều đó

Phương án 2 nên được áp dụng trong hầu hết các trường hợp khác. Khi lớp lẽ ra phải có một giao diện, hãy thêm một giao diện và thay vào đó hãy tạo một thử nghiệm kép cho giao diện

Phần kết luận

Tóm lại là. làm cho các lớp học của bạn final theo mặc định. Một mẹo nhỏ giúp bạn đó là sửa đổi mẫu IDE cho các lớp mới để tự động thêm final cho bạn. Ngoài ra, hãy biến phần thảo luận "cuối cùng" trong phần đánh giá mã kỹ thuật của bạn. Hỏi. tại sao lớp học này không phải là final?

lớp học cuối cùng có nghĩa là gì?

Lớp cuối cùng là lớp được khai báo với từ khóa final . Lớp con không thể kế thừa lớp cuối cùng hoặc lớp cuối cùng không thể được kế thừa bởi bất kỳ lớp con nào. Vì vậy, chúng ta có thể hạn chế kế thừa lớp bằng cách sử dụng lớp cuối cùng.

Sự khác biệt giữa lớp học cuối cùng và lớp học thông thường là gì?

Mục đích chính của việc sử dụng lớp cuối cùng là để ngăn không cho lớp đó được kế thừa (i. e. ) nếu một lớp được đánh dấu là cuối cùng, thì không lớp nào khác có thể kế thừa bất kỳ thuộc tính hoặc phương thức nào từ lớp cuối cùng . Nếu lớp cuối cùng được mở rộng, Java sẽ báo lỗi thời gian biên dịch.

Làm cách nào để kế thừa lớp cuối cùng trong PHP?

Từ khóa cuối cùng ¶ . Nếu bản thân lớp đang được định nghĩa cuối cùng thì nó không thể được mở rộng. prefixing the definition with final . If the class itself is being defined final then it cannot be extended.

Tĩnh và cuối cùng trong PHP là gì?

cuối cùng static khai báo một phương thức tĩnh (có thể được gọi mà không cần phiên bản của lớp) và cuối cùng (không thể bị lớp con ghi đè). static alone can be used to define a class-scoped variable, which isn't constant (but variables can't be final ).