Việc tạo các lớp con cho các trường hợp cụ thể là một thực tiễn xấu?


37

Hãy xem xét các thiết kế sau đây

public class Person
{
    public virtual string Name { get; }

    public Person (string name)
    {
        this.Name = name;
    }
}

public class Karl : Person
{
    public override string Name
    {
        get
        {
            return "Karl";
        }
    }
}

public class John : Person
{
    public override string Name
    {
        get
        {
            return "John";
        }
    }
}

Bạn có nghĩ rằng có điều gì đó sai ở đây? Đối với tôi, các lớp Karl và John chỉ là các thể hiện thay vì các lớp vì chúng giống hệt như:

Person karl = new Person("Karl");
Person john = new Person("John");

Tại sao tôi sẽ tạo các lớp mới khi các thể hiện là đủ? Các lớp không thêm bất cứ điều gì vào thể hiện.


17
Thật không may, bạn sẽ tìm thấy điều này trong rất nhiều mã sản xuất. Làm sạch nó khi bạn có thể.
Adam Zuckerman

14
Đây là một thiết kế tuyệt vời - nếu nhà phát triển muốn biến mình thành không thể thiếu cho mọi thay đổi trong dữ liệu kinh doanh và không có vấn đề gì khả dụng 24/7 ;-)
Doc Brown

1
Đây là một loại câu hỏi liên quan trong đó OP dường như tin vào điều ngược lại với sự khôn ngoan thông thường. programmers.stackexchange.com/questions/253612/...
Dunk

11
Thiết kế các lớp học của bạn tùy thuộc vào hành vi không dữ liệu. Đối với tôi, các lớp con không thêm hành vi mới vào hệ thống phân cấp.
Songo

2
Điều này được sử dụng rộng rãi với các lớp con ẩn danh (như trình xử lý sự kiện) trong Java trước khi lambdas đến với Java 8 (điều tương tự với các cú pháp khác nhau).
marczellm

Câu trả lời:


77

Không cần phải có các lớp con cụ thể cho mỗi người.

Bạn nói đúng, đó là những trường hợp thay thế.

Mục tiêu của các lớp con: để mở rộng các lớp cha

Các lớp con được sử dụng để mở rộng chức năng được cung cấp bởi lớp cha. Ví dụ: bạn có thể có:

  • Một lớp cha Batterycó thể có Power()một cái gì đó và có thể có một Voltagetài sản,

  • Và một lớp con RechargeableBattery, có thể kế thừa Power()Voltage, nhưng cũng có thể là Recharge()d.

Lưu ý rằng bạn có thể truyền một thể hiện của RechargeableBatterylớp làm tham số cho bất kỳ phương thức nào chấp nhận một Batteryđối số. Đây được gọi là nguyên tắc thay thế Liskov , một trong năm nguyên tắc RẮN. Tương tự, trong cuộc sống thực, nếu máy nghe nhạc MP3 của tôi chấp nhận hai pin AA, tôi có thể thay thế chúng bằng hai pin AA có thể sạc lại.

Lưu ý rằng đôi khi rất khó để xác định xem bạn cần sử dụng một trường hoặc một lớp con để thể hiện sự khác biệt giữa một cái gì đó. Ví dụ: nếu bạn phải xử lý pin AA, AAA và 9-Volt, bạn sẽ tạo ba lớp con hoặc sử dụng enum? Thay thế lớp con bằng các trường học trong việc tái cấu trúc bởi Martin Fowler, trang 232, có thể cung cấp cho bạn một số ý tưởng và cách di chuyển từ cái này sang cái khác.

Trong ví dụ của bạn, KarlJohnkhông mở rộng bất cứ điều gì, chúng cũng không cung cấp bất kỳ giá trị bổ sung nào: bạn có thể có cùng chức năng chính xác bằng cách sử dụng Personlớp trực tiếp. Có nhiều dòng mã không có giá trị bổ sung là không bao giờ tốt.

Một ví dụ về trường hợp kinh doanh

Điều gì có thể có thể là một trường hợp kinh doanh trong đó nó thực sự có ý nghĩa để tạo ra một lớp con cho một người cụ thể?

