Thành ngữ Pimpl vs Giao diện lớp ảo thuần túy


118

Tôi đã tự hỏi điều gì sẽ khiến một lập trình viên phải chọn thành ngữ Pimpl hoặc lớp ảo thuần túy và kế thừa.

Tôi hiểu rằng thành ngữ ma cô đi kèm với một hướng dẫn bổ sung rõ ràng cho mỗi phương thức công khai và chi phí tạo đối tượng.

Mặt khác, lớp Pure virtual đi kèm với hướng dẫn ngầm (vtable) để triển khai kế thừa và tôi hiểu rằng không có chi phí tạo đối tượng nào.
CHỈNH SỬA : Nhưng bạn sẽ cần một nhà máy nếu bạn tạo đối tượng từ bên ngoài

Điều gì khiến giai cấp ảo thuần túy ít được mong đợi hơn thành ngữ ma cô?


3
Câu hỏi tuyệt vời, chỉ muốn hỏi điều tương tự. Xem thêm boost.org/doc/libs/1_41_0/libs/smart_ptr/sp_techniques.html
Frank

Câu trả lời:


60

Khi viết một lớp C ++, bạn nên nghĩ xem liệu nó sẽ

  1. Một loại giá trị

    Sao chép theo giá trị, bản sắc không bao giờ là quan trọng. Nó thích hợp để trở thành một chìa khóa trong bản đồ std ::. Ví dụ, một lớp "chuỗi" hoặc một lớp "ngày" hoặc một lớp "số phức". Để "sao chép" các trường hợp của một lớp như vậy có ý nghĩa.

  2. Một loại thực thể

    Danh tính là quan trọng. Luôn luôn được chuyển bằng tham chiếu, không bao giờ bằng "giá trị". Thông thường, không có ý nghĩa gì khi "sao chép" các phiên bản của lớp. Khi nó có ý nghĩa, phương pháp "Nhân bản" đa hình thường thích hợp hơn. Ví dụ: Một lớp Socket, một lớp Cơ sở dữ liệu, một lớp "chính sách", bất kỳ thứ gì có thể là "bao đóng" trong một ngôn ngữ chức năng.

Cả pImpl và lớp cơ sở trừu tượng thuần túy đều là các kỹ thuật để giảm phụ thuộc thời gian biên dịch.

Tuy nhiên, tôi chỉ sử dụng pImpl để triển khai các kiểu Giá trị (loại 1) và chỉ đôi khi khi tôi thực sự muốn giảm thiểu sự phụ thuộc khớp nối và thời gian biên dịch. Thông thường, nó không đáng để bận tâm. Như bạn đã chỉ ra đúng, có nhiều chi phí cú pháp hơn vì bạn phải viết các phương thức chuyển tiếp cho tất cả các phương thức công khai. Đối với các lớp loại 2, tôi luôn sử dụng một lớp cơ sở trừu tượng thuần túy với (các) phương thức factory được liên kết.


6
Hãy xem bình luận của Paul de Vrieze cho câu trả lời này . Pimpl và Pure Virtual khác nhau đáng kể nếu bạn đang ở trong một thư viện và muốn hoán đổi .so / .dll của mình mà không cần tạo lại ứng dụng khách. Khách hàng liên kết đến giao diện người dùng ma cô theo tên, vì vậy việc giữ lại chữ ký của phương pháp cũ là đủ. OTOH trong trường hợp trừu tượng thuần túy, chúng liên kết hiệu quả bằng chỉ mục vtable nên việc sắp xếp lại các phương thức hoặc chèn vào giữa sẽ phá vỡ khả năng tương thích.
SnakE

1
Bạn chỉ có thể thêm (hoặc sắp xếp lại) các phương thức trong giao diện người dùng của lớp Pimpl để duy trì khả năng so sánh nhị phân. Nói một cách hợp lý, bạn vẫn đã thay đổi giao diện và nó có vẻ hơi khó hiểu. Câu trả lời ở đây là sự cân bằng hợp lý cũng có thể giúp kiểm tra đơn vị thông qua "tiêm phụ thuộc"; nhưng câu trả lời luôn phụ thuộc vào yêu cầu. Các tác giả Thư viện bên thứ ba (khác với việc sử dụng thư viện trong tổ chức của riêng bạn) có thể rất thích Pimpl.
Spacen Jasset

