2012-07-10 8 views
16

Giáo sư C++ của chúng tôi đã đề cập rằng việc sử dụng kết quả của toán tử-> làm đầu vào vào một toán tử khác-> được coi là kiểu xấu.Tại sao foo-> bar-> foobar được coi là kiểu xấu? Và làm thế nào để tránh mà không cần thêm mã?

Vì vậy, thay vì viết:

return edge->terminal->outgoing_edges[0]; 

Ông muốn:

Node* terminal = edge->terminal; 
return terminal->outgoing_edges[0]; 
  1. Tại sao điều này được coi là phong cách xấu?
  2. Làm cách nào tôi có thể cấu trúc lại chương trình của mình để tránh 'kiểu xấu' mà còn tránh thêm dòng mã được tạo theo đề xuất trên?
+15

Tôi chưa từng nghe điều đó. Lợi ích duy nhất tôi có thể thấy là nếu bạn nhận được một lỗi từ một con trỏ null bạn có thể nói từ số dòng mà dereference nó được, và nó dễ dàng hơn để kiểm tra con trỏ trung gian trong một trình gỡ lỗi. Nhưng nếu có một nguy cơ của một dereference null bạn nên kiểm tra một cách rõ ràng cho nó trước khi dòng thứ hai. Không, tôi không thể thấy lý do chính đáng. – Rup

+1

Dễ dàng gỡ lỗi một điều. Nó đơn giản hơn để kiểm tra nếu thiết bị đầu cuối là null. –

+0

Có thể vì điều đó sẽ giúp ích khi bạn có một con trỏ rỗng và đã cố gắng tìm ra cái nào.Bằng cách này, khi bạn hiểu được dòng lỗi (từ trình gỡ lỗi chẳng hạn), bạn sẽ biết đó là con trỏ nào) – Mohammad

Trả lời

12

Bạn nên hỏi giáo sư về lý do tại sao anh ấy coi đó là kiểu xấu. Tôi không. Tuy nhiên tôi sẽ xem xét sự thiếu sót của ông về const trong tuyên bố của thiết bị đầu cuối là phong cách xấu.

Đối với một đoạn mã như vậy, có thể không phải là kiểu xấu. Tuy nhiên hãy xem xét điều này:

void somefunc(Edge *edge) 
{ 
    if (edge->terminal->outgoing_edges.size() > 5) 
    { 
     edge->terminal->outgoing_edges.rezize(10); 
     edge->terminal->counter = 5; 
    } 
    ++edge->terminal->another_value; 
} 

Điều này bắt đầu trở nên khó sử dụng - rất khó đọc, rất khó để viết (tôi đã viết khoảng 10 lỗi khi nhập). Và nó đòi hỏi rất nhiều đánh giá của nhà điều hành-> trên 2 lớp đó. OK nếu nhà điều hành là tầm thường, nhưng nếu nhà điều hành làm bất cứ điều gì thú vị, nó sẽ kết thúc làm rất nhiều công việc.

Vì vậy, có 2 câu trả lời có thể:

  1. Maintanability
  2. Hiệu quả

Và một đoạn đơn như vậy, bạn không thể tránh dòng thêm. Trong một cái gì đó như trên, nó đã dẫn đến ít đánh máy.

+6

Với mã này, tôi chắc chắn thấy lợi ích trong việc khai báo con trỏ đầu tiên bởi vì nó được sử dụng nhiều lần trong cùng một khối mã. Như một quy tắc chung, nếu tôi 'sử dụng' một cái gì đó nhiều hơn một lần trong một khối mã tôi khai báo nó như là một biến, và điều này rơi theo quy tắc đó. Vì vậy, có vẻ như trong ví dụ đơn giản của tôi nó có lẽ không phải là phong cách xấu. Nhưng nếu mã là phức tạp hơn, nó sẽ được. – HorseloverFat

+2

Typo? "edge-> terminal-counter" –

+1

Tôi nghỉ ngơi trường hợp của tôi! (mặc dù tôi thấy nó đã được chỉnh sửa để sửa nó) –

20

Có một số lý do.

Law of Demeter đưa ra lý do cấu trúc (lưu ý rằng mã giáo sư C++ của bạn vẫn vi phạm điều này!). Trong ví dụ của bạn, edge phải biết về terminaloutgoing_edges. Điều đó làm cho nó kết hợp chặt chẽ.

Là một thay thế

class Edge { 
private: 
    Node* terminal; 
public: 
    Edges* outgoing_edges() { 
     return terminal->outgoing_edges; 
    } 
} 

Bây giờ bạn có thể thay đổi việc thực hiện outgoing_edges ở một nơi mà không thay đổi ở khắp mọi nơi. Thành thật mà nói, tôi không thực sự mua nó trong trường hợp của một cấu trúc dữ liệu giống như một đồ thị (nó được kết hợp chặt chẽ, các cạnh và các nút không thể thoát nhau). Điều này sẽ quá trừu tượng trong cuốn sách của tôi.

Có vấn đề về dereference null quá, trong biểu thức a->b->c, nếu b là gì?

