Giao diện với các trường tĩnh trong java để chia sẻ 'hằng số'


116

Tôi đang xem xét một số dự án Java mã nguồn mở để sử dụng Java và nhận thấy rất nhiều trong số chúng có một số loại giao diện 'hằng số'.

Ví dụ: processing.org có một giao diện được gọi là PConstants.java và hầu hết các lớp lõi khác triển khai giao diện này. Giao diện có nhiều thành viên tĩnh. Có lý do cho cách tiếp cận này, hoặc điều này được coi là thực hành xấu? Tại sao không sử dụng enum nơi nó có ý nghĩa , hoặc một lớp tĩnh?

Tôi thấy lạ khi sử dụng một giao diện cho phép một số loại 'biến toàn cục' giả.

public interface PConstants {

  // LOTS OF static fields...

  static public final int SHINE = 31;

  // emissive (by default kept black)
  static public final int ER = 32;
  static public final int EG = 33;
  static public final int EB = 34;

  // has this vertex been lit yet
  static public final int BEEN_LIT = 35;

  static public final int VERTEX_FIELD_COUNT = 36;


  // renderers known to processing.core

  static final String P2D    = "processing.core.PGraphics2D";
  static final String P3D    = "processing.core.PGraphics3D";
  static final String JAVA2D = "processing.core.PGraphicsJava2D";
  static final String OPENGL = "processing.opengl.PGraphicsOpenGL";
  static final String PDF    = "processing.pdf.PGraphicsPDF";
  static final String DXF    = "processing.dxf.RawDXF";


  // platform IDs for PApplet.platform

  static final int OTHER   = 0;
  static final int WINDOWS = 1;
  static final int MACOSX  = 2;
  static final int LINUX   = 3;

  static final String[] platformNames = {
    "other", "windows", "macosx", "linux"
  };

  // and on and on

}

15
Lưu ý: static finalkhông cần thiết, nó là thừa đối với một giao diện.
ThomasW

Cũng lưu ý rằng platformNamescó thể là public, staticfinal, nhưng nó chắc chắn không phải là một hằng số. Mảng hằng số duy nhất là mảng có độ dài bằng 0.
Vlasec

@ThomasW Tôi biết điều này đã có từ vài năm trước, nhưng tôi cần chỉ ra lỗi trong nhận xét của bạn. static finalkhông nhất thiết là thừa. Một trường lớp hoặc giao diện chỉ có finaltừ khóa sẽ tạo ra các thể hiện riêng biệt của trường đó khi bạn tạo các đối tượng của lớp hoặc giao diện. Việc sử dụng static finalsẽ làm cho mỗi đối tượng chia sẻ một vị trí bộ nhớ cho trường đó. Nói cách khác, nếu một MyClass của lớp có một trường final String str = "Hello";, thì đối với N trường hợp của MyClass, sẽ có N trường hợp của trường str trong bộ nhớ. Thêm statictừ khóa sẽ chỉ dẫn đến 1 trường hợp.
Sintrias

Câu trả lời:


160

Nó thường được coi là thực hành xấu. Vấn đề là các hằng số là một phần của "giao diện" công khai (muốn có một từ tốt hơn) của lớp thực thi. Điều này có nghĩa là lớp triển khai đang xuất bản tất cả các giá trị này ra các lớp bên ngoài ngay cả khi chúng chỉ được yêu cầu nội bộ. Các hằng số tăng sinh trong suốt mã. Một ví dụ là giao diện SwingConstants trong Swing, được thực hiện bởi hàng chục lớp mà tất cả đều "tái xuất" tất cả các hằng số của nó (ngay cả những hằng số mà chúng không sử dụng) như của riêng chúng.

Nhưng đừng chỉ nghe lời tôi, Josh Bloch cũng nói điều đó thật tệ:

