2009-05-01 36 views
15

Có một quy tắc chung về thiết kế OO mà bạn nên mô hình là một mối quan hệ sử dụng thừa kế và có mối quan hệ bằng cách sử dụng ngăn chặn/tập hợp và chuyển tiếp/ủy quyền. Điều này càng bị thu hẹp bởi lời khuyên từ GoF rằng bạn thường nên ngăn chặn sự thừa kế, có lẽ, nếu bạn có thể tạo ra một trường hợp mạnh mẽ cho một trong một tình huống cụ thể, vấn đề thừa kế đôi khi có thể gây ra.DRY so với "thích ngăn chặn hơn thừa kế"

Tôi hiểu lý do đằng sau suy nghĩ này và tôi không nhất thiết không đồng ý với điều đó. Tuy nhiên, khi tôi thấy một lớp với số điểm của các phương thức, mỗi phương thức chỉ chuyển tiếp đến một số biến mẫu, tôi thấy một dạng sao chép mã. Mã trùng lặp, theo ý kiến ​​của tôi, mùi mã cuối cùng. Reimplementing một giao thức rất lớn của phương pháp chỉ vì mối quan hệ giữa hai lớp học là không đúng là-một có vẻ như quá mức cần thiết. Đó là mã bổ sung, không cần thiết được thêm vào hệ thống, mã mà bây giờ cần được kiểm tra và ghi lại giống như bất kỳ phần nào khác của hệ thống - mã mà bạn có thể sẽ không phải viết nếu bạn vừa kế thừa.

Chi phí tôn trọng hiệu trưởng thừa kế có bao giờ vượt quá lợi ích của nó không?

+0

Bạn có thể đưa ra ví dụ cụ thể hơn không? Đó là một chút quá trừu tượng để bình luận mà không giả định những gì bạn có ý nghĩa.Ví dụ: –

+0

Ví dụ, bạn muốn hành vi giống như HashTable trong một lớp, nhưng lớp không đủ nghiêm ngặt giống như một HashTable để biện minh cho kế thừa từ nó theo quy tắc là-a. Nhưng có nó chứa một HashTable sẽ dẫn đến reimplementing một phần lớn HashTable của phương pháp, hầu hết trong số đó sẽ chỉ gọi phương pháp tương tự trên biến dụ có chứa HashTable. –

+0

Việc tôi thực hiện các vấn đề thừa kế là có các tác dụng phụ không mong muốn bằng cách kế thừa từ các lớp học. Bằng cách kế thừa, các phụ thuộc được tạo ra và điều đó sẽ ảnh hưởng đến khả năng bảo trì tổng thể của hệ thống. Điều này rõ ràng không phải là một vấn đề lớn nếu hệ thống phân cấp thừa kế là nông, nhưng nếu nó thực sự sâu, nó có thể gây ra vấn đề. – Min

Trả lời

19

Chi phí của hầu hết mọi thứ có thể lớn hơn lợi ích của nó. Có khó khăn và nhanh chóng, không có quy tắc ngoại lệ sẽ luôn luôn giúp bạn gặp rắc rối với sự phát triển. Nói chung, một ý tưởng tồi khi sử dụng (nếu ngôn ngữ/thời gian chạy của bạn hỗ trợ nó) phản ánh để có được quyền truy cập vào các biến không có ý định hiển thị cho mã của bạn. Đây có phải là một thực hành không tốt? Như với mọi thứ,

it depends. 

Thành phần đôi khi linh hoạt hơn hoặc dễ bảo trì hơn là thừa kế thẳng lên? Chắc chắn rồi. Đó là lý do tại sao nó tồn tại. Trong cùng một cách, thừa kế có thể linh hoạt hơn hoặc dễ bảo trì hơn so với kiến ​​trúc thành phần thuần túy. Sau đó, bạn có giao diện hoặc (tùy thuộc vào ngôn ngữ) đa kế thừa.

Không có khái niệm nào là xấu, và mọi người nên ý thức rằng đôi khi sự thiếu hiểu biết hoặc kháng cự của chúng ta thay đổi có thể khiến chúng ta tạo ra các quy tắc tùy ý. vì thế.

+0

Không thể đồng ý hơn. Trong khi nó có vẻ là xu hướng tự nhiên của các kỹ sư muốn tìm "một cách thực sự", thực tế là tất cả mọi thứ phụ thuộc vào bối cảnh của nó, không chỉ là một câu trả lời đúng cho mọi thứ. Và * đó *, là câu trả lời đúng cho mọi thứ. – airportyh

