问题
c++Primer plus第十章第一个例子:
设计一个表示股票的类,类里面私有数据是公司名,持股数量,股价,总值。还有一系列的公有函数接口。
按理,初始化要求对象的持股数量大于等于0;但书上给的构造函数例子,完全没有检查输入的初始化是不是负数,就直接构建出来了,显然,现在代码里会有一个不符合业务逻辑的,股票对象。
怎么消除这种情况?
可以换个更简单也更好理解类似案例:
我想构造一笔巨款,未料在构建时,才发现输入金额是负数?我不想要一个“欠债”的对象,此刻我该怎么办?
南老师的回答
1. 先吹一下
先看一个该回答得到的一个评论:
感谢感谢!确实涨姿势了[赞] 这个问题所有答案都看了,感觉你的最靠谱。[赞]就像《你的灯亮着吗》书里说的,很多情况下,最重要的是:找到真正的问题是什么。。
2021-12-07
2. 再看看标准库作者会怎么做?
先说我的理解,然后来看一个标准库自身的例子。
我的理解是:
本质上,这是一个带有业务的设计上问题:即:在这个“带有参数输入错误判断需要的股票构造函数上,适用“构造失败抛出异常”及“两段式”中哪一种方法?依据是什么?具体如何做?
然后,来看标准库一个“构造失败不异常”的案例:输入流。
ifstream ifs("C:/abc.txt");
abc.txt 不存在,ifs 打不开用来读的数据,根本就构造不出一个正经的输入流。但我们能接受并且觉得这很自然,然后自觉地在上面代码之后立即做状态检查:
if (!ifs)
{
cerr << "我靠,实施人员又没有把文件放到C盘根目录……" << endl;
return;
}
//……下面开始做正常的事...
因为我们已经习惯了空的流也是一个合法的流的认识,亦即:将没有打开文件的状态,也视为流对象的正常状态之一。
所以,这事如果就只讨论技术方法。那无非就两种(别的答案都提到了):
- 抛异常,死给调用者看(倒逼他苦吧吧地处理异常)。
- 两段式构造,即,允许对象有一个为空的状态,相应的,这个空状态下,该对象其实什么事也干不了,为此,调用者需要在构造对象后,马上检查并处理。就像上面的 流对象。
有不少答案可能是在讨论“C++构造失败,要不要抛异常?”这件事,而不是在讨论“一个C++写的股票的小例子中,如果股票对象构造失败,要不要抛出异常?”这个问题。
不考虑场景,则在设计上,实际1和2都是正确的方法,所以别的答案里见仁见智,有人选择一反对二,有人选二反对一。
但其实这本质是一个带有业务的设计上问题,是一个需要考虑场景才好得出答案的问题:即:在这个“带有参数输入错误检查需要的股票构造,适用“构造失败抛出异常”及“两段式”中哪一种方法?依据是什么?具体如何做?
3. 复习类的不变式
这就必须再复习一次“类的不变式”了。
一个C++做类设计时的原则:类的不变式
,是一种状态集合,而不是只有一个(“好”的)状态。只关心本问题本身,对此相关设计讨论不感兴趣的读者可以直接跳到下面的分隔线结束位置。
其中有认为“两段式破坏了类设计中的RAII基本保障”,我表示反对一下:“资源初始化即准备好”,并非指资源只能有一种“好”的状态;资源的内部状态只要是逻辑自洽的,那就是好的。典型的“不自洽”状态是指什么呢?就是一半好,一半不好那种。比如说,你有一个结构:
struct S
{
S() = default;
S(size_t c) {...};
char* p = nullptr;
size_t len_of_p = 0;
};
只要不出现 p 指向一字符串,而len却不是p所指字符串的长度,那么RAII就得到了保障(逻辑不矛盾),哪怕就是它默认构造状态,或者,在试图为p分配内存时失败了:但无论是默认构造,还是带参构造但失败了,只要保障构造出来的对象中, p 是空指针, len是0。就仍然是良好的自洽状态(类的不变式未被破坏)。
4.学会需求分析
学习编程的各个技术知识点时,并不难,难的是:编程需要围绕类型具体的业务,我们要理解业务需求,并能分析需求中的各种逻辑。
题主寥寥几笔描述业务中,当下最关键的需求重点,就一句话:
只要是人的输入,就有可能出错。
这个观点对所有正常的程序员来说,是不是一常识?包括前面标准库的例子:
ifstream ifs("C:/abc.txt");
很有可能,abc.txt 按理(业务需求)说,是必须存在的,只是操作人不小心在磁盘上把它的名字打错了,变成 “adc.txt” 而已。显然,标准库并不想为此而抛出异常毁了整个程序。
继续我们的例子。现在有了一个常识(普遍共识)做依托,设计就变得又自然而然,又从容自信:
「股票构造时会因为入参不合法而失败」这件事情,不是那么的容易理解;
但是,「只要是用户输入就有可能错误的」这个道理,却是大家都能理解。
所以,我们应该先有一个类或结构,来表达及存储用户的输入结果。假设叫:
struct StockConstructParameterFromUserInput {...}
我刻意取这么长的名字,意思是:来自_用户输入__的股票构造_参数。如果还不够,那就再加个后缀:MaybeHaveSomethingWrong (可能会带些错误)。
而,它有个 IsValid(),或者是 Check()也行,也可以在此基础上学习 标准库的 streams,重载操作符‘ !’ 和 bool 类型转换符的行为。
现在我是调用者,并且我手上拥有那个输入结果类型的具体变量,名为 ipt。我是正常的程序员,我会:
if (!ipt)
{
...
return;
}
//或者:
if (!ipt.IsValid() )
{
...
return;
}
...
判断合法了,才拿它来构造 股票 Stock。如下:
...
Stock xiaomi (ipt) ; // 都造车了,股票会不会涨啊?
现在,你又站在了历史的重要关口,没错,这个Stock的设计者,还是你。你来知乎问,我司有个很老家伙程序员,这家伙平常就爱倚老卖老,自以为是。他在创建了 ipt 后,也不检查检查,就传给我。我和他理论: 你手上的对象是 用户输入参数;而我手上的对象是股票……所以……”
他却拿着一付:“你个小菜鸟,你懂什么”的神情看我,我该如何是好啊……
还能怎样?老办法:平常调试咱也就忍了,帮他用assert大法检查,等releases上战场了,assert失效,异常直接抛出呗。不用怕,通常上层调用者将不会在每次构造这类类对象,都辛苦地写一个“try { … } catch(…) ”。通常,他们会在某个更上层的位置上,加上这对“try/catch”。
所以,Stock 构造的代码,大致是:
Stock(StockConstructParameterFromUserInput const & ipt)
{
assert( ipt.IsValid() && "兄弟你认真点行吗?");
if (!ipt.IsValid()) throw ....
...
}
这要引出有assert断言了。上面 assert 的用法,可以看这一节课:如何为C++的断言附加消息 。
还需要后面再判断的吗?这个经典问题。经典书籍的大牛在书里说是不用。但,这仍然或者更加不是一个技术与设计的问题,这仍然是团队的问题,人的问题。有时候必须接受一个问题。由于你采用本回复的方法,最后走的人,以及在一众人眼中代码设计得好烂的人,都是你。
事情于是回到了人人在自己的代码里拼命检查别人传来的数据然后拼命打日志以备将来项目上线后事故发生时,能铁证如山,明确自保的状态……这就不好了。
4 小结
归纳一下,我的答案是:
- 先拆出 “来自用户输入的股票构造参数”类。这个类在语义上拥有是“IsValid() / 是否合法输入”是自然而然的,可能比前面提到的标准库的“ ifstream 类” 拥有 “!”重载还要自然。
- 然后,使用前者来构造股票。此时如果发现前者(居然,仍然,竟然……)不合法,那就异常。作为团队的最佳实践方法之一,我们还提到:在调试版本中使用 assert/断言 机制 。
不要因为这个方法把两派的方法(二段式和构造出错异常)都用上了,就认为我是来和稀泥的。我是认真的。《白话C++练功篇,真的不是只讲一门计算机语言,而是花很多文字在讲如何做更好的程序设计。