Tại sao thích các lớp bên trong không tĩnh hơn các lớp tĩnh?


37

Câu hỏi này là về việc tạo một lớp lồng trong Java thành một lớp lồng tĩnh hay một lớp lồng bên trong. Tôi đã tìm kiếm xung quanh đây và trên Stack Overflow, nhưng thực sự không thể tìm thấy bất kỳ câu hỏi nào liên quan đến ý nghĩa thiết kế của quyết định này.

Các câu hỏi tôi tìm thấy đang hỏi về sự khác biệt giữa các lớp lồng nhau tĩnh và bên trong, rõ ràng đối với tôi. Tuy nhiên, tôi vẫn chưa tìm thấy một lý do thuyết phục để sử dụng một lớp lồng tĩnh trong Java - ngoại trừ các lớp ẩn danh, mà tôi không xem xét cho câu hỏi này.

Dưới đây là sự hiểu biết của tôi về tác dụng của việc sử dụng các lớp lồng nhau tĩnh:

  • Ít khớp nối hơn: Chúng ta thường nhận được ít khớp nối hơn, vì lớp không thể truy cập trực tiếp vào các thuộc tính của lớp bên ngoài. Ít khớp nối thường có nghĩa là chất lượng mã tốt hơn, kiểm tra dễ dàng hơn, tái cấu trúc, v.v.
  • Độc thân Class: Trình nạp lớp không cần phải chăm sóc một lớp mới mỗi lần, chúng ta tạo một đối tượng của lớp bên ngoài. Chúng tôi chỉ nhận được các đối tượng mới cho cùng một lớp hơn và hơn.

Đối với một lớp bên trong, tôi thường thấy rằng mọi người coi quyền truy cập vào các thuộc tính của lớp bên ngoài là chuyên nghiệp. Tôi xin khác về quan điểm này theo quan điểm thiết kế, vì truy cập trực tiếp này có nghĩa là chúng ta có một khớp nối cao và nếu chúng ta muốn trích xuất lớp lồng vào lớp cấp cao nhất của nó, chúng ta chỉ có thể làm như vậy sau khi chuyển cơ bản nó thành một lớp lồng tĩnh.

Vì vậy, câu hỏi của tôi đặt ra cho vấn đề này: Tôi có sai không khi cho rằng quyền truy cập thuộc tính có sẵn cho các lớp bên trong không tĩnh dẫn đến tính ghép cao, do đó làm giảm chất lượng mã và tôi suy ra rằng các lớp lồng nhau (không ẩn danh) nên Nói chung là tĩnh?

Hay nói cách khác: Có một lý do thuyết phục tại sao người ta thích một lớp bên trong lồng nhau?


2
từ hướng dẫn Java : "Sử dụng một lớp lồng không tĩnh (hoặc lớp bên trong) nếu bạn yêu cầu quyền truy cập vào các trường và phương thức không công khai của một cá thể kèm theo. Sử dụng một lớp lồng tĩnh nếu bạn không yêu cầu quyền truy cập này."
gnat

5
Cảm ơn về trích dẫn hướng dẫn, nhưng điều này chỉ lặp lại sự khác biệt là gì, không phải lý do tại sao bạn muốn truy cập vào các trường mẫu.
Frank

đó là một hướng dẫn chung, gợi ý về những gì thích hơn. Tôi đã thêm một lời giải thích chi tiết hơn về cách tôi diễn giải nó trong câu trả lời này
gnat

1
Nếu lớp bên trong không được liên kết chặt chẽ với lớp kèm theo, thì có lẽ nó không phải là lớp bên trong ở vị trí đầu tiên.
Kevin Krumwiede

Câu trả lời:


23

Joshua Bloch trong Mục 22 của cuốn sách "Phiên bản thứ hai Java hiệu quả" cho biết khi nào nên sử dụng loại lớp lồng nhau nào và tại sao. Có một số trích dẫn dưới đây:

Một cách sử dụng phổ biến của lớp thành viên tĩnh là lớp trợ giúp công cộng, chỉ hữu ích khi kết hợp với lớp bên ngoài. Ví dụ, hãy xem xét một enum mô tả các hoạt động được hỗ trợ bởi một máy tính. Enum hoạt động phải là một lớp thành viên tĩnh công khai của Calculatorlớp. Khách hàng Calculatorsau đó có thể tham khảo các hoạt động bằng cách sử dụng tên như Calculator.Operation.PLUSCalculator.Operation.MINUS.