Giả sử chúng ta xây dựng một ứng dụng quản lý những người làm việc trong một công ty. Ứng dụng cũng quản lý các quyền, vì vậy Helen, kế toán viên, không thể truy cập kho lưu trữ SVN, nhưng Thomas và Mary, hai lập trình viên, không thể truy cập các tài liệu liên quan đến kế toán.

Jimmy, ông chủ lớn (người sáng lập và CEO của công ty) có những đặc quyền rất cụ thể không ai có. Anh ta có thể, ví dụ, tắt toàn bộ hệ thống, hoặc sa thải một người. Bạn có được ý tưởng.

Mô hình nghèo nhất cho ứng dụng này là có các lớp như:

          Biểu đồ lớp cho thấy các lớp Helen, Thomas, Mary và Jimmy và cha mẹ chung của chúng: lớp Người trừu tượng.

bởi vì sao chép mã sẽ phát sinh rất nhanh. Ngay cả trong ví dụ rất cơ bản của bốn nhân viên, bạn sẽ sao chép mã giữa các lớp Thomas và Mary. Điều này sẽ thúc đẩy bạn tạo ra một lớp cha mẹ phổ biến Programmer. Vì bạn cũng có thể có nhiều kế toán viên, nên có lẽ bạn cũng sẽ tạo Accountantlớp.

          Biểu đồ lớp cho thấy một Người trừu tượng có ba đứa con: Kế toán trừu tượng, Lập trình viên trừu tượng và Jimmy.  Kế toán có một lớp con Helen và Lập trình viên có hai lớp con: Thomas và Mary.

Bây giờ, bạn nhận thấy rằng việc có lớp học Helenkhông phải là rất hữu ích, cũng như giữ ThomasMary: hầu hết các mã của bạn hoạt động ở cấp cao hơn dù sao ở cấp độ của kế toán viên, lập trình viên và Jimmy. Máy chủ SVN không quan tâm nếu đó là Thomas hay Mary, người cần truy cập vào nhật ký, nó chỉ cần biết đó là lập trình viên hay kế toán viên.

if (person is Programmer)
{
    this.AccessGranted = true;
}

Vì vậy, cuối cùng bạn loại bỏ các lớp bạn không sử dụng:

          Biểu đồ lớp chứa các lớp con Kế toán, Lập trình viên và Jimmy với lớp cha chung của nó: Người trừu tượng.

Bạn nghĩ rằng tôi có thể giữ nguyên trạng của Jimmy, vì sẽ luôn chỉ có một CEO, một ông chủ lớn, ông Jimmy Jimmy, bạn nghĩ vậy. Hơn nữa, Jimmy được sử dụng rất nhiều trong mã của bạn, mà thực sự trông giống như thế này, và không giống như trong ví dụ trước:

if (person is Jimmy)
{
    this.GiveUnrestrictedAccess(); // Because Jimmy should do whatever he wants.
}
else if (person is Programmer)
{
    this.AccessGranted = true;
}

Vấn đề với cách tiếp cận đó là Jimmy vẫn có thể bị xe buýt đâm, và sẽ có một CEO mới. Hoặc ban giám đốc có thể quyết định rằng Mary tuyệt vời đến mức cô ấy nên trở thành một CEO mới, và Jimmy sẽ bị giáng chức vào vị trí nhân viên bán hàng, vì vậy bây giờ, bạn cần phải duyệt qua tất cả mã của mình và thay đổi mọi thứ.


6
@DocBrown: Tôi thường ngạc nhiên khi thấy những câu trả lời ngắn không sai nhưng không đủ sâu có điểm cao hơn nhiều so với câu trả lời giải thích sâu về chủ đề. Có lẽ có quá nhiều người không thích đọc nhiều, vì vậy những câu trả lời rất ngắn trông hấp dẫn hơn đối với họ. Tuy nhiên, tôi đã chỉnh sửa câu trả lời của mình để cung cấp ít nhất một số lời giải thích.
Arseni Mourzenko

3
Câu trả lời ngắn có xu hướng được đăng đầu tiên. Bài viết trước nhận được nhiều phiếu bầu hơn.
Tim B

