Nhiều lớp nhỏ so với thừa kế logic (nhưng) phức tạp


24

Tôi đang tự hỏi điều gì là tốt hơn về mặt OOP tốt, mã sạch, tính linh hoạt và tránh mùi mã trong tương lai. Tình huống hình ảnh, nơi bạn có rất nhiều đối tượng rất giống nhau mà bạn cần phải biểu diễn dưới dạng các lớp. Các lớp này không có bất kỳ chức năng cụ thể nào, chỉ có các lớp dữ liệu và khác nhau chỉ theo tên (và ngữ cảnh) Ví dụ:

Class A
{
  String name;
  string description;
}

Class B
{
  String name;
  String count;
  String description;
}

Class C
{
  String name;
  String count;
  String description;
  String imageUrl;
}

Class D
{
  String name;
  String count;
}

Class E
{
  String name;
  String count;
  String imageUrl;
  String age;
}

Sẽ tốt hơn nếu giữ chúng trong các lớp riêng biệt, để có được khả năng đọc "tốt hơn", nhưng có nhiều lần lặp lại mã, hoặc sẽ tốt hơn nếu sử dụng tính kế thừa để trở nên KHÔ hơn?

Bằng cách sử dụng tính kế thừa, bạn sẽ kết thúc với một cái gì đó như thế này, nhưng tên lớp sẽ mất đi ý nghĩa theo ngữ cảnh (vì is-a, không có-a):

Class A
{
  String name;
  String description;
}

Class B : A
{
  String count;
}

Class C : B
{
  String imageUrl;
}

Class D : C
{
  String age;
}

Tôi biết kế thừa không và không nên được sử dụng để tái sử dụng mã, nhưng tôi không thấy bất kỳ cách nào khác có thể làm thế nào để giảm sự lặp lại mã trong trường hợp đó.

Câu trả lời:


58

Quy tắc chung ghi "Ưu tiên ủy quyền hơn thừa kế", không "tránh tất cả thừa kế". Nếu các đối tượng có mối quan hệ logic và B có thể được sử dụng ở bất cứ nơi nào A dự kiến, thì nên sử dụng tính kế thừa. Tuy nhiên, nếu các đối tượng chỉ có các trường có cùng tên và không có mối quan hệ miền, thì không sử dụng tính kế thừa.

Rất thường xuyên, nó trả tiền để đặt câu hỏi về "lý do thay đổi" trong quy trình thiết kế: Nếu lớp A có thêm một trường, bạn có mong đợi rằng lớp B cũng nhận được nó không? Nếu đó là sự thật, các đối tượng chia sẻ một mối quan hệ và thừa kế là một ý tưởng tốt. Nếu không, chịu đựng sự lặp lại nhỏ để giữ các khái niệm riêng biệt trong mã riêng biệt.


Chắc chắn, lý do thay đổi là điểm tốt, nhưng trong tình huống, những lớp này đang và sẽ luôn là không đổi?
dùng969153

20
@ user969153: Khi các lớp sẽ thực sự không đổi, điều đó không thành vấn đề. Tất cả các kỹ thuật phần mềm tốt là dành cho người bảo trì trong tương lai, những người cần phải thay đổi. Nếu không có thay đổi trong tương lai, mọi thứ sẽ ổn, nhưng tin tôi đi, bạn sẽ luôn có những thay đổi trừ khi bạn lập trình cho thùng rác.
thiton

@ user969153: Có thể nói, "lý do thay đổi" là một heuristic. Nó giúp bạn quyết định, không có gì hơn.
thiton

2
Câu trả lời tốt. Lý do để thay đổi là S trong từ viết tắt RẮN của chú Bob sẽ giúp thiết kế phần mềm tốt.
Klee

4
@ user969153, Theo kinh nghiệm của tôi, "nơi các lớp này luôn và sẽ luôn là hằng số?" không phải là trường hợp sử dụng hợp lệ :) Tôi vẫn chưa làm việc trên một ứng dụng có kích thước có ý nghĩa mà không gặp phải sự thay đổi hoàn toàn bất ngờ.
cdkMoose

18

Bằng cách sử dụng tính kế thừa, bạn sẽ kết thúc với một cái gì đó như thế này, nhưng tên lớp sẽ mất đi ý nghĩa theo ngữ cảnh (vì is-a, không có-a):

Chính xác. Nhìn vào điều này, ra khỏi bối cảnh:

Class D : C
{
    String age;
}

D có những tính chất gì? Bạn có thể nhớ không Điều gì xảy ra nếu bạn làm phức tạp thêm thứ bậc:

Class E : B
{
    int numberOfWheels;
}

Class F : E
{
    String favouriteColour;
}

Và nếu như, kinh hoàng vì kinh hoàng, bạn muốn thêm trường imageUrl vào F chứ không phải E thì sao? Bạn không thể lấy nó từ C và E.