Một cách sử dụng phổ biến của một lớp thành viên không phải là định nghĩa một Bộ điều hợp cho phép một thể hiện của lớp bên ngoài được xem như là một thể hiện của một số lớp không liên quan. Ví dụ, hiện thực của Mapgiao diện thường sử dụng các lớp học viên không tĩnh để thực hiện của họ xem bộ sưu tập , được trả về bởi Map's keySet, entrySetvaluesphương pháp. Tương tự, việc triển khai các giao diện bộ sưu tập, chẳng hạn như Set, và Listthường sử dụng các lớp thành viên không phải là thành viên để thực hiện các trình vòng lặp của chúng:

// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
    ... // Bulk of the class omitted

    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        ...
    }
}

Nếu bạn khai báo một lớp thành viên không yêu cầu quyền truy cập vào một thể hiện kèm theo, hãy luôn đặt công cụ staticsửa đổi trong khai báo của nó, làm cho nó trở thành một lớp tĩnh chứ không phải là một lớp thành viên không hoạt động.


1
Tôi không chắc chắn làm thế nào / liệu bộ điều hợp này thay đổi giao diện của các thể hiện của lớp bên ngoài MySet? Tôi có thể hình dung một cái gì đó giống như một loại phụ thuộc đường dẫn là một sự khác biệt, tức là myOneSet.iterator.getClass() != myOtherSet.iterator.getClass(), nhưng một lần nữa, trong Java điều này thực sự là không thể, vì mỗi lớp sẽ giống nhau.
Frank

@Frank Đó là một lớp bộ điều hợp khá điển hình - nó cho phép bạn xem tập hợp dưới dạng luồng các phần tử của nó (Trình lặp).
dùng253751

@immibis cảm ơn, tôi biết rõ về trình lặp / bộ điều hợp làm gì, nhưng câu hỏi này là về cách nó được thực hiện.
Frank

@Frank Tôi đã nói làm thế nào nó thay đổi quan điểm của trường hợp bên ngoài. Đó chính xác là những gì một bộ điều hợp làm - nó thay đổi cách bạn xem một số đối tượng. Trong trường hợp này, đối tượng đó là ví dụ bên ngoài.
dùng253751

10

Bạn đã đúng khi giả định rằng quyền truy cập thuộc tính có sẵn cho các lớp bên trong không tĩnh dẫn đến khả năng ghép cao, do đó chất lượng mã thấp hơn và các lớp bên trong (không ẩn danh và không cục bộ ) thường là tĩnh.

Ý nghĩa thiết kế của quyết định làm cho lớp bên trong không tĩnh được trình bày trong Java Puzzlers , Puzzle 90 (phông chữ đậm trong trích dẫn bên dưới là của tôi):

Bất cứ khi nào bạn viết một lớp thành viên, hãy tự hỏi mình, lớp này có thực sự cần một thể hiện kèm theo không? Nếu câu trả lời là không, hãy thực hiện nó static. Các lớp bên trong đôi khi hữu ích, nhưng chúng có thể dễ dàng đưa ra các biến chứng làm cho một chương trình trở nên khó hiểu. Chúng có các tương tác phức tạp với khái quát (Câu đố 89), phản xạ (Câu đố 80) và kế thừa (câu đố này) . Nếu bạn tuyên bố Inner1static, vấn đề sẽ biến mất. Nếu bạn cũng tuyên bố Inner2static, bạn thực sự có thể hiểu chương trình làm gì: một phần thưởng thực sự tốt.

Tóm lại, hiếm khi thích hợp cho một lớp vừa là lớp bên trong vừa là lớp con của lớp khác. Tổng quát hơn, hiếm khi thích hợp để mở rộng một lớp bên trong; nếu bạn phải, suy nghĩ lâu và chăm chỉ về trường hợp kèm theo. Ngoài ra, thích staticcác lớp lồng nhau hơn không static. Hầu hết các lớp thành viên có thể và nên được khai báo static.

Nếu bạn quan tâm, một phân tích chi tiết hơn về Puzzle 90 được cung cấp trong câu trả lời này tại Stack Overflow .


Điều đáng chú ý là trên thực chất là một phiên bản mở rộng của hướng dẫn được đưa ra trong Java Lớp và đối tượng hướng dẫn :

Sử dụng một lớp lồng không tĩnh (hoặc lớp bên trong) nếu bạn yêu cầu quyền truy cập vào các trường và phương thức không công khai của một thể hiện kèm theo. Sử dụng một lớp lồng tĩnh nếu bạn không yêu cầu quyền truy cập này.