1
@TimB: Tôi thường bắt đầu với các câu trả lời cỡ trung bình, và sau đó chỉnh sửa chúng thật hoàn chỉnh (đây là một ví dụ , cho rằng có lẽ có khoảng mười lăm phiên bản, chỉ có bốn bản được hiển thị). Tôi nhận thấy rằng càng trở thành câu trả lời theo thời gian, nó càng nhận được ít hơn; một câu hỏi tương tự vẫn còn thu hút nhiều người ủng hộ hơn.
Arseni Mourzenko

Đồng ý với @DocBrown. Ví dụ: một câu trả lời hay sẽ nói một cái gì đó như "lớp con cho hành vi , không phải cho nội dung" và tham khảo một số tài liệu tham khảo mà tôi sẽ không làm trong bình luận này.
dùng949300

2
Trong trường hợp không rõ ràng từ đoạn cuối: Ngay cả khi bạn chỉ có 1 người với quyền truy cập không hạn chế, bạn vẫn nên biến người đó thành một thể hiện của một lớp riêng biệt thực hiện quyền truy cập không bị hạn chế. Việc kiểm tra chính người đó dẫn đến tương tác dữ liệu mã và có thể mở ra toàn bộ hộp giun. Ví dụ, điều gì sẽ xảy ra nếu Hội đồng quản trị quyết định bổ nhiệm một người chiến thắng làm CEO? Nếu bạn không có lớp "Không giới hạn", bạn sẽ không phải cập nhật CEO hiện tại mà còn thêm 2 kiểm tra nữa cho các thành viên khác. Nếu bạn đã có nó, bạn chỉ cần đặt chúng là "Không giới hạn".
Nzall

15

Thật ngớ ngẩn khi sử dụng loại cấu trúc lớp này chỉ để thay đổi các chi tiết rõ ràng là các trường có thể giải quyết trên một thể hiện. Nhưng đó là đặc biệt cho ví dụ của bạn.

Tạo các Personlớp khác nhau gần như chắc chắn là một ý tưởng tồi - bạn phải thay đổi chương trình của mình và biên dịch lại mỗi khi có Người mới vào miền của bạn. Tuy nhiên, điều đó không có nghĩa là kế thừa từ một lớp hiện có để một chuỗi khác được trả về ở đâu đó đôi khi không hữu ích.

Sự khác biệt là cách bạn mong đợi các thực thể được đại diện bởi lớp đó thay đổi. Mọi người hầu như luôn luôn được cho là đến và đi, và hầu như mọi ứng dụng nghiêm túc liên quan đến người dùng dự kiến ​​sẽ có thể thêm và xóa người dùng trong thời gian chạy. Nhưng nếu bạn mô hình hóa những thứ như các thuật toán mã hóa khác nhau, thì việc hỗ trợ một thuật toán mới có lẽ là một thay đổi lớn và sẽ hợp lý khi phát minh ra một lớp mới có myName()phương thức trả về một chuỗi khác (và perform()phương thức của nó có thể làm điều gì đó khác).


12

Tại sao tôi sẽ tạo các lớp mới khi các thể hiện là đủ?

Trong hầu hết các trường hợp, bạn sẽ không. Ví dụ của bạn thực sự là một trường hợp tốt trong đó hành vi này không thêm giá trị thực.

Nó cũng vi phạm nguyên tắc Đóng mở , vì các lớp con về cơ bản không phải là một phần mở rộng mà sửa đổi hoạt động bên trong của cha mẹ. Hơn nữa, hàm tạo công khai của cha mẹ hiện đang gây khó chịu trong các lớp con và do đó API trở nên ít dễ hiểu hơn.

Tuy nhiên, đôi khi, nếu bạn chỉ có một hoặc hai cấu hình đặc biệt thường được sử dụng trong toàn bộ mã, đôi khi sẽ thuận tiện hơn và tốn ít thời gian hơn khi chỉ phân lớp cha mẹ có hàm tạo phức tạp. Trong những trường hợp đặc biệt như vậy, tôi có thể thấy không có gì sai với cách tiếp cận như vậy. Hãy gọi nó là currying j / k


Tôi không chắc chắn tôi đồng ý với đoạn OCP. Nếu một lớp đang phơi bày một trình thiết lập thuộc tính cho con cháu của nó thì - giả sử nó tuân theo các nguyên tắc thiết kế tốt - thì thuộc tính đó không phải là một phần của hoạt động bên trong của nó
Ben Aaronson

