面向过程到面向对象-入门,心得(实例剖析)

面向过程到面向对象-入门,心得(实例剖析)

问题描述(略显简洁)

一家书店做促销,部分书籍参与打折,购买量到一定程度有相应的折扣;(例:某书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 一切的设计只是为了用户方便。因此写代码之前,首先应该想到逻辑怎么实现,然后想主程序应该怎么写,最后才是类的设计。

路漫漫其修远兮,吾将上下而求索

在这个充斥着大数据,云计算,函数式编程,并发,网络,多线程编程的年代,我才刚刚开始面向对象编程。

慢慢来,不着急。。。

(刚入门,写的很基础,还请各路大神赐教)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值