Mẫu giao diện không đổi là cách sử dụng giao diện kém. Việc một lớp sử dụng một số hằng số bên trong là một chi tiết triển khai. Việc triển khai một giao diện không đổi khiến chi tiết triển khai này bị rò rỉ vào API đã xuất của lớp. Không có hậu quả gì đối với người dùng của một lớp mà lớp đó thực hiện một giao diện không đổi. Trên thực tế, nó thậm chí có thể khiến họ nhầm lẫn. Tệ hơn nữa, nó đại diện cho một cam kết: nếu trong một bản phát hành trong tương lai, lớp được sửa đổi để nó không cần sử dụng các hằng số nữa, nó vẫn phải triển khai giao diện để đảm bảo tính tương thích nhị phân. Nếu một lớp nonfinal triển khai một giao diện không đổi, tất cả các lớp con của nó sẽ có không gian tên của chúng bị ô nhiễm bởi các hằng số trong giao diện.

Một enum có thể là một cách tiếp cận tốt hơn. Hoặc bạn có thể chỉ cần đặt các hằng số dưới dạng các trường tĩnh công cộng trong một lớp không thể được khởi tạo. Điều này cho phép một lớp khác truy cập chúng mà không làm ô nhiễm API của chính nó.


8
Enums là một con cá trích đỏ ở đây - hoặc ít nhất là một câu hỏi riêng biệt. Tất nhiên nên sử dụng các Enums, nhưng chúng cũng nên được ẩn đi nếu người triển khai không cần chúng.
DJClayworth

12
BTW: Bạn có thể sử dụng một enum không có cá thể làm lớp không thể khởi tạo. ;)
Peter Lawrey

5
Nhưng tại sao lại triển khai các giao diện đó ngay từ đầu? Tại sao không sử dụng chúng như một kho lưu trữ hằng số? Nếu tôi cần một số loại hằng số được chia sẻ trên toàn cầu, tôi không thấy cách thực hiện "sạch" hơn.
shadox,

2
@DanDyer vâng, nhưng một giao diện làm cho một số khai báo ẩn. Giống như chung kết tĩnh công khai chỉ là một mặc định. Tại sao phải bận tâm với một lớp học? Enum - tốt, nó phụ thuộc. Một enum phải xác định một tập hợp các giá trị có thể có cho một thực thể chứ không phải một tập hợp các giá trị cho các thực thể khác nhau.
shadox,

4
Cá nhân tôi cảm thấy Josh đánh bóng sai. Nếu bạn không muốn các hằng số của mình bị rò rỉ - bất kể bạn đặt chúng vào loại đối tượng nào - bạn cần đảm bảo rằng chúng KHÔNG phải là một phần của mã đã xuất. Giao diện hoặc lớp, cả hai đều có thể được xuất khẩu. Vì vậy, câu hỏi phải đặt ra không phải là: tôi đặt chúng vào loại đối tượng nào mà là tôi tổ chức đối tượng này như thế nào. Và nếu các hằng được sử dụng trong mã đã xuất, bạn vẫn phải đảm bảo rằng chúng có sẵn sau khi xuất. Vì vậy, tuyên bố "thực hành xấu" theo ý kiến ​​khiêm tốn của tôi là không hợp lệ.
Lawrence

99

Thay vì triển khai "giao diện hằng số", trong Java 1.5+, bạn có thể sử dụng nhập tĩnh để nhập hằng số / phương thức tĩnh từ một lớp / giao diện khác:

import static com.kittens.kittenpolisher.KittenConstants.*;

Điều này tránh sự xấu xí khi làm cho các lớp của bạn triển khai các giao diện không có chức năng.

Đối với việc thực hành có một lớp chỉ để lưu trữ các hằng số, tôi nghĩ rằng nó đôi khi cần thiết. Có một số hằng số nhất định không có vị trí tự nhiên trong một lớp, vì vậy tốt hơn nên đặt chúng ở vị trí "trung lập".

Nhưng thay vì sử dụng một giao diện, hãy sử dụng một lớp cuối cùng với một phương thức khởi tạo riêng. (Làm cho lớp này không thể khởi tạo hoặc phân lớp, gửi một thông báo mạnh mẽ rằng nó không chứa chức năng / dữ liệu không tĩnh.)

