Kế thừa so với tài sản bổ sung có giá trị null


12

Đối với các lớp có các trường tùy chọn, tốt hơn là sử dụng tính kế thừa hoặc thuộc tính nullable? Xem xét ví dụ này:

class Book {
    private String name;
}
class BookWithColor extends Book {
    private String color;
}

hoặc là

class Book {
    private String name;
    private String color; 
    //when this is null then it is "Book" otherwise "BookWithColor"
}

hoặc là

class Book {
    private String name;
    private Optional<String> color;
    //when isPresent() is false it is "Book" otherwise "BookWithColor"
}

Mã tùy thuộc vào 3 tùy chọn này sẽ là:

if (book instanceof BookWithColor) { ((BookWithColor)book).getColor(); }

hoặc là

if (book.getColor() != null) { book.getColor(); }

hoặc là

if (book.getColor().isPresent()) { book.getColor(); }

Cách tiếp cận đầu tiên có vẻ tự nhiên hơn đối với tôi, nhưng có lẽ nó ít đọc hơn vì sự cần thiết phải thực hiện đúc. Có một số cách khác để đạt được hành vi này?


1
Tôi e rằng giải pháp đi xuống định nghĩa của bạn về các quy tắc kinh doanh. Ý tôi là, nếu tất cả các cuốn sách của bạn có thể có một màu nhưng màu là tùy chọn, thì sự kế thừa là quá mức cần thiết và thực sự không cần thiết, bởi vì bạn muốn tất cả các cuốn sách của bạn biết thuộc tính màu tồn tại. Mặt khác, nếu bạn có thể có cả những cuốn sách không biết gì về màu sắc thì từ trước đến nay và những cuốn sách có màu sắc, tạo ra các lớp học chuyên biệt không phải là xấu. Thậm chí sau đó, tôi có thể đi sáng tác về thừa kế.
Andy

Để làm cho nó cụ thể hơn một chút - có 2 loại sách - một loại có màu và một loại không có. Vì vậy, không phải tất cả các cuốn sách có thể có một màu.
Bojan VukasovicTest

1
Nếu thực sự có một trường hợp, trong đó cuốn sách hoàn toàn không có màu, thì trong trường hợp của bạn, kế thừa là cách đơn giản nhất để mở rộng các thuộc tính của một cuốn sách đầy màu sắc. Trong trường hợp này, có vẻ như bạn sẽ không phá vỡ bất cứ điều gì bằng cách kế thừa lớp sách cơ sở và thêm một thuộc tính màu cho nó. Bạn có thể đọc về nguyên tắc thay thế liskov để tìm hiểu thêm về các trường hợp khi mở rộng một lớp được cho phép và khi nào thì không.
Andy

4
Bạn có thể xác định một hành vi mà bạn muốn từ sách có màu? Bạn có thể tìm thấy một hành vi phổ biến giữa các cuốn sách có và không có màu, trong đó những cuốn sách có màu nên hành xử hơi khác nhau? Nếu vậy, đây là một trường hợp tốt cho OOP với các loại khác nhau, và, ý tưởng sẽ là chuyển các hành vi vào các lớp thay vì thẩm vấn sự hiện diện và giá trị tài sản bên ngoài.
Erik Eidt

1
Nếu các màu sách có thể được biết trước, làm thế nào về trường Enum cho BookColor với tùy chọn cho BookColor.NoColor?
Graham

Câu trả lời:


7

Nó phụ thuộc vào hoàn cảnh. Ví dụ cụ thể là không thực tế vì bạn sẽ không có một lớp con được gọi BookWithColortrong một chương trình thực.

Nhưng nói chung, một thuộc tính chỉ có ý nghĩa đối với các lớp con nhất định chỉ nên tồn tại trong các lớp con đó.

