2012-04-30 30 views
5

Tôi là người tin tưởng mạnh mẽ vào triết lý thiết kế sau:Thiết kế giao diện API tốt hơn để chuyển cấu trúc từ một lớp này sang lớp khác

1> Dịch vụ phải được triển khai càng gần nơi dữ liệu được lưu trữ.

2> Getter và Setter là điều ác và cần được sử dụng cẩn thận.

Tôi không nên tranh luận ở trên hai đối số ở đây và cho rằng chúng có các cạnh của chúng.

Đây là thách thức mà tôi đang gặp phải. Tôi có hai lớp (ví dụ: AComputerA) trong đó Máy tính cung cấp một số dịch vụ cho A và A giữ tất cả các thành viên dữ liệu cơ bản.

Sự thật: Tôi không được phép kết hợp AComputer bên trong A do thiết kế hệ thống. Tôi biết, nó đã phá vỡ quan điểm của tôi 1> nơi tính toán nên ở lại với dữ liệu.

Khi truyền dữ liệu từ A đến AComputer, chúng tôi phải vượt qua 10 (khoảng) tham số riêng lẻ và do đó tốt hơn là thiết kế cấu trúc để thực hiện điều đó. Hầu hết dữ liệu được lưu trữ trong AComputer là bản sao trực tiếp các dữ liệu được lưu trữ trong A. Chúng tôi đã chọn lưu trữ các dữ liệu đó bên trong AComputer vì các chức năng khác trong AComputer cũng cần các biến đó.

Dưới đây là câu hỏi (tôi yêu cầu cho thực hành tốt nhất xem xét bảo trì API & sửa đổi):

1> chúng ta nên xác định đèo cấu trúc PassData ở đâu?

2> Chúng tôi có nên cung cấp getter/setter cho cấu trúc PassData không?

Tôi đã cung cấp mã mẫu như sau để minh họa chi tiết câu hỏi của tôi. Tốt nhất là tôi có thể tìm thấy một API nguồn mở thực sự đã giải quyết cùng một vấn đề để tôi có thể học hỏi từ nó.

Nếu bạn xem riêng tư PassData m_data; được xác định trong lớp AComputer, tôi thực hiện điều này với mục đích. Nói cách khác, nếu chúng tôi thay đổi triển khai cơ bản của AComputer, chúng tôi có thể thay thế PassData m_data; bằng các biến riêng lẻ hoặc một biến nào khác nhưng KHÔNG phá vỡ giao diện của PassData. Vì vậy, trong thiết kế này, tôi KHÔNG cung cấp một getter/setter cho cấu trúc PassData.

Cảm ơn bạn

class AComputer 
{ 
public: 
    struct PassData 
    { // int type just used as an illustration. Real data has different types, 
     // such as double, data, string, enum, etc. 
     // Note: they are not exact copies of variables from A but derived from them 
     int m_v1; 
     // from m_v1 to m_v10 
     //... 
     int m_v10; 
    }; 

    // it is better to store the passed-in data since other functions also need it. 
    AComputer(const PassData& pd) : m_data(pd) {} 

    int GetCombinedValue() const 
    { /* This function returns a value based the passed-in struct of pd */ } 

private: 
    PassData m_data;  
}; 

class A 
{ 
private: 
    int m_i1; 
    // from m_i1 to m_i10 
    // ... 
    int m_i10; 
    // from m_i11 to m_i20 
    // ... 
    int m_i20; 

    boost::shared_ptr<AComputer> m_pAComputer; 

public: 
    A() 
    { 
     AComputer::PassData aData; 
     // populate aData ... 
     m_pAComputer = boost::shared_ptr<AComputer>(new AComputer(aData)); 
    } 

    int GetCombinedValue() const 
    { 
     return m_pAComputer->GetCombinedValue(); 
    } 
}; 
+0

Các đối số có liên quan thực sự tất cả 'int' (hoặc tất cả một loại bất kỳ) không? Nếu vậy, tôi nghĩ rằng tôi chỉ cần vượt qua một 'std :: vector '. Nếu bạn muốn truy cập chúng theo tên, tôi định nghĩa một enum trong 'AComputer' cung cấp tên cho các subscript, vì vậy bạn có thể sử dụng' argument [m_i1] 'thay vì' argument [0] '. –

+0

Điểm tốt. Các đối số có đầy đủ các loại khác nhau, chẳng hạn như int, double, string, date, etc. Tôi sẽ cập nhật OP của tôi để xóa bỏ sự nhầm lẫn. – q0987

+3

