C ++: Lớp nên sở hữu hoặc quan sát sự phụ thuộc của nó?


16

Nói rằng tôi có một lớp Foobarsử dụng (phụ thuộc) lớp Widget. Trong những ngày tốt, Widgetwolud được khai báo là một trường trong Foobarhoặc có thể là một con trỏ thông minh nếu cần hành vi đa hình, và nó sẽ được khởi tạo trong hàm tạo:

class Foobar {
    Widget widget;
    public:
    Foobar() : widget(blah blah blah) {}
    // or
    std::unique_ptr<Widget> widget;
    public:
    Foobar() : widget(std::make_unique<Widget>(blah blah blah)) {}
    (…)
};

Và chúng ta đã sẵn sàng và hoàn thành. Thật không may, ngày nay, những đứa trẻ Java sẽ cười nhạo chúng tôi khi chúng nhìn thấy nó, và đúng, khi nó kết đôi FoobarWidgetcùng nhau. Giải pháp có vẻ đơn giản: áp dụng Dependency Injection để xây dựng các phụ thuộc ra khỏi Foobarlớp. Nhưng sau đó, C ++ buộc chúng ta phải suy nghĩ về quyền sở hữu phụ thuộc. Ba giải pháp xuất hiện trong tâm trí:

Con trỏ độc đáo

class Foobar {
    std::unique_ptr<Widget> widget;
    public:
    Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
    (…)
}

Foobartuyên bố quyền sở hữu duy nhất của Widgetnó được chuyển cho nó. Điều này có những lợi thế sau:

  1. Tác động đến hiệu suất là không đáng kể.
  2. Nó an toàn, vì Foobarkiểm soát trọn đời Widget, vì vậy nó đảm bảo rằng Widgetnó không đột nhiên biến mất.
  3. Nó an toàn Widgetsẽ không bị rò rỉ và sẽ bị phá hủy đúng cách khi không còn cần thiết.

Tuy nhiên, điều này đi kèm với chi phí:

  1. Nó đặt các hạn chế về cách các Widgetthể hiện có thể được sử dụng, ví dụ: không thể sử dụng ngăn xếp được phân bổ Widgets, không Widgetthể chia sẻ.

Con trỏ dùng chung

class Foobar {
    std::shared_ptr<Widget> widget;
    public:
    Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
    (…)
}

Đây có lẽ là Java tương đương gần nhất và các ngôn ngữ được thu gom rác khác. Ưu điểm:

  1. Phổ quát hơn, vì nó cho phép chia sẻ phụ thuộc.
  2. Duy trì sự an toàn (điểm 2 và 3) của unique_ptrgiải pháp.

Nhược điểm:

  1. Lãng phí tài nguyên khi không chia sẻ.
  2. Vẫn yêu cầu phân bổ heap và không cho phép các đối tượng được phân bổ stack.

Con trỏ quan sát đồng bằng ol '

class Foobar {
    Widget *widget;
    public:
    Foobar(Widget *w) : widget(w) {}
    (…)
}

Đặt con trỏ thô bên trong lớp và chuyển gánh nặng sở hữu cho người khác. Ưu điểm:

  1. Đơn giản như nó có thể nhận được.
  2. Phổ thông, chấp nhận bất kỳ Widget.

Nhược điểm:

  1. Không an toàn nữa.
  2. Giới thiệu một thực thể khác chịu trách nhiệm sở hữu cả hai FoobarWidget.

Một số siêu mẫu điên

Ưu điểm duy nhất tôi có thể nghĩ đến là tôi có thể đọc tất cả những cuốn sách mà tôi không tìm thấy thời gian trong khi phần mềm của tôi đang hoạt động;)

Tôi nghiêng về giải pháp thứ ba, vì nó là phổ quát nhất, Foobarsdù sao cũng phải quản lý một cái gì đó , vì vậy quản lý Widgetslà thay đổi đơn giản. Tuy nhiên, sử dụng con trỏ thô làm phiền tôi, mặt khác, giải pháp con trỏ thông minh cảm thấy sai đối với tôi, vì chúng khiến người tiêu dùng phụ thuộc hạn chế cách tạo ra sự phụ thuộc đó.