Ví dụ: nếu BookPhysicalBookDigialBooklà con cháu, thì PhysicalBookcó thể có một weighttài sản và DigitalBookmột sizeInKbtài sản. Nhưng DigitalBooksẽ không có weightvà ngược lại. Booksẽ không có thuộc tính, vì một lớp chỉ nên có các thuộc tính được chia sẻ bởi tất cả các hậu duệ.

Một ví dụ tốt hơn là nhìn vào các lớp từ phần mềm thực sự. Thành JSliderphần có trường majorTickSpacing. Vì chỉ có một thanh trượt có "tích tắc", trường này chỉ có ý nghĩa đối với JSlidervà hậu duệ của nó. Sẽ rất khó hiểu nếu các thành phần anh chị em khác như JButtoncó một majorTickSpacinglĩnh vực.


hoặc bạn có thể giảm cân để có thể phản xạ cả hai, nhưng điều đó có lẽ là quá nhiều.
Walfrat

3
@Walfrat: Sẽ là một ý tưởng thực sự tồi tệ khi có cùng một lĩnh vực đại diện cho những thứ hoàn toàn khác nhau trong các lớp con khác nhau.
JacquesB

Điều gì nếu một cuốn sách có cả phiên bản vật lý và kỹ thuật số? Bạn sẽ phải tạo một lớp khác nhau cho mỗi hoán vị có hoặc không có mỗi trường tùy chọn. Tôi nghĩ rằng giải pháp tốt nhất sẽ chỉ cho phép một số lĩnh vực được bỏ qua hoặc không phụ thuộc vào cuốn sách. Bạn đã trích dẫn một tình huống hoàn toàn khác nhau trong đó hai lớp sẽ hành xử khác nhau theo chức năng. Đó không phải là những gì câu hỏi này ngụ ý. Họ chỉ cần mô hình hóa một cấu trúc dữ liệu.
4 lâu đài

@ 4castle: Bạn sẽ cần các lớp riêng biệt cho mỗi phiên bản sau đó vì mỗi phiên bản cũng có thể có giá khác nhau. Bạn không thể giải quyết điều này với các trường tùy chọn. Nhưng chúng ta không biết từ ví dụ nếu op muốn hành vi khác nhau đối với sách có màu hoặc "sách không màu", vì vậy không thể biết mô hình chính xác trong trường hợp này là gì.
JacquesB

3
đồng ý với @JacquesB. bạn không thể đưa ra một giải pháp chính xác cho "sách mô hình hóa", bởi vì nó phụ thuộc vào vấn đề bạn đang cố gắng giải quyết và doanh nghiệp của bạn là gì. Tôi nghĩ khá an toàn khi nói rằng việc xác định hệ thống phân cấp lớp mà bạn vi phạm liskov là sai về mặt phân loại (tm) mặc dù (vâng, trường hợp của bạn có lẽ rất đặc biệt và đảm bảo điều đó (mặc dù tôi nghi ngờ về điều đó))
sara

5

Một điểm quan trọng dường như không được đề cập: Trong hầu hết các ngôn ngữ, các thể hiện của một lớp không thể thay đổi chúng là thể hiện của lớp nào. Vì vậy, nếu bạn có một cuốn sách không có màu và bạn muốn thêm một màu, bạn cần tạo một đối tượng mới nếu bạn đang sử dụng các lớp khác nhau. Và sau đó có lẽ bạn cần thay thế tất cả các tham chiếu đến đối tượng cũ bằng các tham chiếu đến đối tượng mới.

Nếu "sách không có màu" và "sách có màu" là các thể hiện của cùng một lớp, thì việc thêm màu hoặc xóa màu sẽ ít xảy ra sự cố hơn. (Nếu giao diện người dùng của bạn hiển thị danh sách "sách có màu" và "sách không có màu" thì giao diện người dùng đó sẽ phải thay đổi rõ ràng, nhưng tôi hy vọng đó là thứ bạn cần xử lý, tương tự như "danh sách màu đỏ sách "và" danh sách sách xanh ").