Cái gì ?! Bạn có hai triết lý thiết kế, và vì một lý do nào đó * có * để phá vỡ số 1. Bây giờ bạn hỏi chúng tôi xem bạn có nên phá vỡ số 2 không? Tôi đã bỏ lỡ điều gì ở đây? –

Trả lời

0

Bạn có thể xem xét refactoring để sử dụng một đối tượng mẫu - mục đích duy nhất của đối tượng này sẽ được chứa các tham số cho các lời gọi phương thức. Để cụ thể hơn: http://sourcemaking.com/refactoring/introduce-parameter-object

+0

Tôi có ý tưởng tương tự trong OP. Như bạn có thể thấy, chúng tôi đã giới thiệu một lớp 'PassData' được sử dụng để truyền dữ liệu nhóm từ A đến Máy tính. Đối với tôi, liên kết không cung cấp thông tin mới có thể cải thiện mã hiện tại. -Thx – q0987

11

Tôi nghĩ rằng đó là tốt hơn làm rõ một vài điểm trước khi bắt đầu, bạn nói:

Nếu bạn nhìn vào m_data PassData tư nhân; được định nghĩa trong máy tính lớp học, tôi làm điều này trong mục đích. Nói cách khác, nếu chúng ta thay đổi việc triển khai thực hiện cơ bản của Máy tính, chúng ta có thể thay thế mata dữ liệu PassData; với biến riêng lẻ hoặc một biến nào khác nhưng KHÔNG phá vỡ giao diện của PassData.

Điều này không đúng, PassData là một phần của giao diện của bạn! Bạn không thể thay thế PassData mà không vi phạm mã máy khách, vì bạn yêu cầu PassData trong hàm tạo của máy tính. PassData không phải là chi tiết thực hiện, nhưng nó là giao diện thuần túy.

điểm thứ hai mà cần làm rõ:

2> Getter và Setter là ác và nên được sử dụng một cách cẩn thận.

Đúng! Nhưng bạn nên biết rằng một POD (Plain-Old-Data struct) thậm chí còn tồi tệ nhất. Ưu điểm duy nhất của việc sử dụng POD thay vì lớp với getter và setter là bạn lưu các rắc rối để viết các hàm. Nhưng vấn đề thực sự vẫn còn mở, giao diện của lớp bạn quá cồng kềnh và nó sẽ rất khó duy trì.

Thiết kế luôn luôn là một sự đánh đổi giữa yêu cầu khác nhau:

Một cảm giác sai lầm về tính linh hoạt

thư viện của bạn được phân phối và rất nhiều mã đang sử dụng lớp học của bạn. Trong trường hợp này, thay đổi về PassData sẽ rất ấn tượng. Nếu bạn có thể trả một mức giá nhỏ trong thời gian chạy, bạn có thể làm cho giao diện của bạn linh hoạt. Ví dụ: hàm tạo của Máy tính sẽ là:

AComputer(const std::map<std::string,boost::any>& PassData); 

Hãy xem tăng :: bất kỳ here. Bạn cũng có thể cung cấp factory cho bản đồ, để giúp người dùng dễ dàng tạo bản đồ.

Pro

  • Nếu bạn không yêu cầu một lĩnh vực nữa mã là không thay đổi.

Nhược điểm

  • nhỏ giá thời gian chạy.
  • Mất kiểm tra loại trình biên dịch an toàn.
  • Nếu chức năng của bạn yêu cầu trường bắt buộc khác, bạn vẫn gặp sự cố. Mã máy khách sẽ biên dịch nhưng nó sẽ không hoạt động chính xác.

Nhìn chung giải pháp này là không tốt, vào cuối cùng nó chỉ là một phiên bản ưa thích của bản gốc.

Chiến lược Pattern

struct CalculateCombinedValueInterface 
{ 
    int GetCombinedValue()=0; 
    virtual ~CalculateCombinedValueInterface(){} 
}; 

class CalculateCombinedValueFirst : CalculateCombinedValueInterface 
{ 
    public: 
     CalculateCombinedValueFirst(int first):first_(first){} 
     int GetCombinedValue(); //your implementation here 
    private: 
     //I used one field but you get the idea 
     int first_; 
}; 

Mã khách hàng sẽ là:

CalculateCombinedValueFirst* values = new CalculateCombinedValueFirst(42); 

boost::shared_ptr<CalculateCombinedValueInterface> data(values); 

Bây giờ, nếu bạn đang đi để sửa đổi mã của bạn, bạn không nên chạm vào giao diện đã được triển khai. Giải pháp hướng đối tượng cho điều này là cung cấp một lớp mới kế thừa từ lớp trừu tượng.