Tui bỏ lỡ điều gì vậy? Hay chỉ là Dependency Injection trong C ++ không tầm thường? Lớp học nên sở hữu phụ thuộc của nó hay chỉ quan sát chúng?


1
Ngay từ đầu, nếu bạn có thể biên dịch theo C ++ 11 và / hoặc không bao giờ, có con trỏ đơn giản như một cách để xử lý tài nguyên trong một lớp không còn là cách được đề xuất nữa. Nếu lớp Foobar là chủ sở hữu duy nhất của tài nguyên và tài nguyên sẽ được giải phóng, khi Foobar vượt quá phạm vi, std::unique_ptrlà cách để đi. Bạn có thể sử dụng std::move()để chuyển quyền sở hữu tài nguyên từ phạm vi trên sang lớp.
Andy

1
Làm thế nào tôi biết đó Foobarlà chủ sở hữu duy nhất? Trong trường hợp cũ, nó đơn giản. Nhưng vấn đề với DI, như tôi thấy, là khi nó tách lớp khỏi việc xây dựng các phụ thuộc của nó, nó cũng tách nó khỏi quyền sở hữu của các phụ thuộc đó (vì quyền sở hữu gắn liền với xây dựng). Trong các môi trường rác được thu thập như Java, đó không phải là vấn đề. Trong C ++, đây là.
el.pescado

1
@ el.pescado Cũng có vấn đề tương tự trong Java, bộ nhớ không phải là tài nguyên duy nhất. Ngoài ra còn có các tập tin và kết nối chẳng hạn. Cách thông thường để giải quyết vấn đề này trong Java là có một thùng chứa quản lý vòng đời của tất cả các đối tượng chứa trong nó. Do đó, bạn không phải lo lắng về nó trong các đối tượng chứa trong thùng chứa DI. Điều này cũng có thể hoạt động cho các ứng dụng c ++ nếu có cách để bộ chứa DI biết khi nào nên chuyển sang giai đoạn vòng đời nào.
SpaceTrucker

3
Tất nhiên, bạn có thể làm theo cách cũ mà không cần phức tạp và có mã hoạt động và là cách dễ dàng hơn để duy trì. (Lưu ý rằng các chàng trai Java có thể cười, nhưng họ luôn luôn tiến hành tái cấu trúc, vì vậy việc tách rời không chính xác giúp họ nhiều)
gbjbaanb

3
Cộng với việc người khác cười nhạo bạn không phải là lý do để giống họ. Lắng nghe lập luận của họ (khác với "bởi vì chúng tôi đã nói với") và thực hiện DI nếu nó mang lại lợi ích rõ ràng cho dự án của bạn. Trong trường hợp này, hãy nghĩ về mô hình như một quy tắc rất chung, mà bạn phải áp dụng theo cách dành riêng cho dự án - cả ba cách tiếp cận (và tất cả các cách tiếp cận tiềm năng khác mà bạn chưa nghĩ đến) đều có giá trị chúng mang lại một lợi ích tổng thể (nghĩa là có nhiều mặt hơn so với nhược điểm).

Câu trả lời:


2

Tôi có ý định viết bài này như một bình luận, nhưng hóa ra nó quá dài.

Làm thế nào tôi biết đó Foobarlà chủ sở hữu duy nhất? Trong trường hợp cũ, nó đơn giản. Nhưng vấn đề với DI, như tôi thấy, là khi nó tách lớp khỏi việc xây dựng các phụ thuộc của nó, nó cũng tách nó khỏi quyền sở hữu của các phụ thuộc đó (vì quyền sở hữu gắn liền với xây dựng). Trong các môi trường rác được thu thập như Java, đó không phải là vấn đề. Trong C ++, đây là.

Cho dù bạn nên sử dụng std::unique_ptr<Widget>hay std::shared_ptr<Widget>, đó là tùy thuộc vào bạn để quyết định và xuất phát từ chức năng của bạn.

Giả sử bạn có một Utilities::Factory, chịu trách nhiệm tạo các khối của bạn, chẳng hạn như Foobar. Theo nguyên tắc DI, bạn sẽ cần Widgetcá thể, để tiêm nó bằng cách sử dụng hàm tạo Foobarcủa nó, nghĩa là bên trong một trong các Utilities::Factoryphương thức của nó, ví dụ createWidget(const std::vector<std::string>& params), bạn tạo Widget và đưa nó vào Foobarđối tượng.