@BenAaronson Miễn là hành vi của tài sản không bị thay đổi, bạn không vi phạm OCP, nhưng ở đây họ làm. Tất nhiên bạn có thể sử dụng Tài sản đó trong hoạt động bên trong của nó. Nhưng bạn không nên thay đổi hành vi hiện có! Bạn chỉ nên mở rộng hành vi, không thay đổi nó bằng sự kế thừa. Tất nhiên, có những ngoại lệ cho mọi quy tắc.
Falcon

1
Vì vậy, bất kỳ việc sử dụng thành viên ảo là vi phạm OCP?
Ben Aaronson

3
@Falcon Không cần phải lấy một lớp mới chỉ để tránh các lệnh gọi hàm tạo phức tạp lặp đi lặp lại. Chỉ cần tạo các nhà máy cho các trường hợp phổ biến và tham số hóa các biến thể nhỏ.
Keen

@BenAaronson Tôi muốn nói rằng việc có các thành viên ảo có triển khai thực tế là vi phạm như vậy. Các thành viên trừu tượng, hoặc nói các phương thức không làm gì, sẽ là các điểm mở rộng hợp lệ. Về cơ bản, khi bạn nhìn vào mã của một lớp cơ sở, bạn sẽ biết rằng nó sẽ chạy như hiện tại, thay vì mọi phương thức không cuối cùng là một ứng cử viên để được thay thế bằng một thứ hoàn toàn khác. Hoặc nói cách khác: các cách mà một lớp kế thừa có thể ảnh hưởng đến hành vi của lớp cơ sở sẽ rõ ràng và bị hạn chế là "phụ gia".
millimoose

8

Nếu đây là phạm vi của thực tiễn, thì tôi đồng ý đây là một thực hành xấu.

Nếu John và Karl có những hành vi khác nhau, thì mọi thứ sẽ thay đổi một chút. Có thể personcó một phương pháp cho cleanRoom(Room room)thấy John là bạn cùng phòng tuyệt vời và dọn dẹp rất hiệu quả, nhưng Karl thì không và không dọn dẹp phòng rất nhiều.

Trong trường hợp này, sẽ hợp lý nếu chúng là lớp con của riêng chúng với hành vi được xác định. Có nhiều cách tốt hơn để đạt được điều tương tự (ví dụ: một CleaningBehaviorlớp), nhưng ít nhất cách này không vi phạm các nguyên tắc OO khủng khiếp.


Không có hành vi khác nhau, chỉ là dữ liệu khác nhau. Cảm ơn.
Ignacio Soler Garcia

6

Trong một số trường hợp, bạn biết sẽ chỉ có một số trường hợp được thiết lập và sau đó nó sẽ ổn (mặc dù theo tôi là xấu xí) khi sử dụng phương pháp này. Nó là tốt hơn để sử dụng một enum nếu ngôn ngữ cho phép nó.

Ví dụ về Java:

public enum Person
{
    KARL("Karl"),
    JOHN("John")

    private final String name;

    private Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }
}

6

Rất nhiều người đã trả lời rồi. Nghĩ rằng tôi sẽ đưa ra quan điểm cá nhân của riêng tôi.


Đã có lần tôi làm việc trên một ứng dụng (và vẫn làm) tạo ra âm nhạc.

Ứng dụng có một bản tóm tắt Scalelớp với nhiều lớp con: CMajor, DMinorvv Scalenhìn một cái gì đó giống như vậy:

public abstract class Scale {
    protected Note[] notes;
    public Scale() {
        loadNotes();
    }
    // .. some other stuff ommited
    protected abstract void loadNotes(); /* subclasses put notes in the array
                                          in this method. */
}

Các trình tạo nhạc đã làm việc với một thể hiện cụ Scalethể để tạo nhạc. Người dùng sẽ chọn một tỷ lệ từ một danh sách, để tạo nhạc từ đó.

Một ngày nọ, một ý tưởng tuyệt vời xuất hiện trong đầu tôi: tại sao không cho phép người dùng tạo quy mô của riêng mình? Người dùng sẽ chọn ghi chú từ danh sách, nhấn nút và thang đo mới sẽ được thêm vào danh sách thang đo có sẵn.