+0

Thực tế "không có viên đạn bạc" thực tế. Mặc dù tôi ủng hộ nguyên tắc KISS (Giữ nguyên đơn giản, ngu ngốc) nếu có thể. – RobS

+0

Tôi muốn nhiều người hơn có thái độ của bạn - hiện tượng "bất thường xấu" chỉ phát triển kể từ khi bạn viết ... –

1

Vâng, những gì bạn đang thấy có một vụ va chạm khủng khiếp của mô hình thiết kế từ các góc khác nhau của vũ trụ: kết hợp/thành phần của GoF tận dụng va chạm với "Luật Demeter".

Tôi đang lưu hồ sơ khi tin rằng, trong bối cảnh sử dụng tổng hợp và thành phần, the Law of Demeter is an anti-pattern.

Trái với nó, tôi tin rằng các cấu trúc như person->brain->performThought() là hoàn toàn đúng và phù hợp.

1

Tôi đồng ý với phân tích của bạn và sẽ ưu tiên thừa kế trong những trường hợp đó. Dường như với tôi rằng đây là một điều tương tự như mù quáng thực hiện những người truy cập ngu ngốc trong một nỗ lực ngây thơ để cung cấp đóng gói. Tôi nghĩ bài học ở đây là đơn giản là không có bất kỳ quy tắc phổ quát nào luôn áp dụng.

0

Điều này có thể không trả lời được câu hỏi của bạn nhưng có điều gì đó luôn khiến tôi bận tâm về Java và Stack của nó. Ngăn xếp, theo phạm vi kiến ​​thức của tôi, phải là cấu trúc dữ liệu vùng chứa rất đơn giản (hoặc có lẽ là đơn giản nhất) với ba hoạt động công khai cơ bản: pop, push và peek. Tại sao bạn muốn trong một Stack insertAt, removeAt, et al. Loại chức năng? (trong Java, Stack kế thừa từ Vector).

Người ta có thể nói rằng, ít nhất bạn không phải ghi lại các phương pháp đó, nhưng tại sao có phương pháp không được cho là ở đó ngay từ đầu?

+0

Vâng, đây thực sự là chủ đề. Nhưng tôi đồng ý rằng giao diện bloated của java.util.Stack là gây phiền nhiễu. –

+1

Đây là một quan sát khá thú vị về một số cạm bẫy của thừa kế mặc dù. Nếu câu trả lời của bạn được lặp lại, điều này sẽ khá tốt về chủ đề. Ngăn xếp Java có thể chứa một Vector và sau đó chỉ có pop, push và peek. Trên một lưu ý khác, tôi nghĩ rằng có cấu trúc dữ liệu linh hoạt hơn trong BCL là một điều tốt. – Min

-1

GoF là văn bản cũ bây giờ .. Tùy thuộc vào môi trường OO bạn nhìn (và thậm chí sau đó hiển nhiên bạn có thể gặp nó trong các phần không thân thiện với OO như C-với-lớp).

Đối với môi trường thời gian chạy, bạn không có lựa chọn nào, thừa kế đơn lẻ. Và bất kỳ cách giải quyết nào cũng cố gắng để vượt qua những hạn chế của nó chỉ là điều đó, dù cho nó có vẻ phức tạp hay 'mát mẻ'.

Một lần nữa bạn sẽ thấy điều này được biểu hiện ở mọi nơi, bao gồm C++ (có khả năng nhất), nơi nó giao tiếp với C callbacks (đủ rộng rãi để khiến mọi người chú ý). Tuy nhiên, C++ cung cấp cho bạn các bản mix-in và các thiết kế dựa trên chính sách với các tính năng mẫu để thỉnh thoảng nó có thể trợ giúp cho kỹ thuật cứng.

Trong khi ngăn chặn có thể cung cấp cho bạn lợi ích của việc chuyển hướng, thừa kế có thể cung cấp cho bạn thành phần dễ truy cập. Chọn bạn độc .. kết hợp cổng tốt hơn nhưng luôn luôn trông giống như vi phạm của DRY trong khi thừa kế có thể dẫn đến tái sử dụng dễ dàng hơn với bộ khác nhau của nhức đầu duy trì tiềm năng.

