【设计模式】从游戏角度开始了解设计模式 --- 创建型模式(一)

发布于:2025-09-03 ⋅ 阅读:(20) ⋅ 点赞:(0)

在这里插入图片描述

永远记住,你的存在是有意义的,
你很重要,
你是被爱着的,
而且你为这个世界带来了无可取代的东西。
-- 麦克西 《男孩、鼹鼠、狐狸和马》--

1 面向对象六大原则

面向对象原则是在语法规则与设计模式之间的一种规则:对于语法规则,这是必须进行遵守的,否则程序无法编译通过;对于设计模式,你可以使用,也可以不使用,其作用是优化程序结构,并且同一场景不会使用所有的设计模式。

而面向对象的六大原则,你可以不遵守,但是如果你违背这些规则,从设计上讲是绝对错误的,会让程序蕴含不可避免的危险。

六大原则内容是:

  1. Single ResponsibilityPrinciple:单一职责原则
  2. Open/Closed Principle:开闭原则
  3. LiskovSubstitutionPrinciple:里氏替换原则
  4. InterfaceSegregationPrinciple:接口隔离原则
  5. DependencyInversionPrinciple:依赖倒置原则
  6. LawofDemeter:迪米特法则

下面依次来介绍

单一职责就是要求一个类只能承担一种功能。那么为什么要遵守这个规则呢?

  • 降低耦合度:一个类只做一个事情可以大大降低代码的耦合度,出现问题可以很好的进行定位与修复。
  • 增强可读性:每个类只做一个事情,可以构建出简单单一的类结构,使得后期的修改与扩展可以变得简单。
  • 便于代码复用:每个类只做一件事情,后期有场景想要构建一个多功能类可以直接复用代码。功能拆分得越细,越容易复用。

开闭原则是指“对扩展开放,对修改封闭”。也就是说如果需要再原有基础上添加新功能或者是修改功能,应该添加新的代码而不是对原有代码进行修改。这样的好处显而易见,可以避免因为旧逻辑的改动影响旧代码,避免出现雪崩式的崩溃。后面设计模式中也会涉及这个思想,比如:“父类决定算法框架,子类决定算法步骤

里氏替换原则是指所有父类出现的地方都可以使用子类进行替换,而不会改变代码的正确性。这与面向对象中继承的思想一致:子类是父类更具象化的描述,而遵循与父类相同的功能。该原则可以保证继承体系的正确性,如果子类不完全遵循父类契约,调用者可能会遇到意想不到的问题;同时也可以提高代码复用,可以使得多态机制充分发挥作用,增强系统扩展性。里氏替换规则有以下几条要求:

  1. 子类方法的参数类型必须与其父类的参数类型相匹配或更加抽象:参数类型一致是基本的多态机制,而更加抽象是指子类重写方法时可以允许传入父类参数
  2. 子类方法的返回值类型必须与父类方法的返回值类型或是其子类别相匹配:返回值类型一致是基本的多态机制,该原则可以保证在切换子类或者父类时无需修改代码
  3. 子类中的方法不应抛出基础方法预期之外的异常类型:子类抛出的异常如果超出预期,可能会突破防御机制,导致崩溃。
  4. 子类不应该加强其前置条件:子类不能对重写方法的参数新增限制:比如基类描述了一个参数时int类型,但是子类要求其必须为正数,否则抛出异常,显然是违背规则的
  5. 子类不能削弱其后置条件:子类操作逻辑必须与父类一致,举个例子:如果基类进行了文件读写,并在读写结束后关闭文件句柄,那么子类在重写时也必须遵从这个规则,而不能单独设置一个关闭句柄接口
  6. 父类的不变量必须保留:这里的不变量不是语法上的常量,而是形式上的规则。比如定义猫这个基类,有4个腿1条尾巴,那么所有品种的猫都应该遵从这个规则。这可以确保
  7. 子类不能修改父类中私有成员变量的值:虽然说语法层面可以限制住,先不说c/c++可以通过内存偏移访问到私有变量不说,像是python、lua,JavaScript这些语言中私有变量并没有进行严格的保护!所以子类要避免修改父类私有变量。

接口隔离规则的核心也是在于‘拆’,与其搞一个大而全的接口,不如细分成不同的功能接口。

依赖倒置原则是指高层次的类不能对低层次的类产生依赖,而是将二者的依赖转嫁到抽象接口之上。所谓高层次低层次是指软件层面上的业务与底层架构,低层次的类比如文件IO,网络通信等,高层次的类就是真实的业务逻辑,比如按钮响应,界面显示等。依赖倒置原则比较经典的例子:我们设计一个游戏,一开始依赖TCP进行通信,后期想要更改为UDP,那么如果业务对TCP产生了依赖,那么更改将会是一个大工作。所以应该是将TCP包装成通信类,提供规范接口,更改为UDP时只需修改通信类逻辑即可。