Ví dụ:

/** Set of constants needed for Kitten Polisher. */
public final class KittenConstants
{
    private KittenConstants() {}

    public static final String KITTEN_SOUND = "meow";
    public static final double KITTEN_CUTENESS_FACTOR = 1;
}

Vì vậy, bạn đang giải thích rằng, vì nhập tĩnh, chúng ta nên sử dụng các lớp thay vì các giao diện để thực hiện lại lỗi giống như trước đây ?! Điều đó thật ngớ ngẩn!
gizmo

11
Không, đó không phải là những gì tôi đang nói. Tôi đang nói hai điều độc lập. 1: sử dụng nhập tĩnh thay vì lạm dụng kế thừa. 2: Nếu bạn phải có một kho lưu trữ hằng số, hãy đặt nó trở thành lớp cuối cùng thay vì một giao diện.
Zarkonnen

"Giao diện không đổi", nơi không được thiết kế để trở thành một phần của bất kỳ kế thừa nào, không bao giờ. Vì vậy, nhập tĩnh chỉ dành cho đường cú pháp, và kế thừa từ giao diện như vậy là một lỗi khủng khiếp. Tôi biết Sun đã làm điều này, nhưng họ cũng mắc rất nhiều lỗi cơ bản khác, đó không phải là cái cớ để bắt chước họ.
gizmo

3
Một trong những vấn đề với mã được đăng cho câu hỏi là việc triển khai giao diện được sử dụng chỉ để truy cập các hằng số dễ dàng hơn. Khi tôi thấy thứ gì đó triển khai FooInterface, tôi cho rằng điều đó sẽ ảnh hưởng đến chức năng của nó và điều trên vi phạm điều này. Nhập tĩnh khắc phục sự cố đó.
Zarkonnen

2
gizmo - Tôi không phải là người thích nhập tĩnh, nhưng những gì anh ấy đang làm là tránh phải sử dụng tên lớp, tức là ConstClass.SOME_CONST. Thực tế nhập tĩnh không thêm các thành viên đó vào lớp mà bạn Z. không nói là kế thừa từ giao diện, anh ấy nói ngược lại trên thực tế.
mtruesdell

8

Tôi không giả vờ đúng là đúng, nhưng hãy xem ví dụ nhỏ sau:

public interface CarConstants {

      static final String ENGINE = "mechanical";
      static final String WHEEL  = "round";
      // ...

}

public interface ToyotaCar extends CarConstants //, ICar, ... {
      void produce();
}

public interface FordCar extends CarConstants //, ICar, ... {
      void produce();
}

// and this is implementation #1
public class CamryCar implements ToyotaCar {

      public void produce() {
           System.out.println("the engine is " + ENGINE );
           System.out.println("the wheel is " + WHEEL);
      }
}

// and this is implementation #2
public class MustangCar implements FordCar {

      public void produce() {
           System.out.println("the engine is " + ENGINE );
           System.out.println("the wheel is " + WHEEL);
      }
}

ToyotaCar không biết gì về FordCar, và FordCar không biết về ToyotaCar. nguyên tắc CarConstants nên được thay đổi, nhưng ...

Không nên thay đổi các hằng số, bởi vì bánh xe tròn và bánh xe tròn là cơ khí, nhưng ... Trong tương lai, các kỹ sư nghiên cứu của Toyota đã phát minh ra động cơ điện tử và bánh xe phẳng! Hãy xem giao diện mới của chúng tôi

public interface InnovativeCarConstants {

          static final String ENGINE = "electronic";
          static final String WHEEL  = "flat";
          // ...
}

và bây giờ chúng ta có thể thay đổi sự trừu tượng của mình:

public interface ToyotaCar extends CarConstants

đến

public interface ToyotaCar extends InnovativeCarConstants 