Vấn đề thực sự là ngôn ngữ máy tính hoặc công cụ mô hình sẽ cung cấp cho bạn một tùy chọn những gì nó phải làm độc lập với sự lựa chọn và như vậy làm cho nó ít bị lỗi của con người; nhưng không có nhiều người mẫu trước khi cho phép máy tính viết chương trình cho họ + không có công cụ tốt nào (Osla chắc chắn không phải là một) hoặc môi trường của họ đang đẩy thứ gì đó ngớ ngẩn như phản chiếu, IoC và những thứ không ... chính nó nói rất nhiều. Có một lần một phần được thực hiện cho một công nghệ COM cổ đại được gọi là Universal Delegator bởi một trong những người chơi Doom tốt nhất xung quanh, nhưng nó không phải là loại phát triển bất cứ ai sẽ áp dụng những ngày này .. Nó yêu cầu giao diện chắc chắn (và không phải là một yêu cầu thực sự khó cho trường hợp chung). Ý tưởng rất đơn giản, nó có thể làm gián đoạn quá trình xử lý .. Chỉ các phần mềm tương tự cung cấp cho bạn tốt nhất của cả hai thế giới và chúng có phần rõ ràng trong các phần kịch bản như JavaScript và lập trình hàm (mặc dù ít có thể đọc hoặc thực hiện được).

3

Bạn phải chọn giải pháp phù hợp cho vấn đề trong tầm tay. Đôi khi kế thừa IS tốt hơn là ngăn chặn, đôi khi không. Bạn phải sử dụng phán quyết của bạn, và khi bạn không thể tìm ra con đường để đi, hãy viết một đoạn mã nhỏ và xem nó xấu đến mức nào. Đôi khi viết một số mã có thể giúp bạn đưa ra quyết định không rõ ràng.

Như mọi khi: câu trả lời đúng tùy thuộc vào nhiều yếu tố mà bạn không thể thực hiện các quy tắc cứng và nhanh.

+0

Tôi hiểu điều đó, nhưng tôi thường thấy mọi người đi xa đến mức đề nghị bạn không bao giờ nên sử dụng (thực hiện) thừa kế. Điều này, với tôi, nghe có vẻ điên rồ. Những người này có thích viết mã bản mẫu/giàn giáo không? –

+3

Một số người làm. Tôi có thể thấy nó. Nó rất thư giãn so với công việc thực tế. – chaos

+1

Tôi đã thấy tồi tệ nhất trong cả hai cách. Một ứng dụng tôi xử lý có một lớp chính kế thừa từ 20 (không, tôi không đùa) chủ yếu là các lớp trừu tượng (nhưng không hoàn toàn). 80-90% các hàm trong lớp đó chỉ gọi một hàm khác. Rất nhiều thừa kế và rất nhiều boilerplate! Tôi cố hết sức không làm việc với ứng dụng đó ... –

0

Ở một mức độ nào đó, đây là câu hỏi về hỗ trợ ngôn ngữ. Ví dụ, trong Ruby, chúng tôi có thể thực hiện một chồng đơn giản mà sử dụng một mảng nội bộ như sau:

class Stack 
    extend Forwardable 
    def_delegators :@internal_array, :<<, :push, :pop 
    def initialize() @internal_array = [] end 
end 

Tất cả chúng ta đang làm ở đây là tuyên bố những gì tập hợp con của chức năng lớp khác của chúng tôi muốn sử dụng. Thực sự không có nhiều sự lặp lại. Heck, nếu chúng ta thực sự muốn tái sử dụng tất cả các chức năng lớp khác, chúng ta có thể thậm chí chỉ rõ rằng nếu không thực sự lặp đi lặp lại bất cứ điều gì:

class ArrayClone 
    extend Forwardable 
    def_delegators(:@internal_array, 
        *(Array.instance_methods - Object.instance_methods)) 
    def initialize() @internal_array = [] end 
end 

Rõ ràng (Tôi hy vọng), đó không phải là mã tôi thường sẽ viết, nhưng tôi nghĩ nó cho thấy rằng nó có thể được thực hiện. Trong các ngôn ngữ không dễ lập trình meta, nó có thể hơi khó hơn nói chung để giữ DRY.

1