Bây giờ bạn có một Utilities::Factoryphương thức, tạo ra Widgetđối tượng. Có nghĩa là, phương pháp tôi phải chịu trách nhiệm cho việc xóa nó? Dĩ nhiên là không. Nó chỉ ở đó để làm cho bạn ví dụ.


Hãy tưởng tượng, bạn đang phát triển một ứng dụng, sẽ có nhiều cửa sổ. Mỗi cửa sổ được biểu diễn bằng Foobarlớp, vì vậy trên thực tế các Foobarhoạt động giống như một bộ điều khiển.

Bộ điều khiển có thể sẽ sử dụng một số của bạn Widgetvà bạn phải tự hỏi:

Nếu tôi đi trên cửa sổ cụ thể này trong ứng dụng của mình, tôi sẽ cần những Widget này. Là những vật dụng được chia sẻ giữa các cửa sổ ứng dụng khác? Nếu vậy, có lẽ tôi không nên tạo lại tất cả, vì chúng sẽ luôn trông giống nhau, vì chúng được chia sẻ.

std::shared_ptr<Widget> là con đường để đi

Bạn cũng có một cửa sổ ứng dụng, trong đó chỉ có một cửa sổ được Widgetgắn cụ thể, chỉ có nghĩa là nó sẽ không được hiển thị ở bất kỳ nơi nào khác. Vì vậy, nếu bạn đóng cửa sổ, bạn không cần Widgetbất cứ nơi nào trong ứng dụng của bạn nữa, hoặc ít nhất là ví dụ của nó.

Đó là nơi std::unique_ptr<Widget>để khẳng định ngai vàng của nó.


Cập nhật:

Tôi thực sự không đồng ý với @DominicMcDonnell , về vấn đề trọn đời. Gọi std::movetrên std::unique_ptrhoàn toàn chuyển quyền sở hữu, vì vậy ngay cả khi bạn tạo ra một object Atrong một phương pháp và vượt qua nó để khác object Bnhư một sự phụ thuộc, các object Bbây giờ sẽ chịu trách nhiệm về tài nguyên của object Avà sẽ xóa một cách chính xác nó, khi object Bđi ra khỏi phạm vi.


Ý tưởng chung về quyền sở hữu và con trỏ thông minh là rõ ràng đối với tôi. Tôi dường như không chơi đẹp với ý tưởng về DI. Đối với tôi, Dependency Injection là về việc tách lớp từ các phụ thuộc của nó, nhưng trong trường hợp này, lớp vẫn ảnh hưởng đến cách tạo ra các phụ thuộc của nó.
el.pescado

Ví dụ, vấn đề tôi gặp phải unique_ptrlà thử nghiệm đơn vị (DI được quảng cáo là thân thiện với thử nghiệm): Tôi muốn thử nghiệm Foobar, vì vậy tôi tạo Widget, chuyển nó đến Foobar, tập thể dục Foobarvà sau đó tôi muốn kiểm tra Widget, nhưng trừ khi Foobarbằng cách nào đó, tôi có thể Vì nó đã được yêu cầu bởi Foobar.
el.pescado

@ el.pescado Bạn đang trộn hai thứ lại với nhau. Bạn đang trộn khả năng tiếp cận và quyền sở hữu. Chỉ vì Foobarsở hữu tài nguyên không có nghĩa, không ai khác nên sử dụng nó. Bạn có thể thực hiện một Foobarphương thức như Widget* getWidget() const { return this->_widget.get(); }, nó sẽ trả về cho bạn con trỏ thô mà bạn có thể làm việc với. Sau đó, bạn có thể sử dụng phương thức này làm đầu vào cho các bài kiểm tra đơn vị của mình, khi bạn muốn kiểm tra Widgetlớp.
Andy

Điểm tốt, điều đó có ý nghĩa.
el.pescado

1
Nếu bạn đã chuyển đổi shared_ptr thành unique_ptr, bạn muốn đảm bảo unique_ness như thế nào ???
Aconcagua

2

Tôi sẽ sử dụng một con trỏ quan sát ở dạng tham chiếu. Nó cung cấp một cú pháp tốt hơn nhiều khi bạn sử dụng nó và có lợi thế về ngữ nghĩa mà nó không bao hàm quyền sở hữu, mà một con trỏ đơn giản có thể.

