Qt开发实战3-现代应用程序设计
常用设计模式介绍
设计模式是软件设计中常见问题的典型解决方案。 每个模式就像一张蓝图, 你可以通过对其进行定制来解决代码中的特定设计问题。模式分为两种:惯用技巧和架构模式。最基础的、底层的模式通常被称为惯用技巧。这类模式一般只能在一种编程语言中使用。而更通用的、高层的模式是架构模式。开发者可以在任何编程语言中使用这类模式。与其他模式不同,它们可用于整个应用程序的架构设计。我们讲述的主要是后者即架构模式。那么我们为什么要遵循这种架构模式,或者说我们使用它有什么好处呢?主要出于以下两个方面的考虑:
- 代码复用
- 扩展性
代码复用是减少开发成本时最常用的方式之一。其意图非常明显:与其反复从头开发,不如在新对象中重用已有代码。 我们都知道利用已有的技术进行拓展开发出新的技术,这种发展是最快的,也是成本最低的。对程序开发来说也是一样的,究其根本,每一种新技术的产生绝不是拍拍脑门,凭空产生的。新技术一定是经过长时间的观察、思考乃至不断演进已有的技术从而得以发展的。我们在程序开发过程中一定要注重代码的复用性。
变化是唯一不变的真理。对程序开发来说也是如此,即使我们一段时间内不会有新的需求出现,我们也会“想方设法”为自己铺设需求,即软件的自研(内研)需求。软件是不断变化,不断发展的。在这段时间内代码的架构设计也许是良好的,但是随着时间推移,代码的味道就会慢慢变质,脱离了当初的设计。或者说技术日益发展,当初我们的技术俨然已经不适用于新的业务场景,我们需要寻找一种更适合的框架以满足我们未来更复杂的需求。综上,我们需要掌握一些设计模式,从总体上把握程序开发。
单例模式(Singleton)
概念
只具备一个实例对象的类的设计模式称为单例模式。
单例模式保证了一个类只具有一个实例,并且从外部只有一个全局接口用于访问该实例。
举个例子来说:一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么, “某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。
使用场景
- 如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式
- 需要更加严格地控制全局变量, 可以使用单例模式
实现方法
- 在类中添加一个私有静态成员变量用于保存单例实例。
- 声明一个公有静态构建方法用于获取单例实例。
- 在静态方法中实现延迟初始化。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
伪代码实现延迟初始化:
if(instance == null)
instance = new Instance
return instance
- 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。
- 整理:检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。
工厂模式
概念
定义创建对象的接口,但让实现这个接口的类来决定实例化哪个类,将类的实例化推迟到子类中进行。
可以形象理解为只生产模具,但不进行加工的设计模式
使用场景
- 如果无法预知对象确切类别及其依赖关系时, 可使用工厂方法。
- 希望用户能扩展你软件库或框架的内部组件, 可使用工厂方法。
- 希望复用现有对象来节省系统资源, 而不是每次都重新创建对象, 可使用工厂方法。
实现方法
- 将类的接口设计为更具有抽象意义的概念,以便尽可能的让所有实例都遵循同一接口。 且该接口对实例具有实际意义。
- 在创建类中添加一个空的工厂方法。 该方法的返回类型必须遵循通用的产品接口。
- 整理:在创建者代码中找到对于产品构造函数的所有引用。 将它们依次替换为对于工厂方法的调用, 同时将创建产品的代码移入工厂方法。 你可能需要在工厂方法中添加临时参数来控制返回的产品类型。
- 为工厂方法中的每种产品编写一个创建者子类, 然后在子类中重写工厂方法, 并将基本方法中的相关创建代码移动到工厂方法中。
// 创建者类声明的工厂方法必须返回一个产品类的对象。创建者的子类通常会提供
// 该方法的实现。
class Dialog is
// 创建者还可提供一些工厂方法的默认实现。
abstract method createButton():Button
// 请注意,创建者的主要职责并非是创建产品。其中通常会包含一些核心业务
// 逻辑,这些逻辑依赖于由工厂方法返回的产品对象。子类可通过重写工厂方
// 法并使其返回不同类型的产品来间接修改业务逻辑。
method render() is
// 调用工厂方法创建一个产品对象。
Button okButton = createButton()
// 现在使用产品。
okButton.onClick(closeDialog)
okButton.render()
// 具体创建者将重写工厂方法以改变其所返回的产品类型。
class WindowsDialog extends Dialog is
method createButton():Button is
return new WindowsButton()
class WebDialog extends Dialog is
method createButton():Button is
return new HTMLButton()
// 产品接口中将声明所有具体产品都必须实现的操作。
interface Button is
method render()
method onClick(f)
// 具体产品需提供产品接口的各种实现。
class WindowsButton implements Button is
method render(a, b) is
// 根据 Windows 样式渲染按钮。
method onClick(f) is
// 绑定本地操作系统点击事件。
class HTMLButton implements Button is
method render(a, b) is
// 返回一个按钮的 HTML 表述。
method onClick(f) is
// 绑定网络浏览器的点击事件。
class Application is
field dialog: Dialog
// 程序根据当前配置或环境设定选择创建者的类型。
method initialize() is
config = readApplicationConfigFile()
if (config.OS == "Windows") then
dialog = new WindowsDialog()
else if (config.OS == "Web") then
dialog = new WebDialog()
else
throw new Exception("错误!未知的操作系统。")
// 当前客户端代码会与具体创建者的实例进行交互,但是必须通过其基本接口
// 进行。只要客户端通过基本接口与创建者进行交互,你就可将任何创建者子
// 类传递给客户端。
method main() is
this.initialize()
dialog.render()
适配器模式
概念
适配器模式把一个接口转换成客户端所期待的另一个接口,从而使原本因接口不匹配而无法在一起工作的两个概念能够在一起工作。
可以形象理解为将两个原来不搭边或者联系不紧密的人,通过中间人介绍、联系,从而使两人建立某种联系。
使用场景
- 使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类
- 需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。
实现方法
- 声明一个接口, 该接口描述如何与另一个类进行交互。
- 创建遵循该接口的适配器类。 所有方法暂时都为空。
- 在适配器类中添加一个成员变量用于保存对于另一个类的引用。
- 依次实现适配器类接口的所有方法。
- 整理:后续都通过该接口使用适配器。
// 假设你有两个接口相互兼容的类:圆孔(RoundHole)和圆钉(RoundPeg)。
class RoundHole is
constructor RoundHole(radius) { ... }
method getRadius() is
// 返回孔的半径。
method fits(peg: RoundPeg) is
return this.getRadius() >= peg.getRadius()
class RoundPeg is
constructor RoundPeg(radius) { ... }
method getRadius() is
// 返回钉子的半径。
// 但还有一个不兼容的类:方钉(SquarePeg)。
class SquarePeg is
constructor SquarePeg(width) { ... }
method getWidth() is
// 返回方钉的宽度。
// 适配器类让你能够将方钉放入圆孔中。它会对 RoundPeg 类进行扩展,以接收适
// 配器对象作为圆钉。
class SquarePegAdapter extends RoundPeg is
// 在实际情况中,适配器中会包含一个 SquarePeg 类的实例。
private field peg: SquarePeg
constructor SquarePegAdapter(peg: SquarePeg) is
this.peg = peg
method getRadius() is
// 适配器会假扮为一个圆钉,
// 其半径刚好能与适配器实际封装的方钉搭配起来。
return peg.getWidth() * Math.sqrt(2) / 2
// 客户端代码中的某个位置。
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true
small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // 此处无法编译(类型不一致)。
small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false
观察者模式
概念
观察者模式是一种对象行为模式。它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
可以形象理解为RSS订阅,我们都知道网络上存在着一种RSS订阅的方式以获取自己感兴趣的信息。这个过程就是我们首先在网站上订阅某一主题,然后当信息主题发布了新的内容后,所有订阅该主题的读者都能收到这一更新。
使用场景
- 当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式
- 当应用中的一些对象必须观察其他对象时, 可使用该模式。
实现方法
- 整理:检查你的业务逻辑, 试着将其拆分为两个部分: 独立于其他代码的核心功能将作为发布者; 其他代码则将转化为一组订阅类
- 声明订阅者接口。 该接口至少应声明一个类似Update的方法。
- 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。
- 确定存放实际订阅列表的位置并实现订阅方法。
- 创建具体发布者类。
- 在具体订阅者类中实现通知更新的方法。
- 生成所需的全部订阅者, 并在相应的发布者处完成注册工作。
// 发布者基类包含订阅管理代码和通知方法。
class EventManager is
private field listeners: hash map of event types and listeners
method subscribe(eventType, listener) is
listeners.add(eventType, listener)
method unsubscribe(eventType, listener) is
listeners.remove(eventType, listener)
method notify(eventType, data) is
foreach (listener in listeners.of(eventType)) do
listener.update(data)
// 具体发布者包含一些订阅者感兴趣的实际业务逻辑。我们可以从发布者基类中扩
// 展出该类,但在实际情况下并不总能做到,因为具体发布者可能已经是子类了。
// 在这种情况下,你可用组合来修补订阅逻辑,就像我们在这里做的一样。
class Editor is
public field events: EventManager
private field file: File
constructor Editor() is
events = new EventManager()
// 业务逻辑的方法可将变化通知给订阅者。
method openFile(path) is
this.file = new File(path)
events.notify("open", file.name)
method saveFile() is
file.write()
events.notify("save", file.name)
// ...
// 这里是订阅者接口。如果你的编程语言支持函数类型,则可用一组函数来代替整
// 个订阅者的层次结构。
interface EventListener is
method update(filename)
// 具体订阅者会对其注册的发布者所发出的更新消息做出响应。
class LoggingListener implements EventListener is
private field log: File
private field message
constructor LoggingListener(log_filename, message) is
this.log = new File(log_filename)
this.message = message
method update(filename) is
log.write(replace('%s',filename,message))
class EmailAlertsListener implements EventListener is
private field email: string
constructor EmailAlertsListener(email, message) is
this.email = email
this.message = message
method update(filename) is
system.email(email, replace('%s',filename,message))
// 应用程序可在运行时配置发布者和订阅者。
class Application is
method config() is
editor = new Editor()
logger = new LoggingListener(
"/path/to/log.txt",
"有人打开了文件:%s");
editor.events.subscribe("open", logger)
emailAlerts = new EmailAlertsListener(
"admin@example.com",
"有人更改了文件:%s")
editor.events.subscribe("save", emailAlerts)
Qt软件开发框架
现代软件架构五花八门,但究其本质,是某些层级上拆分出更细的子层级来,但大多数都分为以下三层:
展现层(UI)、网络通讯层、业务逻辑处理层。在业务逻辑处理层可能又细分为核心业务层、中间层、其他逻辑处理层。接下来详细介绍下各自的职责。
- 展现层(UI):负责处理来自业务逻辑层或者其它模块的数据展示,尽量不足或少做业务逻辑的处理。
- 网络通讯层:处理http或者https协议,包含Get/Post请求、返回等。主要采用Socket通讯,各平台或环境在通讯库的选择上可能有所不同,但底层基本都遵循Socket,如HttpClient、WinHttp、WebSocket等。
- 业务逻辑处理层:核心业务层主要处理的是网络模块返回数据的处理;中间层作为与各层间的”中转站“负责和其他模块进行连接;其他逻辑处理层处理一些边边角角的功能,主要有日志的记录、崩溃的处理、守护进程、客户端软件的更新等等功能模块。
优秀的开源Qt软件介绍
这里选取的开源软件在Github上基本都有3K以上Stars,且涉及各类型的软件,我们可以从中学习其软件的设计思想。
Tiled
Git:https://github.com/mapeditor/tiled
Tiled 2D地图编辑器是一种用于通用目的的编辑器。风格类似mini版的photoshop。它可以用于制作多种类型的游戏引擎需要,而且支持使用插件读写map、增加用于引擎的map格式。原先该软件是Java开发的,目前已转为Qt。
Liteide
Git:https://github.com/visualfc/liteide
LiteIDE 是一款简单,开源,跨平台的 Go IDE。
Clementine
Git:https://github.com/clementine-player/Clementine
Clementine 是一款现代音乐播放器和媒体库管理器。Clementine 是一款多平台音乐播放器。它的灵感来源于 Amarok 1.4, 致力于开发一个易于使用的界面,令您能够快速地搜索和播放您的音乐。
Shotcut
Git:https://github.com/mltframework/shotcut
Shotcut是一款免费、开源且支持多平台的视频编辑器。
QtAV
Git:https://github.com/wang-bin/QtAV
基于Qt和FFmpeg的跨平台高性能音视频播放框架。
qTox
Git:https://github.com/qTox/qTox
qTox 是一款强大的 Tox 客户端,使用 Qt 开发,支持主流操作系统。qTox(安全的P2P聊天工具)是一款来自国外功能非常强大的安全的P2P聊天工具,它最大的好处是可以在各种设备上相互连通使用,包括IOS系统和LINUX系统,功能实用且安全,可以将聊天记录保存在自己电脑,不用担心信息被泄露。