迪米特法则通常叫做“最少知识原则”,也就是要求一个对象只与最直接关联的对象进行通信,避免产生链式依赖。这个原则通过减少对象之间的依赖,防止某个对象的变化影响过多的其他对象,降低耦合性。且便于测试与维护。

2 创建型模式

创建型模式关注创建对象的过程,根据不同场景所产生的设计模式(具有不同的机制与步骤),大幅度提升代码复用性。有以下几种

  1. 工厂方法模式:在父类中提供一个创建的对象的接口,在子类中决定具体实例化什么样的对象
  2. 抽象工厂模式:是在工厂方法模式上增加一个维度的工厂,类似一维数组与二维数组的关系。抽象工厂方法可以更灵活的创建多个类别的一组产品
  3. 建造者模式:也叫做生成器模式,通常应用在复杂对象的构造中。将对象构造的逻辑从他本身中抽离出来,使用专门的生成器来负责
  4. 原型设计模式:提供了无依赖的对象复制,可以在不了解对象内部结构的情况下复杂对象。
  5. 单例模式:单例模式可以看做是全局属性的一种面向对象管理策略,确保单例类只能有一个实例,并且提供可以访问这个实例的全局节点。

3 工厂方法模式

游戏开发中通常都会有装备系统,这里假设我们有武器与防具。武器和防具都有不同的属性与品质等私有字段,这里我们就可以借助武器装备工厂生产武器对象,根据游戏状态或者玩家操作动态地创建具体的装备对象

// 武器方法基类
class Weapon{
public:
	//纯虚函数 武器的使用
	virtual void use() = 0;	
}
class Sword : public Weapon{
public:
	void use(){
		//弓箭使用逻辑
	}
}
class Bow : public Weapon{
public:
	void use(){
		//剑使用逻辑
	}
}

对于上面的场景,如果我们不考虑工厂设计模式,那么是可以直接实例化弓箭类或者剑类对象。而如果要使用工厂方法基类,就要创建工厂基类,以及对象工厂类,使用对象工厂创建对应的对象。

class WeaponFactory{
public:
	// 纯虚函数 武器的创建
	virtual Weapon* createWeapon() = 0;
}
// 弓箭工厂
class SwordFactory : public WeaponFactory{
public:
	Weapon* createWeapon(){
		//剑创建逻辑
	}
}
// 武器工厂
class BowFactory : public WeaponFactory{
public:
	Weapon* createWeapon(){
		//弓箭创建逻辑
	}
}

工厂方法模式的关键要素:

  1. 抽象产品:如武器Weapon基类
  2. 具体产品:如剑与弓箭子类
  3. 抽象工厂:抽象工厂不直接参与对象创建,而是定义一套接口,所有创建者返回的对象类型都必须是与产品接口相匹配
  4. 具体工厂:重写抽象工厂的创建方法,实例化不同的产品对象进行返回。

这里有个疑问,为什么要单独设计出一个工厂类,明明可以通过直接构造对应的对象。工厂不过是是将构造对象的过程迁移到了另外一个地方,这样做有什么优势吗?

  • 解耦客户端与具体类的依赖:比如我们只需要让玩家类依赖工厂类,而无需关注产品列内部实现或者构造需求。这样我们可以独立的变更玩家类与武器类,十分优雅。
  • 提高系统的可扩展性:当引入新的产品类时,我们只需要创建新的具体工厂类,不需要改变角色代码或者已有的工厂方法。
  • 封装复杂的对象创建逻辑:工厂类内部屏蔽对象创建和初始化的复杂操作(真实场景下的产品创建可能逻辑很重),让外部感知不到。
  • 遵循了开闭原则与单一职责原则:武器类与工厂类各司其职,将具体产品的代码与使用该产品的客户端内容分离
  • 提高代码可维护性:我们将所有的武器创建集中在了一起,避免了武器对象构造和初始化散落在玩家各处,方便统一管理。

开发场景中还有以下场景可以使用到工厂模式:

  1. 日志系统:提供打印到控制台,文件,网络和数据库的多种方法
  2. 数据库的连接驱动:提供连接到MySQL,SQLite,postgreSQL等不同数据库的驱动层
  3. 文档生成:将同一份内容生成HTML,pdf,MArkdown等不同格式

同时工厂方法模式也有缺点:

  1. 过多的类,过度设计增加简单对象的创建复杂度
  2. 单独的工厂方法模式无法应对复杂的产品族与变种(需要使用抽象工厂模式使用)

网站公告

今日签到

点亮在社区的每一天
去签到