Vấn đề lớn nhất với phương pháp này là thời gian sống. Bạn phải đảm bảo rằng sự phụ thuộc được xây dựng trước và được hủy sau lớp tùy thuộc của bạn. Đó không phải là một vấn đề đơn giản. Sử dụng các con trỏ được chia sẻ (như lưu trữ phụ thuộc và trong tất cả các lớp phụ thuộc vào nó, tùy chọn 2 ở trên) có thể loại bỏ vấn đề này, nhưng cũng đưa ra vấn đề phụ thuộc vòng tròn cũng không tầm thường, và theo tôi thì ít rõ ràng hơn và do đó khó hơn phát hiện trước khi nó gây ra vấn đề. Đó là lý do tại sao tôi không muốn làm điều đó một cách tự động và tự quản lý thời gian sống và trật tự xây dựng. Tôi cũng đã thấy các hệ thống sử dụng cách tiếp cận mẫu ánh sáng, xây dựng một danh sách các đối tượng theo thứ tự chúng được tạo ra và phá hủy chúng theo thứ tự ngược lại, nó không thể đánh lừa được, nhưng nó khiến mọi thứ đơn giản hơn rất nhiều.

Cập nhật

Câu trả lời của David Packer khiến tôi suy nghĩ thêm một chút về câu hỏi. Câu trả lời ban đầu là đúng đối với các phụ thuộc được chia sẻ theo kinh nghiệm của tôi, đây là một trong những lợi thế của việc tiêm phụ thuộc, bạn có thể có nhiều trường hợp sử dụng một thể hiện của một phụ thuộc. Tuy nhiên, nếu lớp của bạn cần có ví dụ riêng của một phụ thuộc cụ thể thì đó std::unique_ptrlà câu trả lời đúng.


1

Trước hết - đây là C ++, không phải Java - và ở đây nhiều thứ khác đi. Người Java không gặp phải những vấn đề đó về quyền sở hữu vì có bộ sưu tập rác tự động giải quyết những vấn đề đó cho họ.

Thứ hai: Không có câu trả lời chung cho câu hỏi này - nó phụ thuộc vào yêu cầu là gì!

Vấn đề về khớp nối FooBar và Widget là gì? FooBar muốn sử dụng một Widget và nếu mọi trường hợp FooBar luôn luôn sẽ có một Widget riêng và cùng một Widget, hãy để nó được ghép nối ...

Trong C ++, bạn thậm chí có thể làm những thứ "kỳ lạ" mà đơn giản là không tồn tại trong Java, ví dụ: có một hàm tạo khuôn mẫu biến đổi (vâng, cũng tồn tại một ... ký hiệu trong Java, cũng có thể được sử dụng trong các hàm tạo, nhưng đó là chỉ cần cú pháp đường để ẩn một mảng đối tượng, điều này thực sự không liên quan gì đến các mẫu biến đổi thực sự!) - thể loại 'Một số mẫu điên siêu dữ liệu':

namespace WidgetFactory
{
    Widget* create(int, int)
    {
        return 0;
    }
    Widget* create(int, bool, long)
    {
        return 0;
    }
}
class FooBar
{
public:
    template < typename ...Arguments >
    FooBar(Arguments... arguments)
        : mWidget(WidgetFactory::create(arguments...))
    {
    }
    ~FooBar()
    {
        delete mWidget;
    }
private:
    Widget* mWidget;
};

FooBar foobar1(10, 12);
FooBar foobar2(51, true, 54L);

Thích nó không?

Tất nhiên, có những lý do khi bạn muốn hoặc cần tách rời cả hai lớp - ví dụ: nếu bạn cần tạo các widget tại một thời điểm trước khi có bất kỳ trường hợp FooBar nào tồn tại, nếu bạn muốn hoặc cần sử dụng lại các widget, ..., hoặc đơn giản bởi vì đối với vấn đề hiện tại, nó phù hợp hơn (ví dụ: nếu Widget là một thành phần GUI và FooBar sẽ / có thể / không phải là một).