Đây thực sự là một điểm quan trọng! Nó định nghĩa liệu người ta có nên xem xét một tập hợp các thuộc tính tùy chọn thay vì các thuộc tính lớp hay không.
nhiễm sắc

1

Hãy nghĩ về JavaBean (như của bạn Book) như một bản ghi trong cơ sở dữ liệu. Các cột tùy chọn là nullkhi chúng không có giá trị, nhưng nó hoàn toàn hợp pháp. Do đó, tùy chọn thứ hai của bạn:

class Book {
    private String name;
    private String color; // null when not applicable
}

Là hợp lý nhất. 1

Hãy cẩn thận với cách bạn sử dụng Optionallớp. Ví dụ, nó không phải Serializable, thường là một đặc tính của JavaBean. Dưới đây là một số lời khuyên của Stephen Colebourne :

  1. Không khai báo bất kỳ biến thể hiện của loại Optional.
  2. Sử dụng nullđể chỉ ra dữ liệu tùy chọn trong phạm vi riêng tư của một lớp.
  3. Sử dụng Optionalcho getters truy cập vào trường tùy chọn.
  4. Không sử dụng Optionaltrong setters hoặc constructor.
  5. Sử dụng Optionallàm kiểu trả về cho bất kỳ phương thức logic nghiệp vụ nào khác có kết quả tùy chọn.

Do đó, trong lớp học của bạn, bạn nên sử dụng nullđể đại diện cho rằng lĩnh vực này là không có mặt, nhưng khi color các Book(như một kiểu trả về) nó phải được bao bọc bởi một Optional.

return Optional.ofNullable(color); // inside the class

book.getColor().orElse("No color"); // outside the class

Điều này cung cấp thiết kế rõ ràng và mã dễ đọc hơn.


1 Nếu bạn có ý định BookWithColorgói gọn cả một "lớp" sách có khả năng chuyên biệt so với các sách khác, thì sẽ rất hợp lý khi sử dụng tính kế thừa.


1
Ai nói bất cứ điều gì về một cơ sở dữ liệu? Nếu tất cả các mẫu OO phải phù hợp với mô hình cơ sở dữ liệu quan hệ, chúng ta sẽ phải thay đổi rất nhiều mẫu OO.
alexanderbird

Tôi cho rằng việc thêm các thuộc tính không sử dụng "chỉ trong trường hợp" là tùy chọn ít rõ ràng nhất. Như những người khác nói, nó phụ thuộc vào trường hợp sử dụng, nhưng tình huống mong muốn nhất sẽ không phải là mang theo các thuộc tính mà ứng dụng bên ngoài cần biết liệu một tài sản tồn tại có thực sự được sử dụng hay không.
Volker Kueffel

@Volker Hãy đồng ý không đồng ý, vì tôi có thể trích dẫn một số người đã chơi một tay trong việc thêm Optionallớp vào Java cho mục đích chính xác này. Nó có thể không phải là OO, nhưng đó chắc chắn là một ý kiến ​​hợp lệ.
4 lâu đài

@Alexanderbird Tôi đã đặc biệt giải quyết một JavaBean, có thể được tuần tự hóa. Nếu tôi lạc đề bằng cách đưa lên JavaBeans, thì đó là sai lầm của tôi.
4 lâu đài

@Alexanderbird Tôi không mong đợi tất cả các mẫu phù hợp với cơ sở dữ liệu quan hệ, nhưng tôi thực sự mong đợi Java Bean đến
4castle

0

Bạn nên sử dụng một giao diện (không phải kế thừa) hoặc một loại cơ chế thuộc tính nào đó (thậm chí có thể là một cái gì đó hoạt động như khung mô tả tài nguyên).

Vì vậy

interface Colored {
  Color getColor();
}
class ColoredBook extends Book implements Colored {
  ...
}

hoặc là