Và bây giờ nếu chúng ta cần thay đổi giá trị cốt lõi nếu ĐỘNG CƠ hoặc BÁNH XE, chúng ta có thể thay đổi Giao diện ToyotaCar ở cấp độ trừu tượng, đừng chạm vào triển khai

Nó KHÔNG AN TOÀN, tôi biết, nhưng tôi vẫn muốn biết rằng bạn có nghĩ về điều này không


Tôi muốn biết ý tưởng của bạn ngay bây giờ vào năm 2019. Đối với tôi, các trường giao diện được dùng để chia sẻ giữa một số đối tượng.
Mưa

Tôi đã viết một câu trả lời liên quan đến ý tưởng của bạn: stackoverflow.com/a/55877115/5290519
mưa

đây là một ví dụ điển hình về cách các quy tắc của PMD rất hạn chế. Cố gắng có được mã tốt hơn thông qua bộ máy hành chính vẫn là một nỗ lực vô ích.
bebbo

6

Có rất nhiều sự ghét bỏ đối với mô hình này trong Java. Tuy nhiên, một giao diện của các hằng số tĩnh đôi khi có giá trị. Về cơ bản bạn cần đáp ứng các điều kiện sau:

  1. Các khái niệm là một phần của giao diện công cộng của một số lớp.

  2. Giá trị của chúng có thể thay đổi trong các bản phát hành trong tương lai.

  3. Điều quan trọng là tất cả các triển khai đều sử dụng các giá trị giống nhau.

Ví dụ: giả sử rằng bạn đang viết một phần mở rộng cho một ngôn ngữ truy vấn giả định. Trong phần mở rộng này, bạn sẽ mở rộng cú pháp ngôn ngữ với một số thao tác mới, được hỗ trợ bởi một chỉ mục. Ví dụ: Bạn sẽ có R-Tree hỗ trợ các truy vấn không gian địa lý.

Vì vậy, bạn viết một giao diện công khai với hằng số tĩnh:

public interface SyntaxExtensions {
     // query type
     String NEAR_TO_QUERY = "nearTo";

     // params for query
     String POINT = "coordinate";
     String DISTANCE_KM = "distanceInKm";
}

Bây giờ sau này, một nhà phát triển mới nghĩ rằng anh ta cần xây dựng một chỉ mục tốt hơn, vì vậy anh ta đến và xây dựng một bản triển khai R *. Bằng cách triển khai giao diện này trong cây mới của mình, anh ta đảm bảo rằng các chỉ mục khác nhau sẽ có cú pháp giống hệt nhau trong ngôn ngữ truy vấn. Hơn nữa, nếu sau này bạn quyết định rằng "nearTo" là một cái tên khó hiểu, bạn có thể đổi nó thành "withinDistanceInKm" và biết rằng cú pháp mới sẽ được tất cả các triển khai chỉ mục của bạn tôn trọng.

Tái bút: Nguồn cảm hứng cho ví dụ này được lấy từ mã không gian Neo4j.


5

Với lợi thế của nhận thức muộn, chúng ta có thể thấy rằng Java bị phá vỡ theo nhiều cách. Một lỗi lớn của Java là hạn chế giao diện đối với các phương thức trừu tượng và các trường cuối cùng tĩnh. Các ngôn ngữ OO mới hơn, tinh vi hơn như các giao diện con Scala theo các đặc điểm có thể (và thường làm) bao gồm các phương thức cụ thể, có thể có độ hiếm bằng 0 (hằng số!). Để biết phần trình bày về các đặc điểm dưới dạng đơn vị của hành vi có thể tổng hợp, hãy xem http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf . Để biết mô tả ngắn gọn về cách các đặc điểm trong Scala so sánh với các giao diện trong Java, hãy xem http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5. Trong bối cảnh dạy thiết kế OO, các quy tắc đơn giản như khẳng định rằng các giao diện không bao giờ được bao gồm các trường tĩnh là ngớ ngẩn. Nhiều đặc điểm tự nhiên bao gồm các hằng số và những hằng số này là một phần thích hợp của "giao diện" công khai được hỗ trợ bởi đặc điểm. Trong việc viết mã Java, không có cách nào rõ ràng và thanh lịch để biểu diễn các đặc điểm, nhưng sử dụng các trường cuối cùng tĩnh trong các giao diện thường là một phần của một giải pháp tốt.


