@TOC
快速掌握只需要看 代码理解与总结 就够了
一、设计模式
1.设计模式简介
每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。
设计模式描述了软件设计过程中某一类常见问题的一般性的解决方案。
2.七大设计原则
二、面向对象设计原则
1.面向对象设计模式与原则
面向对象设计模式描述了面向对象设计过程中、特场景下、类与相互通信的对象之间常见的组织关系
❓对象是什么 ?
- 从概念层面讲,对象是某种拥有责任的抽象
- 从规格层面讲,对象是一系列可以被其他对象使用的公共接口
- 从语言实现层面来看,对象封装了代码和数据
❓有了这些认识之后,怎样才能设计“好的面向对象”?
- 遵循一定的面向对象设计原则
- 熟悉一些典型的面向对象设计模式
2.编程设计原则
针对接口编程,而不是针对实现编程
客户端无需知道所使用对象的特定类型。只需要知道对象拥有客户端所期望的接口。
比方说:假设我们有一个动物园,里面有各种动物,如狮子、老虎、熊等。每种动物都有自己的叫声。如果我们针对实现编程,那么当我们想听到动物的叫声时,我们需要为每种动物创建一个方法。但是,如果我们针对接口编程,我们只需要创建一个接口,比如Animal,并在接口中定义一个方法makeSound()。然后,每种动物都实现这个接口,并提供自己的makeSound()方法的实现。这样,无论我们添加多少种动物,我们都可以使用相同的方法来听到它们的叫声。
优先使用对象组合,而不是类继承
类继承通常为"黑箱复用",对象组合通常为"黑箱复用"。继承在某种程度上破坏了封装性,子类父类耦合度高;而对象组合则只要求被组合的对象具有良好定又的接口,耦合度低。
比方说:当我们使用继承时,子类与父类之间的关系非常紧密,任何对父类的修改都可能影响到子类。如果父类的内部实现发生了改变,那么子类的行为也可能会受到影响,这就破坏了封装性。此外,继承还可能导致类的设计过于复杂,因为子类不仅需要实现自己的逻辑,还需要理解和维护父类的逻辑。
相比之下,组合/聚合则可以使得类之间的关系更加松散,提高了代码的灵活性。我们可以将一个类的对象作为另一个类的成员,这样,如果需要修改某个类的行为,我们只需要替换这个成员对象即可,而不需要修改类的内部实现。这样就降低了类之间的耦合度,提高了代码的可维护性。
封装变化点
使用封装来创建对象之间的分解层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次见的松耦合。
比方说:举个例子,假设我们正在设计一个电商系统,系统中有一个订单处理模块。这个模块的一个主要职责是计算订单的总价。然而,订单的价格计算规则可能会经常变化,比如可能会有各种不同的折扣和优惠活动。如果我们将价格计算的逻辑直接写在订单处理模块中,那么每次规则变化时,我们都需要修改订单处理模块的代码。这不仅会增加维护的难度,而且还可能会引入错误。相反,如果我们将价格计算的逻辑封装到一个单独的对象中,那么每次规则变化时,我们只需要替换这个对象即可,而不需要修改订单处理模块的代码。
使用重构得到模式——设计模式的应用不宜先入为主
- 一上来就使用设计模式是对设计模式最大的
误用
,没有一步到位的设计模式。
3.编程具体设计原则
-
单一职责原则(SRP)
一个类应该仅有一个引起它变化的原因
例如:一个处理报告的类应该只负责生成报告,而不应该同时负责打印报告。这两个功能应该由两个不同的类来处理。 -
开放封闭原则 (OCP)
类模块应该是可扩展的,但是不可修改《对扩展开放,对更改封闭》
例如:如果我们有一个计算形状面积的类,当我们需要添加一个新的形状时,我们应该能够添加一个新的形状类,而不需要修改计算面积的类。 -
Liskov 替换原则 (LSP)
子类必须能够替换它们的基类
例如:如果我们有一个动物类和一个鸟类(鸟类是动物类的子类),那么在任何需要动物的地方,我们都应该能够使用鸟类。 -
依赖倒置原则(DIP)
高层模块不应该依赖于低层模块,二者都应该依赖于抽象抽象不应该依赖于实现细节,实现细节应该依赖于抽象。
例如:一个电脑类不应该直接依赖于具体的CPU类或内存类,而应该依赖于抽象的CPU接口和内存接口 -
接口隔离原则(ISP)
不应该强迫客户程序依赖于它们不用的方法
例如:如果我们有一个多功能打印机类,它有打印、扫描和复印的功能,那么对于只需要打印功能的客户程序,我们不应该强迫它依赖于扫描和复印的方法,而应该提供一个只包含打印方法的接口。这样,客户程序就只需要依赖于这个接口,而不需要知道其他不相关的方法。
4.设计模式分类
- 两个角度
从目的来看: - 创建型(Creational) 模式:负责对象创建
- 结构型(Structural) 模式:处理类与对象间的组合。
- 行为型 (Behavioral) 模式: 类与对象交互中的职责分配
从范围来看: - 类模式处理类与子类的静态关系。
- 对象模式处理对象间的动态关系。
三、Singleton 单例模式
1.核心思想
在单例模式中,类的唯一实例应该在第一次需要时创建,并且在后续的请求中重复使用,而不是每次请求时都创建新的实例
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
2.代码理解
多线程情况下:
静态创建实例 or 静态构造器:
静态构造函数
: 就像一个自动开关,当你第一次使用这个类(比如创建一个新的实例或者调用一个静态成员)的时候,它就会自动运行一次,然后就不再运行了。
静态实例的创建:
就像一个预先准备好的礼物,当你的程序开始运行并且第一次遇到这个类的时候,这个礼物就已经被准备好了。
所以说他们只会调用一次,以确保单例模式只有一个引用实例。只不过前者是用到的时候才会去调用,后者是从一开始就创建好,实际上也就是饿汉式
与懒汉式
3.个别要点
- Singleton模式中的实例构造器可以设置为protected以允许子类派生。
在父子类里面就算我多态创建一个实例,这个实力也同样是调用父类的构造器,所以可以说本质上还是单例模式,没什么区别 - Singleton模式一般不要支持ICloneable接口,因为这可能会导致多个对象实例,与Singleton模式的初衷违背。
- Singleton模式一般不要支持序列化,因为这也有可能导致多个对象实例,同样与Singleton模式的初衷违背。
在反序列化过程中,readObject()方法会创建一个新的对象。这个新的对象是通过调用无参构造函数创建的,然后将序列化时保存的字段值赋给新对象的对应字段。因此,反序列化后得到的对象与原来序列化的对象是不同的实例,即使它们的字段值是相同的 - Singletom模式只考虑到了对象创建的管理,没有考虑对象销毁的管理。就支持垃圾回收的平台和对象的开销来讲,我们一般没有必要对其销毁进行特殊的管理
- 不能应对多线程环境:在多线程环境下,使用Singleton模式仍然有可能得到Singleton类的多个实例对象
- 将一个实例扩展到n个实例,例如对象池的实现
- 将new 构造器的调用转移到其他类中,例如多个类协同工作环境中,某个局部环境只需要拥有某个类的一个实例。
- 理解和扩展Singleton模式的核心是“如何控制用户使用new对一个类的实例构造器的任意调用”
4.解决了什么问题?
未必节省资源
C#中new一个对象通常是 “ 即时创建 > 使用 > 释放 ” 单例模式创建后就会一直保存与内存中,所以说是否节省资源仁者见仁智者见智。根据具体情况来决定。
同一个实例,也不能解决多线程并发问题
测试理解中有详情
单例真正解决的问题是:程序只需要这个对象实例化一次,只允许实例化
例如:数据库连接池: 数据库连接–非托管资源–申请/释放消耗性能池化资源–内置10个链接—使用来拿,用完放回来–避免重复申请和销毁—控制链接数量
5.理解测试
最终Num的值应该为多少?
最终答案: 0 < Num and Num <= 10000
主要原因是因为,Num++这个操作本质有三个步骤,读取、修改、写回。这三个操作不具有原子性,从而导致线程冲突。
四、SimpleFactory 简单工厂模式 (非GOF)
1.核心思想
简单工厂并不算是GOF23中设计模式中的一种,因为它违背了开闭原则,而且拓展性较差。一旦遇到需要升级,就必须再独立写一个类实现接口。
而且子类如果需要具体数据,即便你写了构造器,每一次new都比较复杂。
所以可以配合C#中的ConfigurationManager.appsetting方法,通过配置文件来统一管理类。
但是即便如此你依然还是需要在代码中new一个对象,无论封装在哪里都只是矛盾转移而不是彻底解决问题,出现了新的类型你依然需要写一个new,而不是直接创建出实例,比如构造器和参数来决定你创建哪一个类型的实例
所以我们还可以通过反射
来彻底摆脱扩展时需要多代码进行修改。通过反射我们只需要配置好文件,然后反射获取就可以得到类实例
五、FactoryMethod=工厂方法模式
1.核心思想
在不违背单一职责条件下,工厂方法模式乍看之下将创建类实例变得复杂,只不过是多了一层工厂类来封装创建类的细节。但实际上用途在于,上层并不想知道下层创建类实例过程中需要什么参数和具体细节。比如创建类需要三个形参,你还需要分别获取三个形参。这些东西调用者不想看到,所以全部封装到工厂类中。除此之外当你需要对工厂类进行拓展的时候你可以继续创建一个类来继承这个工厂,重写方法以此来继续扩展功能。
2.代码理解
六、AbstractFactory 抽象工厂模式
1.核心思想
实际上工厂的本质就是把创建对象时的细节置换成接口或者抽象
例如: Dog dog = new Dog(); Dog类中只能表示这一个单一的对象 使用工厂创建IAnimals接口,在配备一个创造示例的方法
一方面能够隐藏创建实例的细节,一方面也能够方便拓展其他类型的"Animals"
相较于工厂方法模式只针对Animals这种单一系列做抽象,抽象工厂模式可以做到除了 Animals 还可以同时抽象 Plants
也就是可以将一系列所需要的类制作成抽象工厂类
换言之,抽象工厂就是一系列的工厂方法
2.代码理解
可以发现,实际上抽象工厂多声明了几个方法。来创建一系列相关的类
比如抽象工厂接口先声明创建武将和创建主公。
根据不同的国家创建一个工厂并且继承抽象类,实现抽象类中的两个方法
分别为创建武将 以及创建主公。
他们同属一个系列,但是一个抽象类可以帮助你完成多个分支的创建。
3.模式缺陷
工厂职责的拓展相当麻烦、抽象类声明方法一旦改动,所有子类工厂都要改动
七、Builder 建造者模式
1.核心思想
指挥者类只负责控制"建造类"的流程,而不会参与创造类的实际操作
实际创造类的过程是交给 抽象类子类来实现具体创建的
简言之,将一个创建类的复杂流程所需方法定义为一个抽象类
具体的子类中重写方法
指挥类的构造方法中传入具体子类,并且创建方法来调用子类的方法执行顺序
将一系列细节封装起来,上层只需要通过指挥类来得到所需实例
2.代码理解
3.个别要点
建造者模式与抽象工厂有些相似
建造者模式主要是针对一个复杂的类
的创建
抽象工厂则是有一系列独立的类
的创建
七、ProtoType 原型模式
1.核心思想
先看看一块单例代码
单例模式中虽然能够多次使用同一对象,但是不同的赋值将会覆盖前一次的赋值
为了解决这个问题,出现了原型模式
既能保持高性能,同时又能不出现"覆盖"现象
原型模式本质是在将原来的实例在内存中有复制了一块一摸一样的
根本不通过构造函数
2.代码理解
3.个别要点
答案为: 3 3 4
C#内存分为:进程堆(进程唯一)
与线程栈(每个线程一个)
//
引用类型在堆里,值类型在栈里—变量都在栈里
通常来说,引用类型都存放在堆当中,如果引用类型中包含了值类型,那么值类型也会存储在堆中。
深浅Copy
拷贝之后如果还是原来的引用,那就是浅拷贝
如果不仅拷贝了引用,还拷贝了引用类型的值就是深拷贝
当你使用原型模式时,只cpoy了原来的引用。因此ClassId
第二次修改影响第一次输出结果
但是Name却不会产生"覆盖"情况,原因是当 string类型 使用 = 例如:
student1.Name = “CodeMan” 的时候。实际上等于 student1.Name = new string(“CodeName”);
因此,每次给string类型赋值的时候都是创建了一个新的引用,所以说不会产生覆盖
总之,string类型是无法修改的,每一次赋值实际上都在堆中开辟了一块新的空间
这一点和Java是一样的
深拷贝的三种方式:1. 直接new 、2. 子类提供原型方式、 3..序列化后再反序列化
八、Adapter 适配器模式 (结构型设计模式)
1.核心思想
类适配器
与 对象适配器
假如你有一台手机,只有typec接口
但是你的耳机却是3mm的插口,此时你又想用耳机听歌
于是你买了一条3mm转typec的转接口,这就是适配器模式
2.代码理解
假设有一个接口,大部分类都可以直接继承并且实现
但是存在一个类,这个类的方法与接口中的方法冲突
为了能够实现接口与该类适配
第一种方案:类适配器
创建一个新类,同时继承接口与类。
对象适配器
两者之间的区别就在于,继承是强制的,子类必须包含所有父类的方法
但是如果你是用的是对象适配器,那么上层只能调用到唯一接口的方法
⭕23设计模式总结
简单模式: 设计一个创造类,输入形参就可以返回对应类实例
(违反开闭原则
,每次调整都需要修改工厂类)
GOF23种设计模式
创建型模式(5种)
单例模式: 类只能存在一个实例,构造为 一个私有构造器和一个静态只读属性Instance。
在程序初始化的时候就会创建实例,之后每一次使用的实例都为同一个
工厂方法模式: 定义一个接口,针对一个产品,由该接口子类决定实例化哪一个类
抽象工厂模式: 定义一个接口,针对一系列产品,由该接口子类决定实例化哪一个类
简单说,工厂方法和抽象工厂都是设计一个接口,前者不需要在意类之间的关联性,如果需要拓展功能,只需要新设计一个类来实现接口就好;
后者需要在意类之间的关联性,一个接口有多个类的创建方法,这些类都相互关联,比如Animals接口,就带着猫、狗、猪等创建实例方法。
而工厂方法一般只针对一个产品,所以说它的出现单纯是为了解决简单工厂存在"违背开闭原则"的情况。
而后者严格来说不算违背,因为如果你的animals需要拓展,你也不需要修改原先的代码,只需要添加新类的创建方法到接口,然后由子类实现就行了。
建造者模式: 能够把一个复杂的类的创建过程解耦成一个抽象类,每次我们需要创建复杂类实例,我们就可以继承抽象类,然后设置对应的参数,这样就可以快速的创建多个相关类的实例
原型模式: 当对象需要在一个高代价的数据库操作后被创建,所以我们直接采取克隆方法 详情参考第5点代码
结构型模式(6种)
桥接模式: 假如你需要编写在 win、linux、mac 三个系统中 能够绘制 长方形、圆形、三角形图案的程序。
按照常理你需要分别写出六个类,两两配对一共是九种方法,但是桥接模式只需要定义一个抽象类和接口,输入win与circle就可以完成。
①:先创建接口及接口方法;
②:实现接口的实现类及重写方法;
③抽象类(含接口实例、构造函数(参数为接口)、抽象方法(调用接口方法););
④:继承抽象类拓展方法。
这么一来就存在了两个能够使用的方法(②,④)。
main函数中我们先创建②的实例(类型为接口),之后将该实例作为参数传入④,当我们调用④的方法的时候,
方法做完自己独立的事情还会去调用②的方法,这是由于我们的抽象类中的抽象方法调用过接口方法。
至此我们实现了只更换参数而调用了两次方法(前提是你有编写好的接口实例和继承抽象类实例)
组合模式: 组合模式的确就是通过类封装列表,并通过公共方法来访问和操作这些列表。这种模式的关键在于,它允许我们将对象组合成树形结构,以表示"整体-部分"的层次关系。
装饰模式:
①抽象类A
②抽象类A的实现类
③抽象类B(继承抽象类A,构造器参数为(A实例),且重写抽象类A的方法(其中也调用了②的方法))
④抽象类B的实现类
假如你本身就有①和②,但是功能不够使用,此时你决定使用装饰模式,于是你创建了③,在③当中你重写方法的时候,既可以调用②的方法,也可以在基础上继续做一些其他修改以增强功能
因此你在创建④实例之后,使用的方法就是增强后的功能了 (整个过程都不违背开闭原则)
外观模式: 创建多个子系统类,每个类都包含一些方法,最后设计一个类,创建多个private子系统类实例,并且创建一个方法,根据需求在方法中去调用这些子系统类实例的方法,这样我们就将多个子系统类的方法封装到一个方法中,外部就可以通过调用这个类的方法来间接调用多个子系统类的方法。
享元模式: 创建一个类(其中包含一个私有属性记录内在状态、一个构造器(参数为字符串等能做key的)、一个方法),在创建一个工厂类(包含一个私有字典(key,类)、根据key获取类方法,获取总共多少类),我感觉本质上就是字典存类,用户需要时就根据用户输入的key从字典中找,如果存在就返回引用,不存在就创建一个类在这个key的位置。跟java中Integer这个变量类型的设计差不多,Integer只要是(-127~128)之间都是同一个引用。底层有一个数组,不会创建新的,只会给你返回固定的,差不多就这个意思
解决问题:
避免频繁调用占用内存,影响效率
代理模式: 当想要封装接口实现类A避免被访问时,采用代理模式就是再创建一个类B,并且在类中创建接口实现类A的私有实例。如此一来只能通过B中的公开方法间接调用接口实现类A的方法。
解决问题:
控制访问、降低系统的复杂性、提高效率
行为型模式(13种)
命令模式:
①:定义命令接口
②:命令接口的实现类(包含私有接受类实例、构造函数(参数为接受类实例)、接口方法实现)
③:定义接收者类(包含方法)
④:创建发送者类(含私有接口实例,设置接口方法(参数为接口类型)、调用接口中方法)
代码详情13.1
本质上就是设计一个请求和接受的框架,程序员可以设计请求内容和接受之后的内容。
解释器模式: 针对一些特定的语法格式要求来做判断等等处理,实际开发中用处并不大
代码详情13.2
迭代器模式: 使用迭代器模式需要创建索引器,索引器会导致类实例可以使用类似数组形式访问,你的索引器声明是什么返回类型,访问方式就是 类实例[返回类型]
实际上把多个不同引用,但却是相同类型
的类。通过迭代器来遍历访问其中的方法
解决问题:
统一访问方式、封装内部实现、支持多种遍历、简化复杂的集合类
代码详情13.3
中介者模式: 简单来说就是为两个类或者多个类创建一个中间类,中间类包含多个类的实例,可以写出多种组合方法,比如某个方法(参数为类a与类b实例),根据需求就可以分别调用这两个类的方法来达成目标;在拥有多个类需要组合调用达成各种方法的时候可以使用
备忘录模式: 管理类创建一个类型为备忘录的列表,里面包含的都是一些控制列表的,你可以理解为游戏存档数据列表。发起者类就类似玩家,类中包含两个备忘录类型的方法(get状态,set状态)和一个变量“状态”,可以把状态当成参数创建一个备忘录类实例,set表示存档,get表示读档,这两个方法都会返回一个备忘录实例,你可以把实例通过管理者类存入列表;主要是针对一个对象的某个状态
观察者模式: 工厂类包含私有列表,也用来存储类,这些被存储的类都是观察者类。比如一个小区有10个人,10个人之间如果有一个人得了流感,其他所有人都会收到通知。这就是观察者类,工厂类中的列表包含了10个观察者类,当其中一个类发生改变,就会马上通知列表中其他类
状态模式: 本质上是为了解决if-else代码不方便拓展的问题,如果你有很多if-else,需要添加复杂的判断内容时很容易出错,因此状态模式采用:每一种可能就创建一个继承抽象类的类,当你需要添加新的判断内容就直接创建一个新的类,这样就不容易出错,也不违反开闭原则。不过乍看之下可以说非常像抽象工厂模式,不过抽象工厂是针对一系列有关联的类的设计模式,而状态模式是为避免多种判断情况下添加功能等影响而将if{…}中的代码块设计为类的一种方式。
策略模式: 简单说,一个问题可能存在多种解决方法,比如列表排序,你可以直接使用list.sort,也可以使用双重for循环来比较大小排序。所以我们先设计一个抽象类(含一个抽象排序方法),创建两个类继抽象类,在类中实现两种不同的方法,在main函数中你就可以根据不同的类来选择不同"策略",这样就是我们说的策略模式
模拟方法模式: 在抽象类当中定义好功能的流程,比如煮饭需要洗锅、洗米、蒸饭。淘米这个过程你可以选择开水或者凉水或者加一点油。但是洗锅和蒸饭是固定的,所以我们在抽象类当中写好洗锅和蒸饭的两个方法,但是把洗米换成抽象方法,这样等到具体的子类来实现洗米这个方法,从而就可以形成三种子类。包括你后需要需要拓展也非常的方便,这就是模拟方法模式的思路,游戏开发中可以使用在不同的战斗数值计算的功能开发中。