class CalculateCombinedValueSecond : CalculateCombinedValueInterface 
{ 
    public: 
     CalculateCombinedValueFirst(int first,double second) 
      :first_(first),second_(second){} 
     int GetCombinedValue(); //your implementation here 
    private: 
     int first_; 
     double second_; 
}; 

Khách hàng sẽ quyết định nâng cấp lên lớp mới hay ở lại với phiên bản hiện tại.

Pro

  • Cải thiện giao diện của bạn mà không cần mã phá vỡ client.
  • Bạn không chạm vào mã hiện có, nhưng bạn giới thiệu chức năng mới trong một tệp mới.
  • Bạn có thể muốn sử dụng template method design pattern nếu bạn muốn kiểm soát chi tiết nhỏ hơn.

Nhược điểm

  • Overhead của việc sử dụng các chức năng ảo (về cơ bản vài pico giây!)
  • Bạn không thể phá vỡ mã hiện. Bạn phải rời khỏi giao diện hiện tại bị ảnh hưởng và thêm một lớp mới để mô hình hóa hành vi khác nhau.

Số thông số

Nếu bạn có một bộ mười thông số đầu vào trong một chức năng, rất có khả năng rằng những giá trị có liên quan một cách logic. Bạn có thể thu thập một số các giá trị này trong các lớp. Các lớp đó có thể được kết hợp trong một lớp khác mà nó sẽ là đầu vào của hàm của bạn. Thực tế là bạn có 10 (hoặc nhiều hơn!) Thành viên dữ liệu trong một lớp nên rung chuông.

Các single responsibility principle nói:

Không bao giờ nên có nhiều hơn một lý do cho một lớp học để thay đổi.

Hệ quả của nguyên tắc này là: lớp học của bạn phải nhỏ. Nếu lớp học của bạn có 20 thành viên dữ liệu rất có khả năng bạn sẽ tìm thấy rất nhiều lý do để thay đổi nó.

Kết luận

Sau khi bạn đã cung cấp một giao diện (bất kỳ loại giao diện) cho khách hàng bạn không thể thay đổi nó (một ví dụ điển hình là tất cả các tính năng Không dùng nữa trong C++ mà trình biên dịch cần phải thực hiện trong nhiều năm qua). Hãy chú ý đến giao diện mà bạn đang cung cấp ngay cả giao diện ngầm. Trong ví dụ của bạn, PassData không phải là chi tiết thực hiện nhưng nó là một phần của giao diện lớp.

Số lượng tham số là tín hiệu thiết kế của bạn cần được xem xét. Rất khó thay đổi một lớp học lớn. Các lớp của bạn nên nhỏ và chỉ phụ thuộc vào các lớp khác chỉ thông qua một giao diện (lớp trừu tượng trong C++ lóng).

Nếu lớp học của bạn là:

1) nhỏ và chỉ với một lý do để thay đổi

2) có nguồn gốc từ một lớp trừu tượng

3) các lớp khác đề cập đến nó sử dụng một con trỏ đến lớp trừu tượng

Mã của bạn có thể được thay đổi dễ dàng (nhưng giao diện đã cung cấp phải được giữ nguyên).

Nếu bạn không đáp ứng tất cả các yêu cầu này, bạn sẽ gặp rắc rối.

LƯU Ý: yêu cầu 2) và 3) có thể thay đổi nếu thay vì cung cấp đa hình động, thiết kế đang sử dụng đa hình tĩnh.

+0

Hãy để tôi trả lời các câu hỏi của bạn từng người một. Point1> Nếu tôi quyết định thay đổi cài đặt bên trong của 'Máy tính ', tôi không phải thay đổi' PassData'. Method1> thay đổi/thay thế định nghĩa của biến 'm_data' (nghĩa là nó có thể trỏ đến một cấu trúc mới). Method2> Bạn đã đưa ra một ví dụ tốt về cách cung cấp các hàm mới mà không vi phạm mã hiện có. Tương tự, chúng ta có thể giới thiệu 'PassDataV2' ở đây mà không phá vỡ giao diện hiện tại. Đó là lý do chính tại sao tôi nói "Tôi không biết liệu getter/setter cho PassData là cần thiết". 'Máy tính 'có hai cấp độ để thích nghi với những thay đổi mới. – q0987

+0

Point2> Tôi hoàn toàn đồng ý với bạn. Trong thực tế, vấn đề cốt lõi ở đây là cấu trúc truyền dữ liệu rất lớn (10 ~ 20). Lớp được sử dụng để tính toán mô hình toán học phức tạp mà không cần nhiều tham số khác nhau để làm cho nó hoạt động. Mối quan tâm chính của câu hỏi là làm thế nào/nơi chúng ta nên xác định rằng 'PassData' để thực hành tốt và bảo trì mã. -Cảm ơn bạn – q0987

