Việc trả về con trỏ tới các đối tượng cấu thành có vi phạm đóng gói không


8

Khi tôi muốn tạo một đối tượng tổng hợp các đối tượng khác, tôi thấy mình muốn cấp quyền truy cập vào các đối tượng bên trong thay vì tiết lộ giao diện cho các đối tượng bên trong với các chức năng thông qua.

Ví dụ: giả sử chúng ta có hai đối tượng:

class Engine;
using EnginePtr = unique_ptr<Engine>;
class Engine
{
public:
    Engine( int size ) : mySize( 1 ) { setSize( size ); }
    int getSize() const { return mySize; }
    void setSize( const int size ) { mySize = size; }
    void doStuff() const { /* do stuff */ }
private:
    int mySize;
};

class ModelName;
using ModelNamePtr = unique_ptr<ModelName>;
class ModelName
{
public:
    ModelName( const string& name ) : myName( name ) { setName( name ); }
    string getName() const { return myName; }
    void setName( const string& name ) { myName = name; }
    void doSomething() const { /* do something */ }
private:
    string myName;
};

Và giả sử chúng ta muốn có một đối tượng Xe bao gồm cả Động cơ và ModelName (điều này rõ ràng là bị chiếm đoạt). Một cách có thể làm như vậy là cấp quyền truy cập cho từng

/* give access */
class Car1
{
public:
    Car1() : myModelName{ new ModelName{ "default" } }, myEngine{ new Engine{ 2 } } {}
    const ModelNamePtr& getModelName() const { return myModelName; }
    const EnginePtr& getEngine() const { return myEngine; }
private:
    ModelNamePtr myModelName;
    EnginePtr myEngine;
};

Sử dụng đối tượng này sẽ trông như thế này:

Car1 car1;
car1.getModelName()->setName( "Accord" );
car1.getEngine()->setSize( 2 );
car1.getEngine()->doStuff();

Một khả năng khác là tạo một chức năng công khai trên đối tượng Xe cho từng chức năng (mong muốn) trên các đối tượng bên trong, như sau:

/* passthrough functions */
class Car2
{
public:
    Car2() : myModelName{ new ModelName{ "default" } }, myEngine{ new Engine{ 2 } } {}
    string getModelName() const { return myModelName->getName(); }
    void setModelName( const string& name ) { myModelName->setName( name ); }
    void doModelnameSomething() const { myModelName->doSomething(); }
    int getEngineSize() const { return myEngine->getSize(); }
    void setEngineSize( const int size ) { myEngine->setSize( size ); }
    void doEngineStuff() const { myEngine->doStuff(); }
private:
    ModelNamePtr myModelName;
    EnginePtr myEngine;
};

Ví dụ thứ hai sẽ được sử dụng như thế này:

Car2 car2;
car2.setModelName( "Accord" );
car2.setEngineSize( 2 );
car2.doEngineStuff();

Mối quan tâm của tôi với ví dụ đầu tiên là nó vi phạm đóng gói OO bằng cách truy cập trực tiếp vào các thành viên tư nhân.

Mối quan tâm của tôi với ví dụ thứ hai là, khi chúng ta đạt đến các cấp cao hơn trong hệ thống phân cấp lớp, chúng ta có thể kết thúc với các lớp "giống như thần" có giao diện công cộng rất lớn (vi phạm "Tôi" trong RẮN).

Điều nào trong hai ví dụ đại diện cho thiết kế OO tốt hơn? Hay cả hai ví dụ đều thể hiện sự thiếu hiểu biết về OO?

Câu trả lời:


5

Tôi thấy mình muốn cấp quyền truy cập vào các đối tượng bên trong thay vì tiết lộ giao diện cho các đối tượng bên trong với các chức năng thông qua.

Vì vậy, tại sao sau đó nó là nội bộ?

Mục tiêu không phải là "tiết lộ giao diện cho đối tượng bên trong" mà là để tạo ra một giao diện mạch lạc, nhất quán, biểu cảm. Nếu chức năng của một đối tượng nội bộ cần được bộc lộ và việc truyền qua đơn giản sẽ thực hiện, sau đó chuyển qua. Thiết kế tốt là mục tiêu, không "tránh mã hóa tầm thường".

Cho phép truy cập vào một đối tượng nội bộ có nghĩa là:

  • Khách hàng phải biết về những người bên trong để sử dụng chúng.
  • Trên đây có nghĩa là sự trừu tượng mong muốn được thổi ra khỏi nước.
  • Bạn trưng ra các phương thức và thuộc tính công khai khác của đối tượng nội bộ, cho phép khách hàng thao tác trạng thái của bạn theo những cách không lường trước được.
  • Khớp nối tăng đáng kể. Bây giờ bạn có nguy cơ phá vỡ mã máy khách, bạn nên sửa đổi đối tượng bên trong, thay đổi chữ ký phương thức hoặc thậm chí thay thế toàn bộ đối tượng (thay đổi loại).
  • Tất cả điều này là lý do tại sao chúng ta có Luật Demeter. Demeter không nói "tốt, nếu nó chỉ đi qua, thì bạn có thể bỏ qua nguyên tắc này."

Lưu ý bên lề: động cơ rất phù hợp với giao diện ContinainableVehicle nhưng nó không liên quan đến DrivableVehicle. Người thợ máy cần biết về động cơ (có thể là tất cả các chi tiết của nó), nhưng người lái xe thì không. (Và hành khách không cần biết về vô lăng)
user253751

2

Tôi không nghĩ rằng nó nhất thiết vi phạm đóng gói để trả về các tham chiếu đến đối tượng được bao bọc, đặc biệt nếu chúng là const. Cả hai std::stringstd::vectorlàm điều này. Nếu bạn có thể bắt đầu thay đổi phần bên trong của đối tượng từ bên dưới mà không thông qua giao diện của nó, điều đó đáng nghi ngờ hơn, nhưng nếu bạn đã có thể làm điều đó một cách hiệu quả với setters, thì dù sao thì đóng gói chỉ là ảo ảnh.

Các container đặc biệt khó phù hợp với mô hình này; thật khó để tưởng tượng một danh sách hữu ích không thể phân tách thành đầu và đuôi. Ở một mức độ nào đó, bạn có thể viết các giao diện như std::find()là trực giao với bố cục bên trong của cấu trúc dữ liệu. Haskell đi xa hơn với các lớp như Có thể gập lại và Traversible. Nhưng đến một lúc nào đó, cuối cùng bạn đã nói rằng mọi thứ bạn muốn phá vỡ đóng gói phải làm bây giờ nằm ​​trong hàng rào đóng gói.


Và đặc biệt nếu tham chiếu là một lớp trừu tượng được mở rộng bằng cách triển khai cụ thể mà lớp của bạn sử dụng; sau đó bạn không tiết lộ việc thực hiện mà chỉ cung cấp một giao diện mà việc triển khai phải hỗ trợ.
Jules
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.