31

Pointer to implementationthường là về việc ẩn các chi tiết triển khai cấu trúc. Interfaceslà về việc cài đặt các triển khai khác nhau. Chúng thực sự phục vụ hai mục đích khác nhau.


13
không nhất thiết, tôi đã thấy các lớp lưu trữ nhiều ma cô tùy thuộc vào việc triển khai mong muốn. Thông thường, điều này được nói là một bản cập nhật win32 so với một bản cấy ghép linux của một cái gì đó cần được triển khai khác nhau trên mỗi nền tảng.
Doug T.

14
Nhưng bạn có thể sử dụng Giao diện để tách các chi tiết triển khai và ẩn chúng
Arkaitz Jimenez

6
Mặc dù bạn có thể triển khai pimpl bằng cách sử dụng một giao diện, nhưng thường không có lý do gì để tách các chi tiết triển khai. Vì vậy, không có lý do gì để đi đa hình. Các lý do cho pimpl là để giữ cho chi tiết thực hiện đi từ khách hàng (trong C ++ để giữ họ ra khỏi tiêu đề). Bạn có thể làm điều này bằng cách sử dụng cơ sở / giao diện trừu tượng, nhưng nói chung đó là mức quá mức không cần thiết.
Michael Burr

10
Tại sao nó quá mức cần thiết? Ý tôi là, có phải phương thức giao diện chậm hơn phương thức pimpl không? Có thể có những lý do hợp lý, nhưng từ quan điểm thực tế, tôi muốn nói rằng nó dễ dàng hơn để làm điều đó với một giao diện trừu tượng
Arkaitz Jimenez

1
Tôi sẽ nói lớp cơ sở / giao diện trừu tượng là cách "bình thường" để thực hiện mọi việc và cho phép kiểm tra dễ dàng hơn thông qua chế độ giả
paulm

28

Thành ngữ pimpl giúp bạn giảm bớt sự phụ thuộc và thời gian xây dựng, đặc biệt là trong các ứng dụng lớn và giảm thiểu việc hiển thị tiêu đề các chi tiết triển khai của lớp bạn với một đơn vị biên dịch. Những người dùng trong lớp của bạn thậm chí không cần phải biết về sự tồn tại của mụn (trừ khi là một con trỏ khó hiểu mà họ không biết đến!).

Các lớp trừu tượng (ảo thuần túy) là thứ mà khách hàng của bạn phải biết: nếu bạn cố gắng sử dụng chúng để giảm các tham chiếu liên kết và vòng tròn, bạn cần thêm một số cách cho phép họ tạo các đối tượng của bạn (ví dụ: thông qua các phương thức hoặc lớp gốc, tiêm phụ thuộc hoặc các cơ chế khác).


17

Tôi đang tìm kiếm một câu trả lời cho cùng một câu hỏi. Sau khi đọc một số bài báo và một số thực hành, tôi thích sử dụng "Giao diện lớp ảo thuần túy" .

  1. Họ thẳng thắn hơn (đây là ý kiến ​​chủ quan). Thành ngữ Pimpl khiến tôi cảm thấy mình đang viết mã "cho trình biên dịch", không phải cho "nhà phát triển tiếp theo" sẽ đọc mã của tôi.
  2. Một số khung thử nghiệm có hỗ trợ trực tiếp cho các lớp ảo thuần túy Mocking
  3. Đúng là bạn cần một nhà máy để có thể tiếp cận từ bên ngoài. Nhưng nếu bạn muốn tận dụng tính đa hình: đó cũng là "pro", không phải là "con". ... và một phương pháp nhà máy đơn giản không thực sự gây hại quá nhiều

Hạn chế duy nhất ( tôi đang cố gắng điều tra về điều này ) là thành ngữ ma cô có thể nhanh hơn

  1. khi các cuộc gọi proxy được nội tuyến, trong khi kế thừa nhất thiết cần có thêm quyền truy cập vào đối tượng VTABLE trong thời gian chạy
  2. dấu chân bộ nhớ của lớp public proxy pimpl nhỏ hơn (bạn có thể dễ dàng thực hiện tối ưu hóa để hoán đổi nhanh hơn và các tối ưu hóa tương tự khác)