Trong khi tôi đồng ý với tất cả những người đã nói "nó phụ thuộc" - và nó cũng phụ thuộc vào ngôn ngữ ở mức độ - tôi ngạc nhiên không ai đề cập đến (trong những từ nổi tiếng của Allen Holub) "extends is evil ". Khi lần đầu tiên tôi đọc bài viết đó, tôi phải thừa nhận rằng tôi có một chút đặt ra, nhưng anh ấy đúng: bất kể ngôn ngữ, mối quan hệ là một dạng khớp nối chặt chẽ nhất có. Chuỗi thừa kế cao là một mẫu chống phân biệt. Vì vậy, trong khi nó không phải để nói rằng bạn nên luôn luôn tránh thừa kế, nó nên được sử dụng ít (đối với các lớp học - giao tiếp thừa kế được khuyến khích). Xu hướng hướng đối tượng-noob của tôi là mô hình hóa mọi thứ như một chuỗi thừa kế, và có, nó làm giảm sự sao chép mã, nhưng với chi phí rất thực tế của khớp nối chặt chẽ, có nghĩa là việc bảo trì không thể tránh khỏi ở đâu đó trên đường.

Bài viết của ông giải thích lý do tại sao thừa kế là khớp nối chặt chẽ, nhưng ý tưởng cơ bản là yêu cầu mọi lớp con (và cháu) phụ thuộc vào việc thực hiện của lớp tổ tiên. "Lập trình cho giao diện" là một chiến lược nổi tiếng để giảm độ phức tạp và hỗ trợ phát triển nhanh. Bạn không thể thực sự lập trình cho giao diện của một lớp cha bởi vì cá thể lớp đó.

Mặt khác, việc sử dụng tập hợp/thành phần buộc đóng gói tốt, làm cho hệ thống trở nên cứng nhắc hơn nhiều. Nhóm mã có thể tái sử dụng đó thành một lớp tiện ích, liên kết với nó với một mối quan hệ có sẵn và lớp khách hàng của bạn hiện đang tiêu thụ một dịch vụ được cung cấp theo một hợp đồng. Bây giờ bạn có thể cấu trúc lại lớp tiện ích thành nội dung trái tim của bạn; miễn là bạn tuân theo giao diện, lớp khách hàng của bạn có thể vẫn không biết gì về sự thay đổi, và (quan trọng) nó không cần phải được biên dịch lại.

Tôi không đề xuất điều này như một tôn giáo, chỉ là cách thực hành tốt nhất. Họ có nghĩa là để bị phá vỡ khi cần thiết, tất nhiên, nhưng nói chung có một lý do tốt mà "tốt nhất" là trong thuật ngữ đó.

0

Bạn có thể xem xét việc triển khai mã "trang trí" trong lớp cơ sở trừu tượng (theo mặc định) chuyển tiếp tất cả các cuộc gọi phương thức đến đối tượng được chứa. Sau đó, phân lớp trang trí trừu tượng và ghi đè/thêm các phương thức nếu cần.

abstract class AbstractFooDecorator implements Foo { 
    protected Foo foo; 

    public void bar() { 
     foo.bar(); 
    } 
} 

class MyFoo extends AbstractFooDecorator { 
    public void bar() { 
     super.bar(); 
     baz(); 
    } 
} 

Điều này ít nhất loại bỏ sự lặp lại mã "chuyển tiếp", nếu bạn có nhiều lớp học bao gồm một loại cụ thể.

Để biết hướng dẫn có hữu ích hay không, tôi cho rằng nên đặt trọng tâm vào từ "thích". Rõ ràng sẽ có trường hợp mà nó làm cho cảm giác hoàn hảo để sử dụng thừa kế. Dưới đây là example of when inheritance should not have been used:

Lớp Hashtable được tăng cường trong JDK 1.2 để bao gồm một phương thức mới, mục nhậpSet, hỗ trợ xóa các mục nhập khỏi Hashtable. Lớp Nhà cung cấp không được cập nhật để ghi đè phương pháp mới này. Sự giám sát này cho phép kẻ tấn công bỏ qua kiểm tra SecurityManager được thực thi trong Provider.remove, và để xóa ánh xạ nhà cung cấp bằng cách đơn giản gọi phương thức Hashtable.entrySet.

Ví dụ nêu bật rằng thử nghiệm vẫn được yêu cầu cho các lớp trong mối quan hệ thừa kế, trái với ý nghĩa rằng chỉ cần duy trì/kiểm tra mã kiểu "đóng gói" - chi phí duy trì một lớp kế thừa từ người khác có thể không rẻ như lần đầu tiên xuất hiện.

+0

Quá xấu không có cách nào trong vb.net hoặc C# để khai báo rằng một lớp là một trình trang trí. Ít nhất với các giao diện, ngôn ngữ có thể cho phép một thứ như vậy được áp dụng một cách rộng rãi mà không mơ hồ, chỉ được cung cấp một phương tiện xác định rằng một tham số kiểu là một giao diện. – supercat

Các vấn đề liên quan