Vì vậy, câu trả lời cho các câu hỏi mà bạn hỏi hay nói cách khác mỗi hướng dẫn là, chỉ vì lý do thuyết phục để sử dụng không tĩnh là khi truy cập vào các lĩnh vực và phương pháp ngoài công lập một ví dụ kèm theo được yêu cầu .

Từ ngữ hướng dẫn có phần rộng (đây có thể là lý do tại sao Java Puzzlers thực hiện một nỗ lực để tăng cường và thu hẹp nó). Cụ thể, việc truy cập trực tiếp vào các trường đối tượng kèm theo chưa bao giờ thực sự cần thiết theo kinh nghiệm của tôi - theo nghĩa là các cách khác như chuyển các tham số này như các tham số phương thức / phương thức xây dựng luôn dễ dàng hơn để gỡ lỗi và duy trì.


Nhìn chung, cuộc gặp gỡ (khá đau đớn) của tôi với việc gỡ lỗi các lớp bên trong truy cập trực tiếp vào các trường của trường hợp kèm theo đã gây ấn tượng mạnh mẽ rằng thực tiễn này giống như việc sử dụng trạng thái toàn cầu, cùng với các tệ nạn đã biết liên quan đến nó.

Tất nhiên, Java làm cho nó thiệt hại đến mức "toàn cầu" như vậy được chứa trong lớp kèm theo, nhưng khi tôi phải gỡ lỗi lớp bên trong cụ thể, cảm giác như một sự trợ giúp ban nhạc như vậy không giúp giảm đau: Tôi vẫn còn để ghi nhớ ngữ nghĩa "nước ngoài" và chi tiết thay vì tập trung hoàn toàn vào phân tích một đối tượng rắc rối cụ thể.


Để hoàn thiện, có thể có trường hợp lý do trên không áp dụng. Ví dụ: theo cách đọc của tôi về map.key Bộ javadocs , tính năng này gợi ý khớp nối chặt chẽ và kết quả là, làm mất hiệu lực các đối số chống lại các lớp không tĩnh:

Trả về một Setcái nhìn của các phím có trong bản đồ này. Bộ được hỗ trợ bởi bản đồ, do đó, các thay đổi đối với bản đồ được phản ánh trong bộ và ngược lại ...

Không phải ở trên bằng cách nào đó sẽ làm cho mã liên quan dễ dàng hơn để duy trì, kiểm tra và gỡ lỗi cho bạn, nhưng ít nhất nó có thể cho phép người ta lập luận rằng các kết quả khớp phức tạp / được chứng minh bằng chức năng dự định.


Tôi chia sẻ kinh nghiệm của bạn về các vấn đề gây ra bởi khớp nối cao này, nhưng tôi không ổn với yêu cầu sử dụng của hướng dẫn . Bạn tự nói với bản thân mình rằng bạn không bao giờ thực sự yêu cầu quyền truy cập trường, thật không may, điều này cũng không giải quyết đầy đủ câu hỏi của tôi.
Frank

@Frank cũng như bạn có thể thấy, cách giải thích của tôi về hướng dẫn hướng dẫn (dường như được Java Puzzlers hỗ trợ) giống như, chỉ sử dụng các lớp không tĩnh khi bạn có thể chứng minh rằng điều này là bắt buộc và đặc biệt, chứng minh rằng các cách khác là tồi tệ hơn . Đáng chú ý là theo kinh nghiệm của tôi, những cách khác luôn trở nên tốt hơn
gnat

Đó là điểm. Nếu nó luôn luôn tốt hơn, thì tại sao chúng ta phải bận tâm?
Frank

@Frank một câu trả lời khác cung cấp các ví dụ hấp dẫn mà điều này không phải lúc nào cũng như vậy. Theo cách đọc của tôi về map.keyset javadocs , tính năng này cho thấy sự kết hợp chặt chẽ và kết quả là, vô hiệu hóa các đối số chống lại các lớp không tĩnh "Tập hợp được hỗ trợ bởi bản đồ, do đó các thay đổi đối với bản đồ được phản ánh trong tập hợp và ngược lại ... "
gnat

1

Một số quan niệm sai lầm:

Ít khớp nối hơn: Chúng ta thường nhận được ít khớp nối hơn, vì lớp [tĩnh bên trong] không thể truy cập trực tiếp vào các thuộc tính của lớp bên ngoài.