+2

Luật Demeter chắc chắn cấm các chuỗi '->', nhưng nó cũng cấm hình thức ưa thích của giáo sư này 'Node * terminal = edge-> terminal; '. Đây có lẽ là một lý do tốt hơn lý do của prof. –

+1

ví dụ này vẫn hoạt động nếu tôi thay đổi 'Thiết bị đầu cuối' thành nút * theo ví dụ mã của tôi? (Tôi đã chỉnh sửa). Tôi không thấy lý do tại sao nó ít kết hợp hơn mặc dù .. không 'Edge' vẫn 'biết' về Node * và outgoing_edges [] trong ví dụ này? – HorseloverFat

+0

Tôi nghĩ Luật Demeter chủ yếu áp dụng cho khả năng kiểm tra. Đối với các đồ thị đối tượng phức tạp hơn, khi thử nghiệm (ví dụ) cạnh, nó sẽ hơi khó chịu khi giả lập một đối tượng 'terminal' phức tạp để trả về một đối tượng' outgoing_edges' được giả lập. Sẽ dễ dàng hơn khi chỉ cần giả lập 'getOutgoingEdges()'. –

3

Hai đoạn mã bạn đã hiển thị là (tất nhiên) giống hệt về mặt ngữ nghĩa. Khi nói đến vấn đề về phong cách, đề nghị bạn chỉ cần làm theo những gì giáo sư của bạn muốn.

Hơi tốt hơn so với đoạn mã thứ hai của bạn sẽ là:

Node* terminal = (edge ? edge->terminal : NULL); 
return (terminal ? terminal->outgoing_edges[0] : NULL); 

Tôi giả định rằng outgoing_edges là một mảng; nếu không, bạn phải kiểm tra xem đó có phải NULL hay trống không.

6

Vâng, tôi không thể chắc chắn về những gì giáo sư của bạn đã làm có nghĩa là, bu tôi có một số suy nghĩ.

Trước hết, mã như thế này có thể là "không thể thấy rõ":

someObjectPointer->foo()->bar()->foobar() 

Bạn không thể thực sự nói là kết quả của foo() là gì, và do đó bạn có thể không thực sự nói trên bar() được gọi là gì. Đó có phải là cùng một đối tượng không? Hoặc có thể là một đối tượng tạm thời của nó đang được tạo ra? Hoặc có thể nó là cái gì khác?

Một điều khác là nếu bạn phải lặp lại mã của mình. Hãy xem xét một ví dụ như thế này:

complexObject.somePart.someSubObject.sections[i].doSomething(); 
complexObject.somePart.someSubObject.sections[i].doSomethingElse(); 
complexObject.somePart.someSubObject.sections[i].doSomethingAgain(); 
complexObject.somePart.someSubObject.sections[i].andAgain(); 

Hãy so sánh nó với ví dụ:

section * sectionPtr = complexObject.somePart.someSubObject.sections[i]; 
sectionPtr->doSomething(); 
sectionPtr->doSomethingElse(); 
sectionPtr->doSomethingAgain(); 
sectionPtr->andAgain(); 

Bạn có thể xem như thế nào ví dụ thứ hai là không chỉ ngắn hơn, nhưng dễ đọc hơn.

Đôi khi chuỗi các chức năng dễ dàng hơn, bởi vì bạn không cần phải bận tâm với những gì họ trở về:

resultClass res = foo()->bar()->foobar() 

vs

classA * a = foo(); 
classB * b = a->bar(); 
classC * c = b->foobar(); 

Một điều được gỡ rối. Rất khó để gỡ lỗi các chuỗi cuộc gọi chức năng dài, bởi vì bạn chỉ đơn giản là không thể hiểu được cái nào trong số chúng gây ra lỗi. Trong trường hợp này, phá vỡ chuỗi sẽ giúp rất nhiều, chưa kể bạn có thể làm một số kiểm tra thêm quá, giống như

classA * a = foo(); 
classB * b; 
classC * c; 
if(a) 
    b = a->bar(); 
if(b) 
    c = b->foobar(); 

Vì vậy, để tóm tắt, bạn không thể nói rằng đó là một "xấu" hay "tốt "phong cách nói chung. Bạn phải xem xét hoàn cảnh của bạn trước.

1

Không nhất thiết phải tốt hơn ở dạng hiện tại của chúng. Tuy nhiên, nếu bạn đã thêm một kiểm tra cho null trong ví dụ thứ hai của bạn, thì điều này sẽ có ý nghĩa.

if (edge != NULL) 
{ 
    Node* terminal = edge->terminal; 
    if (terminal != NULL) return terminal->outgoing_edges[0]; 
} 

Mặc dù thậm chí điều đó vẫn còn tồi vì ai nói rằng outgoing_edges được điền hoặc phân bổ đúng cách. Đặt cược tốt hơn là gọi một hàm có tên là outgoingEdges() thực hiện tất cả các lỗi kiểm tra tốt đẹp cho bạn và không bao giờ khiến bạn hành vi không xác định.