21
Cũng nên nhớ rằng bằng cách sử dụng kế thừa, bạn giới thiệu một sự phụ thuộc vào bố cục vtable. Để duy trì ABI, bạn không thể thay đổi các hàm ảo nữa (thêm vào cuối là loại an toàn, nếu không có lớp con nào thêm các phương thức ảo của riêng chúng).
Paul de Vrieze

1
^ Nhận xét này ở đây phải là một dính.
CodeAngry

10

Tôi ghét mụn nhọt! Họ làm cho lớp xấu xí và không thể đọc được. Tất cả các phương pháp được chuyển hướng đến mụn. Bạn không bao giờ thấy trong tiêu đề, những chức năng nào có lớp, vì vậy bạn không thể cấu trúc lại nó (ví dụ chỉ cần thay đổi khả năng hiển thị của một phương thức). Cả lớp có cảm giác như "mang bầu". Tôi nghĩ rằng việc sử dụng iterfaces là tốt hơn và thực sự đủ để ẩn việc triển khai khỏi máy khách. Bạn có thể cho phép một lớp triển khai một số giao diện để giữ chúng mỏng. Người ta nên thích giao diện hơn! Lưu ý: Bạn không cần thiết phải có lớp nhà máy. Có liên quan là các máy khách lớp giao tiếp với các cá thể của nó thông qua giao diện thích hợp. Việc ẩn các phương thức riêng tư mà tôi thấy là một điều hoang tưởng kỳ lạ và không hiểu lý do gì cho điều này vì chúng ta có các giao diện.


1
Có một số trường hợp bạn không thể sử dụng các giao diện ảo thuần túy. Ví dụ: khi bạn có một số mã kế thừa và bạn có hai mô-đun mà bạn cần tách biệt mà không cần chạm vào chúng.
AlexTheo

Như @Paul de Vrieze đã chỉ ra bên dưới, bạn mất khả năng tương thích ABI khi thay đổi các phương thức của lớp cơ sở, bởi vì bạn có sự phụ thuộc ngầm vào vtable của lớp. Nó phụ thuộc vào trường hợp sử dụng, cho dù đây là một vấn đề.
H. Rittich,

"Việc ẩn các phương thức riêng tư mà tôi thấy là một điều hoang tưởng kỳ lạ" Điều đó không cho phép bạn ẩn các phần phụ thuộc và do đó giảm thiểu thời gian biên dịch nếu một phần phụ thuộc thay đổi?
pooya13

Tôi cũng không hiểu làm thế nào mà Factories lại dễ dàng tái định yếu tố hơn pImpl. Bạn không rời khỏi "giao diện" trong cả hai trường hợp và thay đổi cách triển khai? Trong Factory, bạn phải sửa đổi một tệp .h và một tệp .cpp và trong pImpl, bạn phải sửa đổi một tệp .h và hai tệp .cpp nhưng đó là về điều đó và bạn thường không cần sửa đổi tệp cpp của giao diện của pImpl.
pooya13

8

Có một vấn đề rất thực tế xảy ra với các thư viện được chia sẻ mà thành ngữ ma cô có thể hiểu được một cách gọn gàng mà các thủ thuật thuần túy không làm được: bạn không thể sửa đổi / xóa các thành viên dữ liệu của một lớp một cách an toàn mà không buộc người dùng của lớp đó phải biên dịch lại mã của họ. Điều đó có thể được chấp nhận trong một số trường hợp, nhưng không phải đối với thư viện hệ thống.

Để giải thích vấn đề một cách chi tiết, hãy xem xét mã sau trong thư viện / tiêu đề được chia sẻ của bạn:

// header
struct A
{
public:
  A();
  // more public interface, some of which uses the int below
private:
  int a;
};

// library 
A::A()
  : a(0)
{}

Trình biên dịch phát ra mã trong thư viện được chia sẻ để tính toán địa chỉ của số nguyên được khởi tạo thành một phần bù nào đó (có thể là 0 trong trường hợp này, vì nó là thành viên duy nhất) từ con trỏ đến đối tượng A mà nó biết là this .