IIRC - Trên thực tế, nó có thể. (Tất nhiên nếu chúng là các trường đối tượng, nó sẽ không có đối tượng bên ngoài để xem. Tuy nhiên, nếu nó có được một đối tượng như vậy từ bất cứ đâu, thì nó có thể truy cập trực tiếp vào các trường đó).

Lớp đơn: Trình nạp lớp không cần phải chăm sóc một lớp mới mỗi lần, chúng ta tạo một đối tượng của lớp bên ngoài. Chúng tôi chỉ nhận được các đối tượng mới cho cùng một lớp hơn và hơn.

Tôi không chắc ý của bạn ở đây là gì. Trình tải lớp chỉ tham gia tổng cộng hai lần, một lần cho mỗi lớp (khi lớp được tải).


Ramble sau:

Trong Javaland dường như chúng ta hơi sợ khi tạo các lớp. Tôi nghĩ rằng nó phải cảm thấy như một khái niệm quan trọng. Nó thường thuộc về tập tin riêng của mình. Nó cần nồi hơi đáng kể. Nó cần chú ý đến thiết kế của nó.

So với các ngôn ngữ khác - ví dụ Scala, hoặc có thể là C ++: Hoàn toàn không có kịch tạo ra một người giữ nhỏ bé (lớp, cấu trúc, bất cứ điều gì) bất cứ khi nào hai bit dữ liệu thuộc về nhau. Các quy tắc truy cập linh hoạt giúp bạn bằng cách cắt giảm bản tóm tắt, cho phép bạn giữ mã liên quan gần hơn về mặt vật lý. Điều này làm giảm 'khoảng cách tâm trí' của bạn giữa hai lớp.

Chúng ta có thể loại bỏ mối quan tâm về thiết kế đối với việc truy cập trực tiếp, bằng cách giữ cho lớp bên trong riêng tư.

(... Nếu bạn hỏi 'tại sao một lớp bên trong công cộng không tĩnh ?' Đó là một câu hỏi rất hay - tôi không nghĩ rằng tôi đã bao giờ tìm thấy một trường hợp hợp lý cho những người đó).

Bạn có thể sử dụng chúng bất cứ lúc nào nó giúp dễ đọc mã và tránh vi phạm DRY và nồi hơi. Tôi không có một ví dụ sẵn sàng vì nó không xảy ra thường xuyên theo kinh nghiệm của tôi, nhưng nó đã xảy ra.


Các lớp bên trong tĩnh vẫn là một cách tiếp cận mặc định tốt , btw. Non-static có thêm một trường ẩn được tạo thông qua đó lớp bên trong tham chiếu đến bên ngoài.


0

Nó có thể được sử dụng để tạo ra một mô hình nhà máy. Một mẫu nhà máy thường được sử dụng khi các đối tượng được tạo cần các tham số hàm tạo bổ sung để hoạt động, nhưng rất tẻ nhạt để cung cấp mỗi lần:

Xem xét:

public class QueryFactory {
    @Inject private Database database;

    public Query create(String sql) {
        return new Query(database, sql);
    }
}

public class Query {
    private final Database database;
    private final String sql;

    public Query(Database database, String sql) {
        this.database = database;
        this.sql = sql;
    } 

    public List performQuery() {
        return database.query(sql);
    }
}

Được dùng như là:

Query q = queryFactory.create("SELECT * FROM employees");

q.performQuery();

Điều tương tự có thể đạt được với một lớp bên trong không tĩnh:

public class QueryFactory {
    @Inject private Database database;

    public class Query {
        private final String sql;

        public Query(String sql) {
            this.sql = sql;
        }

        public List performQuery() {
            return database.query(sql);
        }
    }
}

Sử dụng như là:

Query q = queryFactory.new Query("SELECT * FROM employees");

q.performQuery();

Các nhà máy được hưởng lợi từ việc có các phương pháp được đặt tên cụ thể, như create, createFromSQLhoặc createFromTemplate. Điều tương tự cũng có thể được thực hiện ở đây dưới dạng nhiều lớp bên trong:

public class QueryFactory {

    public class SQL { ... }
    public class Native { ... }
    public class Builder { ... }

}

Việc truyền tham số ít hơn là cần thiết từ nhà máy đến lớp được xây dựng, nhưng khả năng đọc có thể bị ảnh hưởng một chút.

Đối chiếu:

Queries.Native q = queries.new Native("select * from employees");

đấu với

Query q = queryFactory.createNative("select * from employees");
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.