将下一个C ++类作为问题的简化:
struct Car
{
virtual int get_price() = 0;
};
struct ExpensiveCar: public Car
{
int get_price( ) {/*..*/ }
void apply_turbo( ){/*..*/};
};
struct CheapCar: public Car
{
int get_price( ) {/*..*/}
};
struct CarRetailer
{
virtual std::vector<Car*> get_cars( ) = 0;
};
struct ExpensiveCarsRetailer : public CarRetailer
{
virtual std::vector<Car*> get_cars( ) { /*..*/ }
std::vector<ExpensiveCar*> get_cars_for_exhibitions( );
};
struct CheapCarsRetailer : public CarRetailer
{
virtual std::vector<Car*> get_cars( ) { /*..*/ }
std::vector<CheapCar*> get_second_hand_cars( );
};
规则是:昂贵的汽车只在ExpensiveCarsRetailers中销售(类似于廉价汽车)。廉价汽车没有涡轮增压,昂贵的汽车不是二手卖。
我在这里遇到的问题是包含继承类的类的耦合。因此,如果ExpensiveCarRetailer
继承自CarRetailer
,则需要实施
virtual std::vector<Car*> get_cars( )
实际上返回了Car*
的向量,但是,内部ExpensiveCarRetailer
仅创建了ExpensiveCar
的对象。此外,get_cars_for_exhibitions()
未包含在公共接口CarRetailer
中,因此,它可以返回std::vector<ExpensiveCar*>
。
API中的混合(Car*
和ExpensiveCar*
的返回向量)非常难看,用户需要编写的代码才能使用apply_turbo( )
函数来自特定ExpesiveCarsRetailer
的汽车列表。
ExpensiveCarsRetailer ferrari;
std::vector<Car*> car = ferrari.get_cars();
ExpensiveCar* expensive_car;
for( int i = 0; i < car.size( ); ++i)
{
expensive_car = dynamic_cast<ExpensiveCar*>(car[i]);
expensive_car->apply_turbo();
}
我确信我缺少一些有助于这种情况的设计模式,其中类继承树的耦合方式是其中一个继承tress的抽象类需要返回一个向量(或set,list,等其他继承树上的类)。我试图尽可能地避免动态铸造。
我还考虑过将CarRetailer
设为模板类,因此:
template<typename T>
struct CarRetailer
{
virtual std::vector<T*> get_cars( ) = 0;
};
然后制作:
struct ExpensiveCarRetailer: public CarRetailer<ExpensiveCar>
{
...
}
但我认为这不会起作用,因为例如CarRetailer
也可以开始销售摩托车(类似于汽车的结构)或自行车等(总是应用昂贵/便宜的模式)并最终出现这个数字需要在CarRetailer
中定义的模板类将是巨大的。
答案 0 :(得分:1)
&#34;包含继承类的类的耦合的整个概念&#34;被称为协方差。在维基百科上,人们可以找到对该主题的广泛评论:http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)。但让我们回到实际的例子:写作
struct CarRetailer
{
virtual std::vector<Car*> get_cars( ) = 0;
};
你承诺太多了,即get_cars()
函数会返回一个向量,你可以用你想要的任何Car
来修改它。这可能不是你的意思:
CarRetailer* ferrari = new ExpensiveCarsRetailer();
auto niceCars ferrari->get_cars();
niceCars.push_back(new Car{"Trabant"}); // you promised in the declaration that it was possible!
解决方案正在减少&#34;承诺&#34;你在根类中做了,返回一个不能用&#34;无关的范围改变的范围&#34;对象。一个只读的容器会很好,但是,唉,C ++不够智能,不足以支持它的变化:
struct CarRetailer
{
virtual const std::vector<const Car*> get_cars( ) = 0;
};
struct ExpensiveCarsRetailer : public CarRetailer
{
const std::vector<const ExpensiveCar*> get_cars( ) = 0;
// Alas, it won't override
};
但是范围(即指针对,希望C ++ 17能提供更好的东西)会做:
struct CarRetailer
{
virtual Car* const cars_begin( ) = 0;
virtual Car* const cars_end( ) = 0;
};
struct ExpensiveCarsRetailer : public CarRetailer
{
ExpensiveCar* const cars_begin( ) override {return cars->begin();}
ExpensiveCar* const cars_end( ) override {return cars->end();}
private:
vector<ExpensiveCar>* cars;
};
(注意:我没有测试我的代码,所以请原谅可能的错误。但这个概念应该很清楚)
这个界面可能看起来比原始界面更丑,但我的观点相反,因为它与强大的<algorithm>
C ++库完美集成,有助于以现代风格编写代码。这是一个简单的例子:
any_of(dealer.cars_begin( ), dealer.cars_end( ),
[](const auto& car) -> bool {return car.hasScratch();}
) ? complain() : congratulate();
这种设计的最大缺点是CarDealer
类必须拥有*cars
容器并管理它的存在,即它必须关心它返回的指针保持活动状态。由于C ++ does not support covariance on them无法返回智能指针这一事实使得这很困难。另外,如果vector<Car>*
和cars_begin
函数应该在重复调用时生成新集合,则可能最终必须保留cars_end
的容器。因此,您需要在具体用例中平衡我的提案的优缺点。
就个人而言,如果我遇到同样的问题,我会使用模板化的类并完全避免继承:这类问题恕我直言说明为什么OOP有时会使事情变得复杂而不是简化它们。
修改强>
如果我理解为什么你觉得模板经销商不会满足你的需求,我认为这个提议应该更合适:
struct ExpensiveCarsRetailer /* not derived, not templated */
{
std::vector<ExpensiveCar> get_cars( ) { /*..*/ }
// you can also return a vector of pointers or of unique_pointers, as you feel like.
};
struct CheapCarsRetailer /* not derived, not templated */
{
std::vector<CheapCar> get_cars( );
};
使用模板化函数代替重载:
template <typename T> print_car_table(T dealer) {
// This will work on both Expensive and Cheap dealers
// Not even a hierarchy for the Car classes is needed:
// they can be independent structs, like the Dealer ones
auto cars = dealer.get_cars();
for (const auto& car : cars) { std::cout << car.name() << "\t" << car.color() << "\n"; }
}
template <typename T> apply_turbo(T dealer) {
// This will work if dealer is an ExpensiveDealer,
// and fail on compile time if not, as wanted
auto cars = dealer.get_cars();
for (auto& car : cars) { car.apply_turbo(); }
}
这个系统的最大优点是你甚至不必提前规划界面,每个类只能实现你需要的方法。因此,如果将来添加CarMuseum
,您可以决定实施get_cars()
以使print_car_table(T)
使用它,但您可以自由地不创建任何其他方法。通过继承,您将被迫实现基本接口中声明的所有函数,或者创建许多分段接口(class CheapDealer : public HasACarList, public HasAPriceList, /* yuck ...*/
)。
这种模板化设计的缺点是经销商类不是亲戚的结果。这意味着您无法创建vector<Dealer>
也不能创建Dealer*
(即使您从一个很小的公共接口派生它们,也无法通过指向此类接口的指针调用get_cars())。
为了完整起见,我会指出相反的,动态的解决方案:
struct Car {
virtual int get_price() = 0;
virtual void apply_turbo( ) = 0;
};
struct CheapCar: public Car
{
int get_price( ) {/*..*/}
void apply_turbo( ){throw OperationNonSupportedException();};
};
它感觉不是非常惯用的C ++,是吗?我觉得它很不优雅,但我认为这个设计应该在丢弃之前进行评估。优点和缺点或多或少与模板化解决方案相反。
最后,我猜访问者模式或Scala(提供非常强大的协方差设施)可以为您的问题提供其他替代解决方案。但我对这两者都没有经验,所以我把它们留给其他人来说明。
答案 1 :(得分:0)
为什么不像你使用get_cars_for_exhibitions
一样拥有非虚拟功能来获取汽车?在示例中,您显示调用者知道他们有ExpensiveCarsRetailer
因此他们不需要多态性。我假设你确实需要其他上下文中的继承和多态,但是很容易从ExpensiveCar
指针的向量转换为Car
指针的向量来实现CarRetailer
接口:
template<typename T>
using Ptrs = std::vector<std::unique_ptr<T>>;
template<class T, class U>
Ptrs<U> upcast(Ptrs<T> input) {
return Ptrs<U>(std::make_move_iterator(input.begin()),
std::make_move_iterator(input.end()));
}
struct CarRetailer {
virtual Ptrs<Car> getCars() = 0;
};
struct ExpensiveCarsRetailer : CarRetailer {
Ptrs<ExpensiveCar> getCarsImpl() { ... }
Ptrs<Car> getCars() override { return upcast<Car>(getCarsImpl()); }
};
如果你需要ExpensiveCar
指针的向量,你可以调用非虚函数:
ExpensiveCarsRetailer ferrari;
auto expensive_cars = ferrari.getCarsImpl();
for (auto& expensive_car : expensive_cars)
expensive_car->apply_turbo();
在多态上下文中,您可以调用虚函数:
CarRetailer* retailer = &ferrari;
auto cars = retailer->getCars();
for (auto& car : cars)
std::cout << "Car price " << car->get_price() << "\n";