Về phía người dùng của mã, new Atrước tiên a sẽ cấp phát sizeof(A)byte bộ nhớ, sau đó đưa một con trỏ tới bộ nhớ đó cho hàm A::A()tạo nhưthis .

Nếu trong bản sửa đổi sau này của thư viện, bạn quyết định loại bỏ số nguyên, làm cho nó lớn hơn, nhỏ hơn hoặc thêm các thành viên, sẽ có sự không khớp giữa lượng mã của người dùng bộ nhớ phân bổ và sự chênh lệch mà mã phương thức mong đợi. Kết quả có thể là một sự cố, nếu bạn may mắn - nếu bạn kém may mắn hơn, phần mềm của bạn hoạt động một cách kỳ lạ.

Bằng cách pimpl'ing, bạn có thể thêm và xóa các thành viên dữ liệu vào lớp bên trong một cách an toàn, khi cấp phát bộ nhớ và lệnh gọi hàm tạo diễn ra trong thư viện được chia sẻ:

// header
struct A
{
public:
  A();
  // more public interface, all of which delegates to the impl
private:
  void * impl;
};

// library 
A::A()
  : impl(new A_impl())
{}

Tất cả những gì bạn cần làm bây giờ là giữ cho giao diện công cộng của bạn không có các thành viên dữ liệu khác ngoài con trỏ đến đối tượng triển khai và bạn sẽ an toàn trước lớp lỗi này.

Chỉnh sửa: Tôi có thể nên nói thêm rằng lý do duy nhất tôi đang nói về hàm tạo ở đây là tôi không muốn cung cấp thêm mã - lập luận tương tự áp dụng cho tất cả các hàm truy cập thành viên dữ liệu.


4
Thay vì void *, tôi nghĩ truyền thống hơn là chuyển tiếp khai báo lớp triển khai:class A_impl *impl_;
Frank Krueger,

9
Tôi không hiểu, bạn không nên khai báo các thành viên riêng tư trong một lớp thuần ảo mà bạn định sử dụng làm giao diện, ý tưởng là giữ cho lớp này trừu tượng, không có kích thước, chỉ có các phương thức ảo thuần túy, tôi không thấy gì cả bạn không thể làm thông qua thư viện được chia sẻ
Arkaitz Jimenez

@Frank Krueger: Bạn nói đúng, tôi đã lười biếng. @Arkaitz Jimenez: Hiểu lầm nhẹ; nếu bạn có một lớp chỉ chứa các hàm ảo thuần túy, thì không có nhiều điểm khi nói về các thư viện dùng chung. Mặt khác, nếu bạn đang xử lý các thư viện được chia sẻ, việc hướng dẫn các lớp công khai của bạn có thể cần thận trọng vì lý do đã nêu ở trên.

10
Điều này không chính xác. Cả hai phương thức đều cho phép bạn ẩn trạng thái triển khai của các lớp, nếu bạn đặt lớp khác của mình trở thành lớp "cơ sở trừu tượng thuần túy".
Paul Hollingsworth

10
Câu đầu tiên trong anser của bạn ngụ ý rằng các hình ảnh ảo thuần túy với một phương thức factory được liên kết bằng cách nào đó không cho phép bạn ẩn trạng thái bên trong của lớp. Đo không phải sự thật. Cả hai kỹ thuật đều cho phép bạn ẩn trạng thái bên trong của lớp. Sự khác biệt là nó trông như thế nào đối với người dùng. pImpl cho phép bạn vẫn đại diện cho một lớp có ngữ nghĩa giá trị nhưng cũng ẩn trạng thái bên trong. Phương thức gốc thuần túy Abstract Base Class + cho phép bạn biểu diễn các kiểu thực thể và cũng cho phép bạn ẩn trạng thái bên trong. Sau đó là cách COM hoạt động chính xác. Chương 1 của "Essential COM" có một sự phân biệt lớn về điều này.
Paul Hollingsworth

6

Chúng ta không được quên rằng kế thừa là một mối liên kết chặt chẽ hơn, chặt chẽ hơn là ủy quyền. Tôi cũng sẽ tính đến tất cả các vấn đề được nêu ra trong các câu trả lời được đưa ra khi quyết định sử dụng thành ngữ thiết kế nào để giải quyết một vấn đề cụ thể.


3

