Theo bình luận của bạn, những gì bạn đã stumbled khi được biết đến (hồ nghi) là Expression Problem, được thể hiện bởi Philip Wadler:
Vấn đề biểu thức là tên mới cho một vấn đề cũ. Mục tiêu là định nghĩa một kiểu dữ liệu theo các trường hợp, nơi người ta có thể thêm các trường hợp mới vào kiểu dữ liệu và các hàm mới trên kiểu dữ liệu, mà không biên dịch lại mã hiện có, và trong khi vẫn giữ an toàn kiểu tĩnh (ví dụ, không có phôi).
Đó là, mở rộng cả "chiều dọc" (thêm các loại cho hệ thống phân cấp) và "ngang" (thêm chức năng để được overriden đến lớp cơ sở) là cứng trên các lập trình viên.
Có một cuộc thảo luận dài (luôn luôn) về nó trên Reddit mà tôi đã đề xuất một solution in C++.
Đây là cầu nối giữa OO (tuyệt vời khi thêm loại mới) và lập trình chung (tuyệt vời khi thêm các chức năng mới). Ý tưởng là có một loạt các giao diện thuần túy và một tập hợp các loại không đa hình. Các hàm tự do được định nghĩa trên các loại cụ thể khi cần và cầu nối với giao diện thuần túy được mang bởi một lớp mẫu đơn cho mỗi giao diện (được bổ sung bởi một hàm mẫu để khấu trừ tự động).
Tôi đã tìm thấy một hạn chế duy nhất cho đến nay: nếu một hàm trả về một giao diện Base
, nó có thể đã được tạo ra, ngay cả khi loại thực tế được hỗ trợ nhiều hoạt động hơn. Đây là điển hình của một thiết kế mô-đun (các chức năng mới không có sẵn tại trang web cuộc gọi). Tôi nghĩ rằng nó minh họa một thiết kế sạch sẽ, tuy nhiên tôi hiểu rằng người ta có thể muốn "recast" nó vào một giao diện dài dòng hơn. Go
có thể, với hỗ trợ ngôn ngữ (về cơ bản, thời gian chạy nội suy của các phương pháp có sẵn). Tôi không muốn mã số này bằng C++.
Như đã giải thích cho bản thân về reddit ... Tôi sẽ chỉ tái tạo và chỉnh sửa mã mà tôi đã gửi ở đó.
Vì vậy, hãy bắt đầu với 2 loại và một thao tác đơn lẻ.
struct Square { double side; };
double area(Square const s);
struct Circle { double radius; };
double area(Circle const c);
Bây giờ, chúng ta hãy làm một giao diện Shape
:
class Shape {
public:
virtual ~Shape();
virtual double area() const = 0;
protected:
Shape(Shape const&) {}
Shape& operator=(Shape const&) { return *this; }
};
typedef std::unique_ptr<Shape> ShapePtr;
template <typename T>
class ShapeT: public Shape {
public:
explicit ShapeT(T const t): _shape(t) {}
virtual double area() const { return area(_shape); }
private:
T _shape;
};
template <typename T>
ShapePtr newShape(T t) { return ShapePtr(new ShapeT<T>(t)); }
Okay, C++ là tiết. Hãy kiểm tra việc sử dụng ngay lập tức:
double totalArea(std::vector<ShapePtr> const& shapes) {
double total = 0.0;
for (ShapePtr const& s: shapes) { total += s->area(); }
return total;
}
int main() {
std::vector<ShapePtr> shapes{ new_shape<Square>({5.0}), new_shape<Circle>({3.0}) };
std::cout << totalArea(shapes) << "\n";
}
Vì vậy, tập thể dục đầu tiên, chúng ta hãy thêm một hình dạng (vâng, đó là tất cả):
struct Rectangle { double length, height; };
double area(Rectangle const r);
Được rồi, cho đến nay tốt như vậy, chúng ta hãy thêm một hàm mới. Chúng tôi có hai lựa chọn.
Đầu tiên là sửa đổi Shape
nếu nó nằm trong quyền lực của chúng tôi. Đây là nguồn tương thích, nhưng không tương thích nhị phân.
// 1. We need to extend Shape:
virtual double perimeter() const = 0
// 2. And its adapter: ShapeT
virtual double perimeter() const { return perimeter(_shape); }
// 3. And provide the method for each Shape (obviously)
double perimeter(Square const s);
double perimeter(Circle const c);
double perimeter(Rectangle const r);
Có vẻ như chúng tôi rơi vào vấn đề biểu thức ở đây, nhưng chúng tôi không. Chúng ta cần thêm chu vi cho mỗi lớp (đã biết) vì không có cách nào để tự động suy ra nó; tuy nhiên nó không yêu cầu chỉnh sửa từng lớp!
Do đó, sự kết hợp của Giao diện bên ngoài và các chức năng miễn phí cho phép chúng tôi gọn gàng (tốt, đó là C++ ...) bên lề vấn đề.
sodraz
nhận thấy trong nhận xét rằng việc thêm chức năng đã chạm vào giao diện gốc có thể cần phải được cố định (do bên thứ ba cung cấp hoặc các vấn đề tương thích nhị phân).
Các tùy chọn thứ hai do đó không xâm nhập, với chi phí là hơi dài dòng hơn:
class ExtendedShape: public Shape {
public:
virtual double perimeter() const = 0;
protected:
ExtendedShape(ExtendedShape const&) {}
ExtendedShape& operator=(ExtendedShape const&) { return *this; }
};
typedef std::unique_ptr<ExtendedShape> ExtendedShapePtr;
template <typename T>
class ExtendedShapeT: public ExtendedShape {
public:
virtual double area() const { return area(_data); }
virtual double perimeter() const { return perimeter(_data); }
private:
T _data;
};
template <typename T>
ExtendedShapePtr newExtendedShape(T t) { return ExtendedShapePtr(new ExtendedShapeT<T>(t)); }
Và sau đó, xác định perimeter
chức năng cho tất cả những Shape
chúng tôi muốn sử dụng với ExtendedShape
.
Mã cũ, được biên dịch để hoạt động với Shape
, vẫn hoạt động. Nó không cần chức năng mới.
Mã mới có thể sử dụng chức năng mới và vẫn giao diện không đau đớn với mã cũ. (*)
Chỉ có một vấn đề nhỏ, nếu mã cũ trả về ShapePtr
, chúng tôi không biết liệu hình dạng thực sự có chức năng chu vi hay không (lưu ý: nếu con trỏ được tạo bên trong, nó không được tạo bằng cơ chế newExtendedShape
). Đây là giới hạn của thiết kế được đề cập ở đầu. Rất tiếc :)
(*) Lưu ý: không có nghĩa là bạn biết chủ sở hữu là ai. Một std::unique_ptr<Derived>&
và std::unique_ptr<Base>&
không tương thích, tuy nhiên, std::unique_ptr<Base>
có thể được tạo từ một số std::unique_ptr<Derived>
và Base*
từ một Derived*
để đảm bảo chức năng của bạn sạch sẽ và bạn vàng.
Nếu bạn đang truyền động cho một chế độ phụ bậc nhất cố định (ví dụ: không thực hiện chuỗi thử-động-phôi), thì hãy đi với nó, vì đó là cách rõ ràng nhất để đạt được điều này. Tốt nhất là thêm một giao diện và dàn diễn động vào giao diện. –
Không rõ là bạn muốn có một giải pháp OO rõ ràng, hoặc nếu bạn muốn chạy mã của bạn và chăm sóc cho hiệu suất. Kể từ khi sao chép 'shared_ptr's là giá rẻ, chỉ cần công cụ nó trong' std :: vector 'khác nhau như bạn đề nghị. Bạn sẽ không có nhánh bên trong vòng lặp, đó có thể là điểm cộng cho hiệu suất. – eudoxos