面向过程到面向对象-入门,心得(实例剖析)
问题描述(略显简洁)
一家书店做促销,部分书籍参与打折,购买量到一定程度有相应的折扣;(例:某书10本以内,不打折,10本以上打9折)。
结账时,打印出账单,账单条目如下(按isbn排序):
isbn,数量,商品价格;
最后打印所有书籍总价格。
问题分析
1)基本分析
书籍有至少有两种:一种打折,一种不打折;
对于一个顾客,买了n本书,需要对每一个顾客的书计算价格,最后得到总价格。
2)深入分析
a)
两种书籍有共同点:isbn,价格;
不同点:折扣,数量;
因此设计一个继承体系的类,用于表示不同种类的书籍(对象)。
b)对于每一个顾客,设计一个书籍类的vector,保存每一本书的信息,到最后遍历这个vector,进行操作即可。
但因为需要排序打印,故用multiset取代vector(可能买多本书籍)。
因为multiset中只能存放的是同一类型的对象,而书籍的类型不一致,故只能存放指针。这里采用智能指针。
面向过程解决方案
1)书籍类的设计:
Quote表示不打折书籍;
Bulk_Quote表示打折书籍,继承自Quote
class Quote {
public:
Quote() = default;
Quote(const string& book, double sales_price): bookNo(book), price(sales_price) {}
string isbn() const { return bookNo; }
virtual double net_price(size_t n) const { return n * price; }
virtual ~Quote() = default;
protected:
double price = 0.0; //价格
private:
string bookNo; //isbn
};
class Bulk_Quote : public Quote{
public:
Bulk_Quote() = default;
Bulk_Quote(const string& book, double p, size_t qty, double disc):
Quote(book, p), min_qty(qty), discount(disc) {}
double net_price(size_t n) const override { //override只出现在声明中,const &等之后
if (n >= min_qty) {
return n * (1 - discount) * price;
} else {
return n * price;
}
}
private:
size_t min_qty = 0; //能打折需要达到的最少数量
double discount = 0.0; //具体的折扣
};
简单解析:
如果有不同的折扣活动,可以通过继承,实现不同的折扣方法。
2)对于每一本书籍,打印其基本信息
//item 书籍(具体的对象);
//n 书籍的数量
double print_total(ostream& os, const Quote& item, size_t n) {
double ret = item.net_price(n); //多态调用其价格计算的方法
os << "isbn: " << item.isbn() << " sold: " << n << " total due: " << ret << endl;
return ret;
}
3)对于multiset,因为关键字必须定义一个<运算符,故采用继承标准库提供的方法,模版特例化一个版本
template <typename T>
struct compare : public std::binary_function<T, T, bool> {
bool operator() (const T& x, const T& y) const {
return x->isbn() < y->isbn();
}
};
这样定义之后,就可以定义如下的multiset
multiset<shared_ptr<Quote>, compare<shared_ptr<Quote>>> ms;
简单解释:
1 定义了一个名为ms的multiset
2 其中存放的对象类型为shared_ptr
multiset<shared_ptr<Quote>, compare<shared_ptr<Quote>>> ms;
//定义几个对象;
Quote q1("11-11", 50);
Quote q11("11-11", 50);
Quote q2("11-12", 40);
Bulk_Quote b1("22-11", 50, 10, 0.1);
Bulk_Quote b2("22-12", 40, 10, 0.1);
//将对象插入ms,因为ms保存的是指针类型,所以必须用对象显示构造指针;
ms.insert(make_shared<Quote>(q1));
ms.insert(make_shared<Quote>(q11));
ms.insert(make_shared<Quote>(q2));
ms.insert(make_shared<Quote>(b1));
ms.insert(make_shared<Quote>(b2));
//输出信息
double total = 0.0;
for (auto iter = ms.cbegin(); iter != ms.cend(); iter = ms.upper_bound(*iter)) {
total += print_total(cout, **iter, ms.count(*iter));
}
cout << "total sales: " << total << endl;
对于输出信息模块,简单解释如下:
1 因为multiset中可能有多个相同的对象,ms.upper_bound(*iter)
即使找到该对象的最后一个重复版本的下一个位置
2 因为iter是迭代器,需要解引用得到对象;而muliset中保存的是指针,故再次解引用得到Quote对象;
5)总结:
该方法最大的问题就在于需要用户显示的构造指针,并且暴露了太多的multiset实现的细节。正因为如此,才引入了面向对象的方法。
面向对象方法1
面向过程暴露了太多的实现细节,那可以把那些细节都用一个类表示即可。
1)设计一个类如下:
class Basket_ver1 {
public:
void add_item(const shared_ptr<Quote>& sale) {
items.insert(sale);
}
double total_receipt(ostream& os) const;
private:
multiset<shared_ptr<Quote>, compare<shared_ptr<Quote>>> items;
};
double Basket_ver1::total_receipt(ostream& os) const {
double sum = 0.0;
for (auto iter = items.cbegin(); iter != items.cend(); iter = items.upper_bound(*iter)) {
sum += print_total(os, **iter, items.count(*iter));
}
os << "total sales: " << sum << endl;
return sum;
}
2)主程序如下:
Basket_ver1 bst;
Quote q1("11-11", 50);
Quote q11("11-11", 50);
Quote q2("11-12", 40);
Bulk_Quote b1("22-11", 50, 10, 0.1);
Bulk_Quote b2("22-12", 40, 10, 0.1);
bst.add_item(make_shared<Quote>(q1));
bst.add_item(make_shared<Quote>(q2));
bst.add_item(make_shared<Quote>(q11));
bst.add_item(make_shared<Quote>(b1));
bst.add_item(make_shared<Quote>(b2));
bst.total_receipt(cout);
3)总结
因为类中add_item函数接受的是指针,很方便类的设计(自动进行派生类到基类指针的转换),但麻烦了用户(用户得显示的创建指针); 所以,引出了方法2
面向对象方法2
1)问题
方法1中:add_item的参数是基类指针。此时不论用户提供的是基类类型还是派生类类型,都可以存入item中(派生类指针可以自动转化为基类指针)。然而,用户更应该直接提供对象,而不应该提供指针。所以需要修改函数参数为对象类型,函数内部通过对象来构造指针。
那么问题来了:
void add_item(const Quote& sale);
函数接受一个对象的引用,如何创建对应类型的指针呢?
new Quote(sale) or new Bulk_Quote(sale) ?
貌似都不行。
我们希望编写如下代码:
sale.clone();
可以根据对象的实际类型创建对应的对象;
这是,我们应该在类的继承体系中添加虚函数clone实现多态;
2)重新设计
a)继承体系类
class Quote {
public:
Quote() = default;
Quote(const string& book, double sales_price): bookNo(book), price(sales_price) {}
string isbn() const { return bookNo; }
virtual double net_price(size_t n) const { return n * price; }
virtual ~Quote() = default;
//新添加函数:可以接受左值或右值
virtual Quote* clone() const & {
return new Quote(*this);
}
virtual Quote* clone() const && {
return new Quote(std::move(*this));
}
protected:
double price = 0.0;
private:
string bookNo;
};
class Bulk_Quote : public Quote{
public:
Bulk_Quote() = default;
Bulk_Quote(const string& book, double p, size_t qty, double disc):
Quote(book, p), min_qty(qty), discount(disc) {}
double net_price(size_t n) const override { //override只出现在声明中,const &等之后
if (n >= min_qty) {
return n * (1 - discount) * price;
} else {
return n * price;
}
}
//新添加函数:可以接受左值或右值
Bulk_Quote* clone() const & {
return new Bulk_Quote(*this);
}
Bulk_Quote* clone() const && {
return new Bulk_Quote(std::move(*this));
}
private:
size_t min_qty = 0;
double discount = 0.0;
};
简单解析:
1 先不看右值版本(&&),只看左值版本(&);
2 在Quote中定义一个虚函数clone,返回一个本对象的指针;在Bulk_Quote中覆盖该函数;
b)功能类
class Basket_ver2 {
public:
void add_item(const Quote& sale) {
items.insert(shared_ptr<Quote>(sale.clone()));
}
void add_item(Quote&& sale) {
items.insert(shared_ptr<Quote>(std::move(sale).clone()));
}
double total_receipt(ostream& os) const;
private:
multiset<shared_ptr<Quote>, compare<shared_ptr<Quote>>> items;
};
简单解析:
1 只看接受左值参数版本
2 sale调用clone,因为接受的参数是基类引用类型且调用虚函数,所以在此处一定会发生多态调用,这样返回的指针类型即是正确的。
3 因为返回的是原生指针,需要显示构造智能指针,才能存入items
3)主程序如下:
Basket_ver2 bst;
Quote q1("11-11", 50);
Quote q11("11-11", 50);
Quote q2("11-12", 40);
Bulk_Quote b1("22-11", 50, 10, 0.1);
Bulk_Quote b2("22-12", 40, 10, 0.1);
bst.add_item(q1);
bst.add_item(q11);
bst.add_item(q2);
bst.add_item(b1);
bst.add_item(b2);
bst.total_receipt(cout);
三种方法的结果都是相同的:
isbn: 11-11 sold: 2 total due: 100
isbn: 11-12 sold: 1 total due: 40
isbn: 22-11 sold: 1 total due: 50
isbn: 22-12 sold: 1 total due: 40
total sales: 230
4)总结
该主程序相对于方法1中的主程序区别在于一点:用户只需要提供对象即可插入到bst中,而不需要做任何多余的事情。(显示构造指针)
小结面向对象
最后的主程序或许能简单反映出面向对象编程的思想:面向 对象。
1 程序中一切东西都是对象:不论是bst,或q1,b1都是对象,通过调用对象的方法,管理对象;
2 封装:主程序只能看到对象,完全不知道其实现细节(add_item并不是细节,而是解决问题的逻辑),用户只需要把精力集中在逻辑的实现(1 创建对象 2 插入对象 3 打印对象)。当能够弄清楚逻辑的实现时,问题也应该已经得到解决。
3 继承和多态:面向对象的核心实现即是继承和多态。仅仅是修改用户的一处操作,就需要修改整个继承体系类的设计,目的就是为了能够方便用户。如果没有继承和多态,根本无法实现。
4 一切的设计只是为了用户方便。因此写代码之前,首先应该想到逻辑怎么实现,然后想主程序应该怎么写,最后才是类的设计。
路漫漫其修远兮,吾将上下而求索
在这个充斥着大数据,云计算,函数式编程,并发,网络,多线程编程的年代,我才刚刚开始面向对象编程。
慢慢来,不着急。。。
(刚入门,写的很基础,还请各路大神赐教)