Mặc dù đã được đề cập rộng rãi trong các câu trả lời khác, có thể tôi có thể nói rõ hơn một chút về một lợi ích của pimpl so với các lớp cơ sở ảo:

Cách tiếp cận pimpl là trong suốt từ quan điểm người dùng, có nghĩa là bạn có thể tạo các đối tượng của lớp trên ngăn xếp và sử dụng chúng trực tiếp trong các vùng chứa. Nếu bạn cố gắng ẩn việc triển khai bằng cách sử dụng lớp cơ sở ảo trừu tượng, bạn sẽ cần phải trả lại một con trỏ dùng chung cho lớp cơ sở từ một nhà máy, làm phức tạp việc sử dụng nó. Hãy xem xét mã khách hàng tương đương sau:

// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();

std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
    std::cout << o.SomeFun1();

// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();

std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
    std::cout << o->SomeFun1();

2

Theo hiểu biết của tôi, hai điều này phục vụ những mục đích hoàn toàn khác nhau. Mục đích của thành ngữ mụn về cơ bản là cung cấp cho bạn cách thực hiện để bạn có thể thực hiện những việc như hoán đổi nhanh cho một loại.

Mục đích của các lớp ảo hơn là cho phép đa hình, tức là bạn có một con trỏ chưa biết đến một đối tượng của kiểu dẫn xuất và khi bạn gọi hàm x, bạn luôn nhận được đúng hàm cho bất kỳ lớp nào mà con trỏ cơ sở thực sự trỏ đến.

Táo và cam thực sự.


Tôi đồng ý với những quả táo / cam. Nhưng có vẻ như bạn sử dụng pImpl cho một chức năng. Mục tiêu của tôi chủ yếu là xây dựng-kỹ thuật và che giấu thông tin.
xtofl

2

Vấn đề khó chịu nhất về thành ngữ ma cô là nó làm cho việc duy trì và phân tích mã hiện có trở nên cực kỳ khó khăn. Vì vậy, bằng cách sử dụng ma cô bạn phải trả bằng thời gian và sự thất vọng của nhà phát triển chỉ để "giảm sự phụ thuộc và thời gian xây dựng và giảm thiểu việc hiển thị tiêu đề của chi tiết triển khai". Hãy tự mình quyết định, nếu nó thực sự xứng đáng.

Đặc biệt "thời gian xây dựng" là vấn đề bạn có thể giải quyết bằng phần cứng tốt hơn hoặc sử dụng các công cụ như Incredibuild (www.incredibuild.com, cũng đã có trong Visual Studio 2017), do đó không ảnh hưởng đến thiết kế phần mềm của bạn. Thiết kế phần mềm nói chung phải độc lập với cách phần mềm được xây dựng.


Bạn cũng phải trả tiền bằng thời gian của nhà phát triển khi thời gian xây dựng là 20 phút thay vì 2, vì vậy một chút cân bằng, một hệ thống mô-đun thực sự sẽ giúp ích rất nhiều ở đây.
Arkaitz Jimenez

IMHO, cách phần mềm được xây dựng hoàn toàn không ảnh hưởng đến thiết kế bên trong. Đây là một vấn đề hoàn toàn khác.
Trantor

2
Điều gì làm cho nó khó phân tích? Một loạt các cuộc gọi trong một tệp triển khai chuyển tiếp đến lớp Impl nghe có vẻ không khó.
mabraham

2
Hãy tưởng tượng việc triển khai gỡ lỗi trong đó cả pimpl và giao diện đều được sử dụng. Bắt đầu từ một lệnh gọi trong mã người dùng A, bạn theo dõi đến giao diện B, chuyển đến lớp C bị bẻ khóa để cuối cùng bắt đầu gỡ lỗi lớp thực hiện D ... Bốn bước cho đến khi bạn có thể phân tích điều gì thực sự xảy ra. Và nếu toàn bộ điều được triển khai trong một DLL, bạn sẽ có thể tìm thấy một giao diện C ở đâu đó giữa ...
Trantor

Tại sao bạn lại sử dụng giao diện với pImpl khi pImpl cũng có thể thực hiện công việc của một giao diện? (tức là nó có thể giúp bạn đạt được sự đảo ngược phụ thuộc)
pooya13
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.