class PropertiesHolder {
  <T> extends Property<?>> Optional<T> getProperty( Class<T> propertyClass ) { ... }
  <V, T extends Property<V>> Optional<V> getPropertyValue( Class<T> propertyClass ) { ... }
}
interface Property<T> {
  String getName();
  T getValue();
}
class ColorProperty implements Property<Color> {
  public Color getValue() { ... }
  public String getName() { return "color"; }
}
class Book extends PropertiesHolder {
}

Làm rõ (chỉnh sửa):

Chỉ cần thêm các trường tùy chọn trong lớp thực thể

Đặc biệt với các tùy chọn-wrapper (chỉnh sửa: xem 4castle câu trả lời 's) Tôi nghĩ rằng đây (thêm các lĩnh vực trong thực thể ban đầu) là một cách hữu hiệu để thêm các thuộc tính mới trong một quy mô nhỏ. Vấn đề lớn nhất với phương pháp này là nó có thể hoạt động chống lại khớp nối thấp.

Hãy tưởng tượng lớp sách của bạn được xác định trong một dự án dành riêng cho mô hình miền của bạn. Bây giờ bạn thêm một dự án khác sử dụng mô hình miền để thực hiện một nhiệm vụ đặc biệt. Nhiệm vụ này đòi hỏi một tài sản bổ sung trong lớp sách. Hoặc là bạn kết thúc với sự kế thừa (xem bên dưới) hoặc bạn phải thay đổi mô hình miền phổ biến để thực hiện nhiệm vụ mới. Trong trường hợp sau, bạn có thể kết thúc với một loạt các dự án mà tất cả phụ thuộc vào thuộc tính của riêng chúng được thêm vào lớp sách trong khi lớp sách theo cách phụ thuộc vào các dự án này, vì bạn không thể hiểu lớp sách mà không có dự án bổ sung.

Tại sao thừa kế có vấn đề khi nói đến một cách để cung cấp các thuộc tính bổ sung?

Khi tôi thấy lớp mẫu của bạn "Sách", tôi nghĩ về một đối tượng miền thường có nhiều trường và kiểu con tùy chọn. Chỉ cần tưởng tượng bạn muốn thêm một tài sản cho những cuốn sách bao gồm một đĩa CD. Hiện nay có bốn loại sách: sách, sách có màu, sách có CD, sách có màu CD. Bạn không thể mô tả tình huống này với sự kế thừa trong Java.

Với giao diện bạn tránh được vấn đề này. Bạn có thể dễ dàng soạn các thuộc tính của một lớp sách cụ thể với các giao diện. Đoàn và thành phần sẽ giúp bạn dễ dàng có được chính xác lớp bạn muốn. Với sự kế thừa, bạn thường kết thúc với một số thuộc tính tùy chọn trong lớp chỉ có ở đó vì một lớp anh chị em cần chúng.

Đọc thêm tại sao thừa kế thường là một ý tưởng có vấn đề:

Tại sao sự thừa kế thường được xem là một điều xấu bởi những người đề xướng OOP

JavaWorld: Tại sao kéo dài là xấu xa

Vấn đề với việc thực hiện một bộ giao diện để soạn một tập các thuộc tính

Khi bạn sử dụng các giao diện để mở rộng, mọi thứ đều ổn miễn là bạn chỉ có một bộ nhỏ. Đặc biệt là khi mô hình đối tượng của bạn được sử dụng và mở rộng bởi các nhà phát triển khác, ví dụ như trong công ty của bạn, số lượng giao diện sẽ tăng lên. Và cuối cùng, bạn kết thúc việc tạo ra một "giao diện thuộc tính" chính thức mới, bổ sung một phương thức mà các đồng nghiệp của bạn đã sử dụng trong dự án của họ cho khách hàng X cho một trường hợp sử dụng hoàn toàn không liên quan - Ugh.

chỉnh sửa: Một khía cạnh quan trọng khác được đề cập bởi gnasher729 . Bạn thường muốn tự động thêm các thuộc tính tùy chọn vào một đối tượng hiện có. Với sự kế thừa hoặc giao diện, bạn sẽ phải tạo lại toàn bộ đối tượng với một lớp khác không phải là tùy chọn.

Khi bạn mong đợi các tiện ích mở rộng cho mô hình đối tượng của mình đến một mức độ như vậy, bạn sẽ tốt hơn bằng cách mô hình hóa rõ ràng khả năng của tiện ích mở rộng động . Tôi đề xuất một cái gì đó giống như ở trên nơi mỗi "phần mở rộng" (trong trường hợp này là thuộc tính) có lớp riêng. Lớp này hoạt động như không gian tên và định danh cho phần mở rộng. Khi bạn đặt các quy ước đặt tên gói theo cách này cho phép số lượng tiện ích mở rộng vô hạn mà không gây ô nhiễm không gian tên cho các phương thức trong lớp thực thể ban đầu.

Trong phát triển trò chơi, bạn thường gặp phải tình huống bạn muốn soạn thảo hành vi và dữ liệu theo nhiều biến thể. Đây là lý do tại sao mô hình kiến ​​trúc -thành phần-hệ thống kiến trúc trở nên khá phổ biến trong giới phát triển trò chơi. Đây cũng là một cách tiếp cận thú vị mà bạn có thể muốn xem xét, khi bạn mong đợi nhiều phần mở rộng cho mô hình đối tượng của mình.


Và nó đã không giải quyết câu hỏi "làm thế nào để quyết định giữa các tùy chọn này", đó chỉ là một lựa chọn khác. Tại sao điều này luôn tốt hơn tất cả các tùy chọn mà OP đưa ra?
alexanderbird

Bạn nói đúng, tôi sẽ cải thiện câu trả lời.
nhiễm sắc

@ 4castle Trong giao diện Java không phải là sự kế thừa! Triển khai các giao diện là một cách để tuân thủ hợp đồng trong khi kế thừa một lớp về cơ bản là mở rộng hành vi của nó. Bạn có thể thực hiện nhiều giao diện trong khi bạn không thể mở rộng nhiều lớp trong một lớp. Trong ví dụ của tôi, bạn mở rộng với các giao diện trong khi bạn sử dụng tính kế thừa để kế thừa hành vi của thực thể ban đầu.
chromanoid

Có ý nghĩa. Trong ví dụ của bạn, PropertiesHoldercó lẽ sẽ là một lớp trừu tượng. Nhưng những gì bạn sẽ đề nghị là một thực hiện cho getPropertyhoặc getPropertyValue? Dữ liệu vẫn phải được lưu trữ trong một số loại trường ví dụ ở đâu đó. Hoặc bạn sẽ sử dụng một Collection<Property>để lưu trữ các thuộc tính thay vì sử dụng các trường?
4 lâu đài

1
Tôi đã Bookmở rộng PropertiesHoldervì lý do rõ ràng. Các PropertiesHolderthường phải là một thành viên trong tất cả các lớp học cần chức năng này. Việc thực hiện sẽ giữ một Map<Class<? extends Property<?>>,Property<?>>hoặc một cái gì đó như thế này. Đây là phần xấu của việc triển khai nhưng nó cung cấp một khung thực sự tốt, thậm chí hoạt động với JAXB và JPA.
chromanoid

-1

Nếu Booklớp là dẫn xuất mà không có thuộc tính màu như dưới đây

class E_Book extends Book{
   private String type;
}

sau đó thừa kế là hợp lý.


Tôi không chắc tôi hiểu ví dụ của bạn? Nếu colorlà riêng tư thì bất kỳ lớp dẫn xuất nào cũng không cần phải quan tâm color. Chỉ cần thêm một số hàm tạo để Bookbỏ qua colorhoàn toàn và nó sẽ mặc định null.
4 lâu đà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.