C++20之设计模式(19):空对象

server/2024/9/22 22:46:32/

空对象

      • 空对象
        • 场景
      • 空对象
      • 共享指针不是空对象
        • 改进设计
        • 隐式空对象
        • 总结

空对象

我们并不能总能选择自己想使用的接口。例如,我宁愿让我的车自己开车送我去目的地,而不必把100%的注意力放在道路和开车在我旁边的危险疯子身上。软件也是如此:有时你并不是真的想要某一项功能,但它是内置在接口里的。那么你会怎么做呢?创建一个空对象。

场景

假设继承了使用下列接口的库:

struct Logger
{virtual ~Logger() = default;virtual void info(const string& s) = 0;virtual void warn(const string& s) = 0;
}

这个库使用下面的接口来操作银行账户:

struct BankAccount
{std::shared_ptr<Logger> log;string name;int balance = 0;BankAccount(const std::share_ptr<Logger>& logger, const string& name, int balance):log{ logger },name{ name },balance {balance}{// more members here}
};

事实上,BankAccount可以拥有如下的成员函数:

void BankAccount::deposit(int amount)
{balance += amount;log->info(("Deposited $" + lexical_cast<string>(amount)+ " to " + name + ", balance is now $" + lexical_cast<string>(balance));
}

好了,这个实现有什么吗?如果你确实需要日志记录,也没有问题,你只需实现自己的日志记录类…

struct ConsoleLogger : Logger
{void info(const string& s) override{cout << "INFO: " << s << endl;}void warn(const string& s) override{cout << "WARNNING!!!" << s << endl;}
};

你可以直接使用它。但是,如果你根本不想要日志记录呢?

空对象

我们再来仔细看下BankAccount的构造函数

BankAccount(const shared_ptr<Logger>& logger, const string& name, int balance)

由于构造函数接受一个日志记录器,因此传递一个未初始化的shared_ptr<BankAccount>是不安全的。BankAccout可以使用指针之前,在内部检查指针是否为空,但你不知道它是否这样做了,因为没有额外的文档是不可能知道的。

因此,唯一可以传入BankAccount的是一个空对象,一个符合接口但不包含功能的类:

struct NullLoggor : Logger
{void info(const string& s) override { }void warn(const string& s) override { }
};

共享指针不是空对象

值得注意的是,shared_ptr和其他智能指针类都不是空对象。空对象是保留正确操作(执行无操作)的对象。但是,使用对未初始化的智能指针会崩溃会导致程序崩溃:

shared_ptr<int> n;
int x = *n + 1; // yikes!

值得注意的是,从调用的角度来看,没有办法使智能指针是安全的。换句话说,如果foo没有初始化,那么foo->bar()会神奇地变成一个空操作,那么你不能编写这样的智能指针。原因是前缀*和后缀->操作符只是代理了底层(原始)指针。没有办法对指针做无操作。

改进设计

停下来想一想:如果BankAccount在你的控制之下,你能改进接口使它更容易使用吗?这里有一些想法:

  • 在所有地方都进行指针检查。这就理清了BankAccount的正确性,但并没有消除库使用者的困惑。请记住,你仍然没有说明指针可以是空的。
  • 添加一个默认实参值,类似于const shared_ptr<Logger>& logger = no_logging其中no_loggingBankAccount类的某个成员。即使是这样,你仍然必须在想要使用对象的每个位置对指针值执行检查
  • 使用可选(optional)类型。它的习惯用法是正确的,并且可以传达意图,但是会导致传入一个optional<shared_ptr<T>>以及随后检查可选项是否为空。
隐式空对象

这里有一个激进的想法,需要进行两步操纵。它把涉及到把日志记录过程细分为调用(我们想要一个好的日志记录器接口)和操作(日志记录器实际做的事情)。因此,请考虑以下几点:

struct OptionalLogger : Logger 
{shared_ptr<Logger> impl;static shared_ptr<Logger> no_logging;Logger(const shared_ptr<Logger>& logger) : impl { logger } { }virtual void info(const string& s) override{if(impl) impl->info(s); // null check here}// and similar checks for other members
};// a static instance of a null object
shared_ptr<Logger> BankAccount::no_logging{};

现在我们已经从实现中抽象出了调用。我们现在要做的是像下面这样重新定义BankAccount构造函数:

shared_ptr<OptionalLogger> logger;
BankAccount(const string& name, int balance, const shared_ptr<Logger>& logger = no_logging) : log{ make_shared<OptionalLogger>(logger) },name{ name },balance{ balance } { }

如您所见,这里有一个巧妙的诡计:我们使用一个Logger,但存储一个OptionalLogger(这是代理设计模式)。然后,对这个可选记录器的所有调用都是安全的-它们只有在底层对象可用时才“发生”:

BankAccount account{ "primary account", 1000 };
account.deposit(2000); // no crash

上例中实现的代理对象本质上是Pimpl编程技法的自定义版本。

总结

空对象模式提出了一个API设计的问题:我们可以对我们所依赖的对象做什么样的假设?如果我们取一个指针(裸指针或智能指针),那么是否有义务在每次使用时检查该指针?

如果你觉得没有这种义务,那么用户实现空对象的唯一方法是构造所需接口的无操作实现,并将该实例传递进来。也就是说,这只适用于函数:例如,如果对象的字段也被使用,那么你就遇到了真正的麻烦。

如果你想主动支持空对象作为参数传递的想法,你需要明确:要么指定参数类型为std::optional,给参数一个默认值,暗示它是一个内置的空对象(例如,= no_logging),或只写文档说明什么样的值应当出现在这个位置。


http://www.ppmy.cn/server/90909.html

相关文章

Weakly Supervised Contrastive Learning 论文阅读

Abstract 无监督视觉表示学习因对比学习的最新成就而受到计算机视觉领域的广泛关注。现有的大多数对比学习框架采用实例区分作为预设任务&#xff0c;将每个实例视为一个不同的类。然而&#xff0c;这种方法不可避免地会导致类别冲突问题&#xff0c;从而损害所学习表示的质量…

人工智能在医疗领域的应用及未来展望

随着科技的不断发展&#xff0c;人工智能&#xff08;AI&#xff09;逐渐成为人们关注的焦点。在众多领域中&#xff0c;医疗行业与AI的结合备受瞩目&#xff0c;为现代医疗带来了前所未有的变革。本文将探讨人工智能在医疗领域的应用及其未来发展。 一、人工智能在医疗领域的…

XPathParser类

XPathParser类是mybatis对 javax.xml.xpath.XPath的包装类。 接下来我们来看下XPathParser类的结构 1、属性 // 存放读取到的整个XML文档private final Document document;// 是否开启验证private boolean validation;// 自定义的DTD约束文件实体解析器&#xff0c;与valida…

Android笔试面试题AI答之线程Handler、Thread(1)

答案仅供参考&#xff0c;来自 讯飞星火大模型 目录 1.Dvm的进程和Linux的进程, 应用程序的进程是否为同一个概念&#xff1f;2.简述Handler &#xff1f;Handler机制是什么&#xff1f;其原理是什么&#xff1f;3.简述使用Handler的时候一般会遇到的问题&#xff1f;4.Android…

网络爬虫必备工具:代理IP科普指南

文章目录 1. 网络爬虫简介1.1 什么是网络爬虫&#xff1f;1.2 网络爬虫的应用领域1.3 网络爬虫面临的主要挑战 2. 代理IP&#xff1a;爬虫的得力助手2.1 代理IP的定义和工作原理2.2 为什么爬虫需要代理IP&#xff1f;2.3 代理IP如何解决爬虫的常见问题&#xff1f; 3. 代理IP的…

动静资源的转发操作

目录 Nginx中的location指令 静态资源的转发 动态资源的转发 注意事项 深入研究 如何在Nginx中实现对特定后缀文件的静态资源进行反向代理&#xff1f; Nginx中location指令的优先级是怎样确定的&#xff1f; 为什么在使用proxy_pass时要区分是否带有斜杠&#xff1f; N…

《古陶瓷有意思》:从文物保护到古陶瓷审美

腰骏驰&#xff0c;文物保护工作者&#xff0c;奥尔梅克海外文物回流俱乐部创始人&#xff0c;北京邢定文物商店总经理&#xff0c;《古陶瓷有意思》作者&#xff0c;古陶瓷学者、资深文物经纪人。参加多次大型窑址、古战场、古代墓葬发掘活动。参与多项省市级文物保护巡视巡察…

1.Spring Boot 简介(Spring MVC+Mybatis-plus)

文章目录 一&#xff0c;Spring Boot 简介二&#xff0c;搭建springboot项目并整合mybatis-plus框架1.pom导依赖2.添加启动项3.配置文件.yml 三&#xff0c;springboot集成 Spring MVC1.springmvc定义2.应用注解 一&#xff0c;Spring Boot 简介 SpringBoot是Spring的子工程(或…