12
Tự phụ kinh khủng và ngày nay đã lỗi thời.
Esko

1
Thông tin chi tiết tuyệt vời (+1), mặc dù có thể hơi quá quan trọng đối với Java.
peterh - Phục hồi Monica

0

Theo đặc tả JVM, các trường và phương thức trong Giao diện chỉ có thể có Công khai, Tĩnh, Cuối cùng và Trừu tượng. Tham khảo từ Inside Java VM

Theo mặc định, tất cả các phương thức trong giao diện là trừu tượng, thậm chí khó mà bạn không đề cập rõ ràng.

Các giao diện chỉ cung cấp thông số kỹ thuật. Nó không thể chứa bất kỳ triển khai nào. Vì vậy, để tránh thực hiện các lớp để thay đổi đặc tả, nó được thực hiện cuối cùng. Vì Giao diện không thể được khởi tạo, chúng được đặt tĩnh để truy cập trường bằng cách sử dụng tên giao diện.


0

Tôi không có đủ danh tiếng để đưa ra nhận xét cho Pleerock, vì vậy tôi phải tạo câu trả lời. Tôi xin lỗi vì điều đó, nhưng anh ấy đã cố gắng hết sức và tôi muốn trả lời anh ấy.

Pleerock, bạn đã tạo ra một ví dụ hoàn hảo để chỉ ra lý do tại sao các hằng số đó phải độc lập với các giao diện và không phụ thuộc vào kế thừa. Đối với khách hàng của ứng dụng, điều quan trọng là có sự khác biệt về kỹ thuật giữa những lần thực hiện xe ô tô. Chúng giống nhau đối với khách hàng, chỉ là ô tô. Vì vậy, khách hàng muốn nhìn chúng từ góc độ đó, đó là một giao diện giống như I_Somecar. Trong toàn bộ ứng dụng, khách hàng sẽ chỉ sử dụng một góc nhìn và không sử dụng các góc nhìn khác nhau cho từng thương hiệu xe khác nhau.

Nếu khách hàng muốn so sánh những chiếc xe trước khi mua, anh ta có thể có một phương pháp như sau:

public List<Decision> compareCars(List<I_Somecar> pCars);

Giao diện là một hợp đồng về hành vi và hiển thị các đối tượng khác nhau từ một góc độ. Theo cách bạn thiết kế, mỗi thương hiệu xe hơi sẽ có dòng kế thừa riêng. Mặc dù trên thực tế điều đó hoàn toàn chính xác, bởi vì những chiếc xe có thể khác nhau đến mức có thể giống như việc so sánh các loại đồ vật hoàn toàn khác nhau, cuối cùng vẫn có sự lựa chọn giữa những chiếc xe khác nhau. Và đó là góc nhìn của giao diện mà tất cả các thương hiệu phải chia sẻ. Việc lựa chọn các hằng số sẽ không làm cho điều này trở nên bất khả thi. Hãy xem xét câu trả lời của Zarkonnen.


-1

Điều này xuất hiện từ thời điểm trước khi Java 1.5 tồn tại và mang đến cho chúng ta các enum. Trước đó, không có cách nào tốt để xác định một tập hợp các hằng số hoặc các giá trị bị ràng buộc.

Điều này vẫn được sử dụng, hầu hết thời gian hoặc để tương thích ngược hoặc do số lượng cấu trúc lại cần thiết để loại bỏ, trong rất nhiều dự án.


2
Trước Java 5, bạn có thể sử dụng kiểu enum type-safe (xem java.sun.com/developer/Books/shiftintojava/page1.html ).
Dan Dyer
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.