Tôi đã thấy một tình huống thực tế trong đó rất nhiều loại Thành phần khác nhau đều có tên, ID và Mô tả. Nhưng đây không phải là bản chất của các thành phần của chúng, nó chỉ là một sự trùng hợp ngẫu nhiên. Mỗi người có một ID vì chúng được lưu trữ trong cơ sở dữ liệu. Tên và mô tả là để hiển thị.

Sản phẩm cũng có ID, Tên và Mô tả, vì lý do tương tự. Nhưng chúng không phải là linh kiện, cũng không phải là sản phẩm linh kiện. Bạn sẽ không bao giờ nhìn thấy hai điều cùng nhau.

Nhưng, một số nhà phát triển đã đọc về DRY và quyết định anh ta sẽ xóa mọi gợi ý sao chép khỏi mã. Vì vậy, ông gọi tất cả mọi thứ là một sản phẩm và không cần phải xác định các lĩnh vực đó. Chúng được định nghĩa trong Sản phẩm và mọi thứ cần các trường đó có thể xuất phát từ Sản phẩm.

Tôi không thể nói điều này đủ mạnh: Đây không phải là một ý tưởng tốt.

DRY không phải là về việc loại bỏ sự cần thiết cho các thuộc tính chung. Nếu một đối tượng không phải là Sản phẩm, đừng gọi đó là Sản phẩm. Nếu Sản phẩm không YÊU CẦU Mô tả, về bản chất nó là Sản phẩm, không xác định Mô tả là Thuộc tính của Sản phẩm.

Cuối cùng, chúng tôi đã có các Thành phần mà không có mô tả. Vì vậy, chúng tôi bắt đầu có Mô tả null trong các đối tượng. Không vô giá trị. Vô giá trị. Luôn luôn. Đối với bất kỳ Sản phẩm nào thuộc loại X ... hoặc mới hơn Y ... và N.

Đây là một vi phạm nghiêm trọng của Nguyên tắc thay thế Liskov.

DRY là về việc không bao giờ đặt mình vào vị trí mà bạn có thể sửa lỗi ở một nơi và quên làm điều đó ở một nơi khác. Điều này sẽ không xảy ra vì bạn có hai đối tượng có cùng thuộc tính. Không bao giờ. Vì vậy, đừng lo lắng về nó.

Nếu bối cảnh của các lớp làm cho rõ ràng rằng bạn nên, điều này sẽ rất hiếm, sau đó và chỉ sau đó bạn nên xem xét một lớp cha mẹ phổ biến. Nhưng, nếu đó là thuộc tính, hãy xem xét lý do tại sao bạn không sử dụng giao diện thay thế.


4

Kế thừa là cách để đạt được hành vi đa hình. Nếu các lớp của bạn không có bất kỳ hành vi nào, thì thừa kế là vô ích. Trong trường hợp này, tôi thậm chí sẽ không xem chúng là các đối tượng / lớp, mà là các cấu trúc thay thế.

Nếu bạn muốn có thể đặt các cấu trúc khác nhau trong các phần khác nhau trong hành vi của mình, thì hãy xem xét các giao diện với các phương thức hoặc thuộc tính get / set, như IHasName, IHasAge, v.v. Điều này sẽ giúp thiết kế sạch hơn nhiều và cho phép bố cục tốt hơn loại nghiên cứu.


3

Đây không phải là những gì giao diện dành cho? Các lớp không liên quan với các chức năng khác nhau nhưng với một thuộc tính tương tự.

IName = Interface(IInterface)
  function Getname : String;
  property Name : String read Getname
end;

Class Dog(InterfacedObject, IName)
private
  FName : String;
  Function GetName : String;
Public
  Name : Read Getname;
end;

Class Movie(InterfacedObject, IName)
private
  FCount;
  FName : String;
  Function GetName : String;
Public
  Name : String Read Getname;
  Count : read FCount; 
end;

1

Câu hỏi của bạn dường như được đóng khung trong ngữ cảnh của một ngôn ngữ không hỗ trợ nhiều kế thừa nhưng không chỉ định cụ thể điều này. Nếu bạn đang làm việc với một ngôn ngữ hỗ trợ nhiều kế thừa, điều này trở nên dễ dàng giải quyết chỉ với một mức độ kế thừa duy nhất.

Dưới đây là một ví dụ về cách giải quyết vấn đề này bằng cách sử dụng nhiều kế thừa.

trait Nameable { String name; }
trait Describable { String description; }
trait Countable { Integer count; }
trait HasImageUrl { String imageUrl; }
trait Living { String age; }

class A : Nameable, Describable;
class B : Nameable, Describable, Countable;
class C : Nameable, Describable, Countable, HasImageUrl;
class D : Nameable, Countable;
class E : Nameable, Countable, HasImageUrl, Living;
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.