设计模式原则
设计模式主要作用为以下几种:
(1)重用设计,重用设计比重用代码更有意义,它会自动带来代码的重用
(2)为设计提供共同的词汇,每个模式名就是一个设计词汇,其概念是的程序员间的交流更加方便。
(3)在开发文档中采用模式词汇可以让其他人更容易理解你的想法和做法,编写开发文档也更方便。
(4)应用设计模式可以让重构系统变得容易,可以确保开发正确的代码,并降低在设计或实现中出现的错误的可能。
(5)支持变化,可以为重写其他应用程序提供很好的系统架构。
(6)正确设计模式,可以节省大量时间。
使用设计模式的根本原因是适应变化,提高代码复用率,使软件更具有可维护性和可扩展性。
在进行设计的时候,我们需要遵循以下几个原则:单一职责原则、开闭原则、里氏替代原则、依赖倒置原则、接口隔离原则、合成复用原则和迪米特法则。
这些设计原则并非独立存在的,他们相互依赖,相互补充。
单一职责原则
专业的人做专业的事,面向对象编程中类也是一样,一个类只负责一些特定的职责。
如User类只负责用户相关的业务功能,Order类只负责订单相关的功能,想象下如果我们把用户和订单的功能放在一个类,然后去设计用户的权限中会怎么样呢?订单业务和权限耦合了。
单一职责原则作用是降低职责间的耦合,同时提高代码的复用率。
接口隔离原则
接口隔离原则指的是提倡使用多个专门的接口,而不是使用一个总的接口。
以一个订单系统为例,我们自己的系统可以对订单进行增删改查操作,给第三方只提供了查询操作。
如果把增删改查的功能都定义在一个接口中那么第三方的实现类也要做增删改的实现。分成两个接口就避免了这种情况。
public interface IOrderService1
{
void Add();//添加
void Delete();//删除
void Edit();//修改
}
public interface IOrderService2
{
void Query();
}
//我们用的订单服务类
public class MyOrderService : IOrderService1, IOrderService2
{
public void Add(){//添加操作}
public void Delete(){//删除操作}
public void Edit() {//修改操作}
public void Query(){//查询操作}
}
//给第三方提供的订单服务类
public class OtherOrderServeice : IOrderService2
{
public void Query(){//查询操作}
}
开闭原则
开闭原则通俗的讲就是对添加(扩展)开放,对修改关闭。
如我们维护一个软件系统,这时要添加一个新功能,我们可以写新代码来添加这个功能,但是不能修改以前的代码,想象下我们为了新功能修改了一些旧代码,然后发现一些旧的功能挂掉了时的心情,这也是为什么我们要遵循开闭原则的原因。
里式替换原则
里氏替代原则指的是父类存在的地方,子类都可以替换,并且行为不发生改变,只有所有的父类都可以被子类替代且行为不发生变化时才能说明父类是可复用的。
一个违背里氏替换原则的例子:我们定义了鸟类有fly方法,表示鸟类都可以飞,但是企鹅也是鸟类而且企鹅不会飞,这时企鹅(子类)就不能替代其鸟类(父类)了,说明我们设计的鸟类违背了里氏替代原则。
合成复用原则
合成复用指的是一个对象A包含了另一个对象B,那么A就可以委托B来使用B的功能。
在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不用继承关系
下边的例子中订单中包含了用户,我们可以使用order.User.getName()来调用User类中的方法。
//订单类
public class Order
{
public User user;//订单中有用户
}
//用户类
public class User
{
public string UserName { get; set; }
public string getName()
{
return this.UserName;
}
}
依赖倒置原则
依赖倒置原则作用是方便实现类的切换,降低业务要求和具体实现间的耦合。
抽象不应该依赖细节,而细节应该依赖抽象,这就是我们为什么使用面向接口编程,而不是面向实现编程的原因。
迪米特原则(最少知道原则)
迪米特法则指的是各个类之间应该尽可能的不了解,最好是相互独立的。
但是很多时候类与类之间不可避免的要传输信息,如上边的Order和User就存在强依赖关系,我们应该尽可能地降低类和类间的依赖。怎么降低依赖呢?一种常用的方式是通过中间件(如redis/rabbitmq/autofac等)来降低依赖。
如A和B类要通信,原始的方式是new一个实例出来直接通信 A<-->B ,现在我们用一个中间件充当传话人那么A和B通信就变成了 A<-->中间件<-->B 形式,这样A和B就降低了耦合。
一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生简介交换。
个人理解精简版
单一职责原则
专业的人做专业的事情,一个类值负责特定的职责。
否则互相调用就会让代码耦合度增加。
接口隔离原则
使用多个接口,对于不同的需求给不同的接口。
比如说我们给普通成员查询权限,给公会会长增删改查权限,那么把增删改查四个功能分为两个接口提高系统的耦合性。
里式替换原则
子类需要满足父类的所有条件。
比如企鹅类继承自鸟类,企鹅是鸟但是不会飞,如果鸟类写了飞的方法,就不符合历史转换原则。
合成复用原则
我们要少使用继承,而多使用对象的组合来达到代码复用的目的。
比如你要用父类的某一个功能, 你直接实例化一个对象然后调用就好了,而不是去继承他然后直接调用,因为会增加耦合性。
依赖倒置原则
要面向接口编程,而不是面向实现编程。
比如我们写一个功能,只需要他抽象出来的接口确定,就算实现接口的细节不断改变,只要抽象不变,程序就不会有太大变动
开闭原则
对于一个功能,我们应该对他的拓展开启,而不允许修改。
如果不使用开闭原则,我们实现新功能的时候需要修改老的代码,可能造成老的功能出问题。
迪米特原则
又被称为知道最少原则,每个类要尽可能的独立。
比如我们使用mvc的时候m和v相互独立,只通过c层来通信,提高了可维护性。
设计模式分类
创造性模式:
- 静态工厂模式
- 抽象工厂模式
- 单例模式
- 建造者模式
- 原形模式
结构型模式:
- 适配器模式
- 装饰器模式
- 代理模式
- 外观模式
- 桥接模式
- 组合模式
- 享元模式
行为型模式:
- 策略模式
- 模板方法模式
- 观察者模式
- 迭代子模式
- 责任链模式
- 命令模式
- 备忘录模式
- 状态模式
- 访问者模式
- 中介者模式
- 解释器模式
单例模式
- 类只有一个实例,比如一些单一功能的管理器
- 方便的引用类的方法,可以快速获取对象
- 同时间只存在一个对象
例子:想要在enemy类中获取到player的信息
下面的例子使用普通方法、在Awake中instance = this、定义属性、单例模板类等方法创建了单例模式
public class Player : MonoBehaviour
{
//单例模式
public static Player instance;
//单例方法1,直接在Awake中写
private void Awake()
{
//instance = this;
}
//单例方法2,定义静态属性
public static Player Instance
{
get
{
//如果instance没有被指定,则在场景中找一个带着Player的gameboject
if (instance == null)
{
instance = FindObjectOfType<Player>();
}
//如果场景中没有,则自己创建一个(一般不这样写,因为场景内玩家应该提前被创建好)
if (instance == null)
{
GameObject obj = new GameObject();
obj.AddComponent<Player>();
instance = obj.GetComponent<Player>();
}
return instance;
}
}
string name_ = "player";
public void sayHello()
{
Debug.Log("I am " + name_);
}
}
//NET支持的类型参数约束有以下五种:
//where T : struct | T必须是一个结构类型
//where T : class | T必须是一个Class类型
//where T : new() | T必须要有一个无参构造函数
//where T : NameOfBaseClass | T必须继承名为NameOfBaseClass的类
//where T : NameOfInterface | T必须实现名为NameOfInterface的接口
/// <summary>
/// 单例模板类
/// </summary>
/// <typeparam name="T">任意类型</typeparam>
public class UnitySingleton<T> : MonoBehaviour
where T : Component
{
private static T instance;
public static T Instance
{
get
{
if(instance == null)
{
instance = FindObjectOfType(typeof(T)) as T;
}
if (instance == null)
{
GameObject obj = new GameObject();
obj.AddComponent<T>();
//obj.hideFlags = HideFlags.HideAndDontSave;//隐藏实例化的gameobject,些微提高安全性防止误删
instance = obj.GetComponent<T>();
}
return instance;
}
}
}
//使用模板类
public class PlayerC : UnitySingleton<PlayerC>
{
string playerName = "wang";
public void sayBye()
{
Debug.Log(playerName + " byebye");
}
}
public class Enemy : MonoBehaviour
{
//普通方法
//GameObject player;
void Start()
{
//普通方法
//player = GameObject.Find("Player");
//Playerc = player.GetComponent<Player>();
}
//单例方法
void Update()
{
//普通方法
//Debug.Log(player.transform.position);
//Playerc.sayHello();
//单例方法1
//Debug.Log(Player.instance.transform.position);
//Player.instance.sayHello();
//单例方法2
Debug.Log(Player.Instance.transform.position);
Player.Instance.sayHello();
//单例模板类
Debug.Log(PlayerC.Instance.name);
PlayerC.Instance.sayBye();
}
}
观察者模式
- 观察者模式【Observer】定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。
- 这个主题对象在状态发生改变时,会通知所有观察者对象,使他们能够自动更新自己。
eg:一个频道,有若干个观察者(订阅者),频道更新时,观察者都收到信息
//观察者类,在每个观察者下挂在此脚本
public class NetUser : MonoBehaviour
{
void Start ()
{
YouKuZHJ.Instance.zhjDel += Show;//订阅操作,将该对象加入观察者列表中,之后调用事件时会调用该类的Show函数
}
private void Show(string info)
{
Debug.Log(gameObject.name + "收到新消息:" + info);
}
}
//被观察者类,被称之为主题【subject】,或者被称为发布者
public delegate void ZHJDelegate(string info);
public class YouKuZHJ : MonoBehaviour
{
public static YouKuZHJ Instance;//被观察对象是一个单例模式
private int index = 1;//当前剧集.
public event ZHJDelegate zhjDel;//定义事件.
void Awake()
{
Instance = this;//实现单例
}
void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
index++;
zhjDel("更新到第" + index + "集");//调用这个事件
}
}
}
简单工厂模式
- 也被称为静态方法模式。
- 简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。
- 简单工厂模式是工厂模式家族最简单实用的模式
eg:我们以一个生产鼠标为例来分析简单工厂的作用,鼠标有两种:戴尔鼠标和惠普鼠标
//鼠标抽象类
public abstract class Mouse
{
public abstract void Print();
}
//戴尔鼠标
public class DellMouse : Mouse
{
public override void Print()
{
Console.WriteLine("生产了一个Dell鼠标!");
}
}
//惠普鼠标
public class HpMouse : Mouse
{
public override void Print()
{
Console.WriteLine("生产了一个惠普鼠标!");
}
}
/// <summary>
/// 鼠标工厂类
/// </summary>
public class MouseFactory
{
private Mouse mouse = null;
public Mouse CreateMouse(string brand)
{
switch (brand)
{
case "dell":
mouse = new DellMouse();
break;
case "hp":
mouse = new HpMouse();
break;
default:
break;
}
return mouse;
}
}
class Program
{
static void Main(string[] args)
{
//实例化一个工厂类
MouseFactory mouseFactory = new MouseFactory();
//通过工厂类创建鼠标
Mouse mouse1 = mouseFactory.CreateMouse("dell");
Mouse mouse2 = mouseFactory.CreateMouse("dell");
Mouse mouse3 = mouseFactory.CreateMouse("dell");
Mouse mouse4 = mouseFactory.CreateMouse("dell");
Mouse mouse5 = mouseFactory.CreateMouse("dell");
mouse1.Print();
Console.ReadKey();
}
}
简单工厂的优点:
- 简单工厂可以有效地降低客户端和具体对象的耦合,将new具体对象的任务交给了一个简单工厂类
- 可以有效的进行代码复用,如客户端A和客户端B都需要一个具体对象,客户端A和客户端B可以通过同一个简单工厂来获取具体类型的实例
简单工厂的缺点:
- 一定程度上违背了开闭原则,在新增产品时需要修改简单工厂类
工厂模式
- 由于简单工厂违反了开闭原则,因此工厂模式就是为了解决这一缺陷而出现的。
- 工厂模式把创建具体实例的任务放在了工厂的子类中,工厂只提供了创建实例的的接口。
eg:与简单工厂一样
//鼠标类不变
/// <summary>
/// 鼠标工厂抽象类
/// </summary>
public abstract class MouseFactory
{
public abstract Mouse CreateMouse();
}
//戴尔鼠标工厂
public class DellMouseFactroy : MouseFactory
{
public override Mouse CreateMouse()
{
return new DellMouse();//在具体的工厂中实例化产品
}
}
//惠普鼠标工厂
public class HpMouseFactory : MouseFactory
{
public override Mouse CreateMouse()
{
return new HpMouse();//在具体的工厂中实例化产品
}
}
class Program
{
static void Main(string[] args)
{
//生产一个戴尔鼠标
MouseFactory dellMouseFactory = new DellMouseFactroy();
Mouse dellMouse= dellMouseFactory.CreateMouse();
dellMouse.Print();
//生产一个惠普鼠标
MouseFactory hpMouseFactory = new HpMouseFactory();
Mouse hpMouse = hpMouseFactory.CreateMouse();
hpMouse.Print();
Console.ReadKey();
}
}
工厂模式优点:
- 工厂模式有效地解决了添加新产品必须要修改工厂类代码的问题,符合设计原则中的开闭原则。
工厂模式缺点:
- 工厂模式的本质是将具体实例的创建工作放在了具体子类工厂中进行,这造成一个新的问题:将选择实例类型的任务交给了客户端,如我们想生产一个戴尔鼠标,就必须在客户端new一个戴尔鼠标工厂。
- 想象下如果我们new了100个戴尔鼠标工厂,这是要换到惠普鼠标怎么办?只能把new DellMouseFactory一个一个地替换成new HpMouseFactory。
- 所以简单工厂和工厂模式都不是完美的,我们应该根据具体的情况来选择。
抽象工厂模式
- 为了解决系列产品的问题,出现了抽象工厂模式。
- 抽象工厂可以生产多种产品,而在简单工厂/工厂模式中只能生产一种产品。
eg:一个工厂同时生产鼠标和键盘
//鼠标类不变
//键盘抽象类
public abstract class Keybo
{
public abstract void Print();
}
//戴尔键盘类
public class DellKeybo : Keybo
{
public override void Print()
{
Console.WriteLine("生产了一个戴尔键盘!");
}
}
//惠普键盘
public class HpKeybo : Keybo
{
public override void Print()
{
Console.WriteLine("生产了一个惠普键盘!");
}
}
/// <summary>
/// Pc产品工厂抽象类
/// </summary>
public abstract class PcFactory
{
public abstract Mouse CreateMouse();
public abstract Keybo CreateKeybo();
}
//戴尔pc工厂
public class DellPcFactroy : PcFactory
{
public override Keybo CreateKeybo()
{
return new DellKeybo();
}
public override Mouse CreateMouse()
{
return new DellMouse();
}
}
//惠普pc工厂
public class HpPcFactory : PcFactory
{
public override Mouse CreateMouse()
{
return new HpMouse();
}
public override Keybo CreateKeybo()
{
return new HpKeybo();
}
}
class Program
{
static void Main(string[] args)
{
//生产一个戴尔鼠标/键盘
PcFactory dellFactory = new DellPcFactroy();
Mouse dellMouse= dellFactory.CreateMouse();
Keybo dellKeybo = dellFactory.CreateKeybo();
dellMouse.Print();
dellKeybo.Print();
//生产一个惠普鼠标/键盘
PcFactory hpFactory = new HpPcFactory();
Mouse hpMouse = hpFactory.CreateMouse();
Keybo hpKeybo = hpFactory.CreateKeybo();
hpMouse.Print();
hpKeybo.Print();
Console.ReadKey();
}
}
抽象工厂的优点:
- 抽象工厂具有工厂模式的优点,对添加系列产品符合闭合原则(工厂模式的系列产品只有一种)。
- 如要生产华硕的鼠标和键盘,只需要添加一个AsusPcFactory(继承与PcFactory),AsusMouse(继承Mouse),AsusKeybo(继承Keybo),就可以在客户端通过以下代码生产华硕鼠标和键盘:
抽象模式的缺点:
- 抽象模式对添加新产品不符合开闭原则。
- 如要生产显示器,不仅要添加显示器抽象类和显示器具体类,还需要修改PcFactory,在抽象工厂中添加一个CreateDisplay抽象方法(或接口),戴尔工厂和惠普工厂也要实现新增的抽象方法(或者接口)。
适配器模式
-
适配器模式的作用是将一个类的接口,转换成客户端希望的另外一种接口。
-
适配器作为原始接口(我们的类中本来具有的功能)和目标接口(客户端希望的功能)之间的桥梁。
-
举个例子:我们知道安卓数据线是不能给苹果手机充电的,但是我们没有苹果数据线,所以只能使用安卓数据线给苹果手机充电,这时怎么解决呢?通过一个转换头就可以了,这个转换头就是一个适配器。
-
在适配器模式中:安卓数据线是我们现有的类,充电是安卓数据线的功能,但是充电功能就是因为接口不兼容所以不适应新环境(给苹果手机充电),转换头作为苹果手机和安卓数据线之间的桥梁,让原有的充电功能使用于新环境(给苹果手机充电)。
-
适配器模式有两种类型:类适配器和对象适配器,类适配器通过多重继承实现接口的匹配,C#不支持多重继承,我们不考虑。我们主要介绍对象适配器。首先说明介绍适配器模式的三个角色:
- Adaptee:初始角色,实现了我们想要使用的功能,但是接口不匹配
- Target:目标角色,定义了客户端期望的接口
- Adapter:适配器角色,实现了目标接口。实现目标接口的方法是:内部包含一个Adaptee的对象,通过这个对象调用Adaptee的原有方法实现目标接口。(注:这里说的是对象适配器)
eg:typec数据线给苹果手机充电
/// <summary>
/// 安卓数据线,adaptee初始角色
/// </summary>
public class AndroidLine
{
public void AndroidCharge()
{
Console.WriteLine("安卓数据线充电....");
}
}
/// <summary>
/// 苹果手机充电接口,Target目标接口
/// </summary>
public interface IAppleCharge
{
void AppleChange();
}
/// <summary>
/// 苹果适配器,Adapter适配器角色
/// </summary>
public class AppleAdapter : IAppleCharge
{
AndroidLine androidLine = new AndroidLine();//适配器内部包含一个Adaptee对象
public void AppleChange()
{
androidLine.AndroidCharge();//客户端调用时,表面上是用的是AppleChange方法,本质还是用的AndroidCharge方法
}
}
class Program
{
static void Main(string[] args)
{
//获取一个target的实例
IAppleCharge appleAdapter = new AppleAdapter();
appleAdapter.AppleChange();
//表面上用的苹果充电方法AppleChange,本质上还是用的安卓充电方法AndriodChange。
Console.ReadKey();
}
}
适配器的使用场景:
- 系统想使用一个类,但是这个类的接口不符合系统的要求时使用适配器模式。
适配器模式的优点:
- 提高代码复用,复用了Adaptee中的方法
- 灵活性好,如没有关联的两个类A和类B,类A想借用类B的方法,可以在类A中添加一个类B的实例,然后调用类B实例的方法即可。
适配器模式的缺点:
- 过多使用适配器会让代码凌乱,如我们明明调用的是A接口的功能,但是却被适配成了B接口的功能。
状态模式
状态模式是一种行为型模式,在状态模式中,类的行为是基于它的状态改变的。
实际上就是对ifelse或者switch的一种优化,取代条件判断反之封装为一个个状态类,通过currentState统一管理,当该对象的状态发生改变时,我们只需要改变 currentState 所保存(引用)的对象,将 currentState 指向另一个状态类的对象,然后调用新的状态类对象的行为方法,就完成了状态的切换。
在 Unity 中的应用:有限状态机,可以用于角色状态(行走、攻击、死亡等)的切换
https://www.bilibili.com/read/cv17824042
命令模式
将一个请求封装一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志日志,以及支持可撤销的操作。
说人话就是用一个命令封装对象,以达到实现解耦(比如键盘输入和人物移动模块相耦合)、改变命令对象、撤销的功能
在Unity中的应用:角色的输入模块(比如WASD),需要储存某些指令的时候
https://www.bilibili.com/video/BV1WF411N7jS
https://github.com/WittyKyrie/UnityUtil/tree
策略模式
定义一组算法,将每个算法都封装起来,并且使它们之间可以相互转换
基本使用场景就是MMORPG的职业属性计算或者伤害公式计算,定义一个基础公式封装成类,然后根据接口的职业参数去调用不同的特化公式
与状态模式的区别:状态模式关注的是什么情况会导致状态的改变,策略模式更关注策略的本身
https://www.cnblogs.com/Yunrui-blogs/p/15189860.html
https://www.bilibili.com/video/BV1vT4y1E7g6
资料来源
https://www.cnblogs.com/wyy1234/
https://www.bilibili.com/video/BV1m741157EK
http://www.mkcode.net/html/csharp_jc/zhongji/delegate.html
https://blog.csdn.net/Vblegend_2013/article/details/79218107
https://www.cnblogs.com/zhaoqingqing/p/4288454.html
https://www.bilibili.com/read/cv17824042
https://www.bilibili.com/read/cv17764713
https://www.bilibili.com/video/BV1WF411N7jS
https://www.cnblogs.com/Yunrui-blogs/p/15189860.html
https://www.bilibili.com/video/BV1vT4y1E7g6
https://www.bilibili.com/read/cv15897739?spm_id_from=333.999.0.0