Nhưng tôi đã không thể làm điều này. Đó là bởi vì tất cả các thang đo đã được đặt ở thời gian biên dịch - vì chúng được biểu thị dưới dạng các lớp. Sau đó, nó đánh tôi:

Nó thường trực quan để suy nghĩ theo nghĩa 'siêu lớp và lớp con'. Hầu hết mọi thứ có thể được thể hiện thông qua hệ thống này: siêu lớp Personvà các lớp con JohnMary; siêu lớp Carvà các lớp con VolvoMazda; siêu lớp Missilevà các lớp con SpeedRocked, LandMineTrippleExplodingThingy.

Nghĩ theo cách này là điều rất tự nhiên, đặc biệt đối với người tương đối mới với OO.

Nhưng chúng ta nên luôn nhớ rằng các lớp là các mẫucác đối tượng là nội dung được đổ vào các mẫu này . Bạn có thể đổ bất cứ nội dung nào bạn muốn vào mẫu, tạo ra vô số khả năng.

Đây không phải là công việc của lớp con để điền vào mẫu. Đó là công việc của đối tượng. Công việc của lớp con là thêm chức năng thực tế hoặc mở rộng mẫu .

Và đó là lý do tại sao tôi nên tạo một Scalelớp cụ thể , với một Note[]trường và để các đối tượng điền vào mẫu này ; có thể thông qua các nhà xây dựng hoặc một cái gì đó. Và cuối cùng, vì vậy tôi đã làm.

Mỗi khi bạn thiết kế một mẫu trong một lớp (ví dụ: một Note[]thành viên trống cần được điền hoặc một String nametrường cần được gán một giá trị), hãy nhớ rằng đó là công việc của các đối tượng của lớp này để điền vào mẫu ( hoặc có thể những người tạo ra các đối tượng này). Các lớp con có nghĩa là để thêm chức năng, không phải để điền vào các mẫu.


Bạn có thể bị cám dỗ để tạo ra một loại "siêu lớp Person, lớp con JohnMary" hệ thống, giống như bạn đã làm, bởi vì bạn thích hình thức này mang lại cho bạn.

Bằng cách này, bạn chỉ có thể nói Person p = new Mary(), thay vì Person p = new Person("Mary", 57, Sex.FEMALE). Nó làm cho mọi thứ có tổ chức hơn, và có cấu trúc hơn. Nhưng như chúng tôi đã nói, việc tạo một lớp mới cho mọi kết hợp dữ liệu không phải là cách tiếp cận tốt, vì nó làm mờ mã không có gì và giới hạn bạn về khả năng thời gian chạy.

Vì vậy, đây là một giải pháp: sử dụng một nhà máy cơ bản, thậm chí có thể là một nhà máy tĩnh. Thích như vậy:

public final class PersonFactory {
    private PersonFactory() { }

    public static Person createJohn(){
        return new Person("John", 40, Sex.MALE);
    }

    public static Person createMary(){
        return new Person("Mary", 57, Sex.FEMALE);
    }

    // ...
}

Bằng cách này, bạn có thể dễ dàng sử dụng 'cài đặt sẵn' 'đi kèm với chương trình', như vậy: Person mary = PersonFactory.createMary()nhưng bạn cũng có quyền thiết kế người mới một cách linh hoạt, ví dụ trong trường hợp bạn muốn cho phép người dùng làm như vậy . Ví dụ:

// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);

Hoặc thậm chí tốt hơn: làm một cái gì đó như vậy:

public final class PersonFactory {
    private PersonFactory() { }

    private static Map<String, Person> persons = new HashMap<>();
    private static Map<String, PersonData> personBlueprints = new HashMap<>();

    public static void addPerson(Person person){
        persons.put(person.getName(), person);
    }

    public static Person getPerson(String name){
        return persons.get(name);
    }

    public static Person createPerson(String blueprintName){
        PersonData data = personBlueprints.get(blueprintName);
        return new Person(data.name, data.age, data.sex);
    }

    // .. or, alternative to the last method
    public static Person createPerson(String personName){
        Person blueprint = persons.get(personName);
        return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
    }

}

public class PersonData {
    public String name;
    public int age;
    public Sex sex;