Sau đó, chúng tôi quay trở lại điểm thứ hai: Không có câu trả lời chung chung. Bạn cần phải quyết định, cái gì - cho vấn đề thực tế - là giải pháp phù hợp hơn. Tôi thích cách tiếp cận tham chiếu của DominicMcDonnell, nhưng nó chỉ có thể được áp dụng nếu quyền sở hữu sẽ không được FooBar thực hiện (tốt, thực sự, bạn có thể, nhưng điều đó ngụ ý mã rất, rất bẩn ...). Ngoài ra, tôi tham gia câu trả lời của David Packer (câu trả lời có nghĩa là được viết dưới dạng nhận xét - nhưng dù sao cũng là một câu trả lời hay).


1
Trong C ++, không sử dụng classes mà không có gì ngoài các phương thức tĩnh, ngay cả khi đó chỉ là một nhà máy như trong ví dụ của bạn. Điều đó vi phạm các nguyên tắc OO. Sử dụng không gian tên thay thế và nhóm các chức năng của bạn bằng cách sử dụng chúng.
Andy

@DavidPacker Hmm, chắc chắn rồi, bạn nói đúng - Tôi âm thầm cho rằng lớp học có một số nội bộ riêng tư, nhưng điều đó không thể nhìn thấy cũng như không được đề cập ở nơi khác ...
Aconcagua

1

Bạn đang thiếu ít nhất hai tùy chọn khác có sẵn cho bạn trong C ++:

Một là sử dụng phép nội xạ phụ thuộc 'tĩnh' trong đó các phụ thuộc của bạn là các tham số mẫu. Điều này cung cấp cho bạn tùy chọn giữ các phụ thuộc của bạn theo giá trị trong khi vẫn cho phép biên dịch tiêm phụ thuộc thời gian. Các container STL sử dụng phương pháp này để phân bổ và các hàm so sánh và hàm băm chẳng hạn.

Một cách khác là lấy các đối tượng đa hình theo giá trị bằng cách sử dụng một bản sao sâu. Cách truyền thống để làm điều này là sử dụng phương pháp nhân bản ảo, một tùy chọn khác trở nên phổ biến là sử dụng kiểu xóa để tạo ra các loại giá trị hoạt động đa hình.

Lựa chọn nào phù hợp nhất thực sự phụ thuộc vào trường hợp sử dụng của bạn, thật khó để đưa ra câu trả lời chung chung. Nếu bạn chỉ cần đa hình tĩnh mặc dù tôi muốn nói các mẫu là cách tốt nhất để sử dụng C ++.


0

Bạn đã bỏ qua câu trả lời thứ tư có thể, kết hợp mã đầu tiên bạn đã đăng ("ngày tốt" của việc lưu trữ thành viên theo giá trị) với nội dung phụ thuộc:

class Foobar {
    Widget widget;
public:
    Foobar(Widget w) // pass w by value
     : widget{ std::move(w) } {}
};

Mã khách hàng sau đó có thể viết:

Widget w;
Foobar f1{ w }; // default: copy w into f1
Foobar f2{ std::move(w) }; // move w into f2

Việc thể hiện sự ghép nối giữa các đối tượng không nên được thực hiện (hoàn toàn) trên các tiêu chí bạn đã liệt kê (nghĩa là không hoàn toàn dựa trên "điều này tốt hơn cho quản lý trọn đời an toàn").

Bạn cũng có thể sử dụng các tiêu chí khái niệm ("một chiếc xe có bốn bánh" so với "một chiếc xe sử dụng bốn bánh mà người lái phải mang theo").

Bạn có thể có các tiêu chí được áp đặt bởi các API khác (nếu những gì bạn nhận được từ API là trình bao bọc tùy chỉnh hoặc std :: unique_ptr chẳng hạn, các tùy chọn trong mã máy khách của bạn cũng bị hạn chế).


1
Điều này ngăn chặn hành vi đa hình, làm hạn chế nghiêm trọng giá trị gia tăng.
el.pescado

Thật. Tôi chỉ nghĩ là đề cập đến nó vì đây là hình thức tôi sử dụng nhiều nhất (tôi chỉ sử dụng tính kế thừa trong mã hóa cho hành vi đa hình - không sử dụng lại mã, vì vậy tôi không có nhiều trường hợp cần hành vi đa hình) ..
utnapistim
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.