0

Trong một thiết kế lớp bình thường, tất cả các chức năng thành viên có con trỏ này thông qua như là một tham số ngầm để họ có thể truy cập vào các thành viên dữ liệu:

// Regular class 
class SomeClass 
{ 
public: 
    // will be name-mangled by the compiler as something like: 
    // void SomeClass_getValue(const SomeClass*) const; 
    void getValue() const 
    { 
     return value_; // actually: return this->value_; 
    } 

private: 
    int value_; 
}; 

Bạn nên bắt chước này càng nhiều càng tốt. Nếu vì một số lý do bạn không được phép hợp nhất các lớp Máy tính và A thành một lớp sạch, điều tốt nhất tiếp theo sẽ là để cho Máy tính điện tử lấy con trỏ đến A làm thành viên dữ liệu. Trong mọi chức năng thành viên của Máy tính, bạn sẽ phải sử dụng rõ ràng các hàm getter/setter của A để truy cập các thành viên dữ liệu có liên quan.

class AComputer 
{ 
public: 
    AComputer(A* a): p_(a) {} 

    // this will be mangled by the compiler to something like 
    // AComputer_GetCombinedValue(const Acomputer*) const; 
    int GetCombinedValue() const 
    { 
     // in a normal class it would be: return m_i1 + m_i2 + ... 
     // which would actually be: return this->m_i1 + this->m_i12 + ... 
     // the code below actually is: return this->p_->m_i1 + this->p_->m_i2 + ... 
     return p_->get_i1() + p_->get_i2() + ...  
    } 

private: 
    class A; 
    A* p_; 
}; 

class A 
{ 
public: 
    // setters and getters 

private: 
    // data only, NO pointer to AComputer object 
} 

Vì vậy, trong thực tế, bạn đã tạo ra một cấp thêm gián tiếp tạo ra ảo giác cho người dùng rằng AComputer và A là một phần của sự trừu tượng tương tự.

+0

Đây chính là cách mã di sản thực hiện. Lớp kết thúc với 20 ~ 40 hàm chỉ có chức năng cung cấp getter/setter. Tôi cũng đã xem xét để sử dụng bạn bè nhưng bạn bè giới thiệu vấn đề lớp khớp nối mặc dù. – q0987

+0

@ q0987 Nếu bạn nghĩ về 'Máy tính' về mặt giao thoa, và 'A' về mặt thực hiện, đây là điều tốt nhất bạn có thể làm. Tạo một lớp khác PassData chỉ đơn giản là che khuất ý định của bạn bởi vì sau đó bạn có một mức độ khác. Các tác giả của mã di sản của bạn nên đã thực hiện tích hợp các lớp học. – TemplateRex

+0

phương pháp đề xuất của bạn giới thiệu sự phụ thuộc vòng tròn và tôi cố gắng tránh. Cũng không giới thiệu PassData, hàm tạo sẽ kết thúc với ~ 10 tham số pass-in. – q0987

0

Sử dụng PassData thay vì 10 đối số là tốt nếu bạn có toàn quyền kiểm soát tất cả các máy khách AComputer. Nó có hai ưu điểm: bạn cần phải thực hiện ít thay đổi hơn khi bạn thêm một phần dữ liệu khác để truyền và bạn có thể sử dụng gán cho các thành viên cấu trúc trên trang người gọi để làm cho ý nghĩa của từng "đối số" rõ ràng.

Tuy nhiên, nếu những người khác sẽ sử dụng Máy tính, sử dụng PassData có một nhược điểm nghiêm trọng. Nếu không có nó, khi bạn thêm đối số thứ 11 vào hàm tạo máy tính, trình biên dịch sẽ phát hiện lỗi cho người dùng không cập nhật danh sách đối số thực tế. Nếu bạn thêm thành viên thứ 11 vào PassData, trình biên dịch sẽ âm thầm chấp nhận cấu trúc mà thành viên mới là thùng rác, hoặc, trong trường hợp tốt nhất, bằng không.

Theo tôi, nếu bạn sử dụng PassData, có getters và setters sẽ là quá mức cần thiết. "Tiêu chuẩn mã hóa C++" của Sutter và Alexandresku đồng ý với điều đó. Tiêu đề của mụC# 41 là: "làm cho các thành viên dữ liệu riêng tư, ngoại trừ các tập hợp hành vi (các cấu trúc kiểu C)" (nhấn mạnh là của tôi).

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