    public PersonData(String name, int age, Sex sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

Tôi đã mang đi. Tôi nghĩ rằng bạn có được ý tưởng.


Các lớp con không có nghĩa là điền vào các mẫu được đặt bởi các siêu lớp của chúng. Các lớp con có nghĩa là để thêm chức năng . Đối tượng có nghĩa là điền vào các mẫu, đó là những gì họ đang làm.

Bạn không nên tạo một lớp mới cho mọi kết hợp dữ liệu có thể. (Giống như tôi không nên tạo một Scalelớp con mới cho mọi kết hợp có thể có của Notes).

Đây là một hướng dẫn: bất cứ khi nào bạn tạo một lớp con mới, hãy xem xét nếu nó thêm bất kỳ chức năng mới nào không tồn tại trong siêu lớp. Nếu câu trả lời cho câu hỏi đó là "không", thì bạn có thể đang cố gắng 'điền vào mẫu' của siêu lớp, trong trường hợp đó chỉ cần tạo một đối tượng. (Và có thể là một Nhà máy với 'cài đặt trước', để làm cho cuộc sống dễ dàng hơn).

Mong rằng sẽ giúp.


Đây là một ví dụ tuyệt vời, cảm ơn bạn rất nhiều.
Ignacio Soler Garcia

@SoMoS Vui mừng tôi có thể giúp.
Manila Cohn

2

Đây là một ngoại lệ đối với 'nên là các trường hợp' ở đây - mặc dù hơi cực.

Nếu bạn muốn trình biên dịch thực thi các quyền đối với các phương thức truy cập dựa trên việc đó là Karl hay John (trong ví dụ này) thay vì yêu cầu triển khai phương thức để kiểm tra xem trường hợp nào đã được thông qua thì bạn có thể sử dụng các lớp khác nhau. Bằng cách thực thi các lớp khác nhau thay vì khởi tạo, bạn có thể thực hiện kiểm tra phân biệt giữa 'Karl' và 'John' trong thời gian biên dịch thay vì thời gian chạy và điều này thể hữu ích - đặc biệt là trong bối cảnh bảo mật nơi trình biên dịch có thể đáng tin cậy hơn thời gian chạy kiểm tra mã của (ví dụ) thư viện bên ngoài.


2

Nó được coi là một thực hành xấu để có mã trùng lặp .

Điều này ít nhất là một phần vì các dòng mã và do đó kích thước của codebase tương quan với chi phí bảo trì và lỗi .

Đây là logic thông thường có liên quan với tôi về chủ đề cụ thể này:

1) Nếu tôi cần thay đổi điều này, tôi cần đến bao nhiêu nơi? Ít hơn là tốt hơn ở đây.

2) Có một lý do tại sao nó được thực hiện theo cách này? Trong ví dụ của bạn - không thể có - nhưng trong một số ví dụ có thể có một số lý do để làm điều này. Người ta có thể nghĩ ra khỏi đỉnh đầu của tôi là trong một số khuôn khổ như Grails thời kỳ đầu, nơi mà sự thừa kế không nhất thiết phải chơi độc đáo với một số bổ trợ cho GORM. Ít và xa giữa.

3) Có sạch hơn và dễ hiểu hơn theo cách này không? Một lần nữa, nó không nằm trong ví dụ của bạn - nhưng có một số mẫu thừa kế có thể kéo dài hơn trong đó các lớp tách biệt thực sự dễ duy trì hơn (các cách sử dụng nhất định của các mẫu lệnh hoặc chiến lược xuất hiện trong tâm trí).


1
cho dù nó xấu hay không thì cũng không được đặt trong đá. Ví dụ, có các kịch bản trong đó chi phí phụ của việc tạo đối tượng và các cuộc gọi phương thức có thể gây bất lợi cho hiệu năng đến mức ứng dụng không còn đáp ứng các đặc tả.
jwenting

Xem điểm # 2. Chắc chắn có lý do tại sao, đôi khi. Đó là lý do tại sao bạn cần phải hỏi trước khi đưa ra mã của ai đó.
dcgregorya

0

Có một trường hợp sử dụng: hình thành một tham chiếu đến một hàm hoặc phương thức như một đối tượng thông thường.

Bạn có thể thấy điều này trong mã GUI Java rất nhiều:

ActionListener a = new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
        /* ... */
    }
};

Trình biên dịch giúp bạn ở đây bằng cách tạo một lớp con ẩn danh mới.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.