【设计模式】学习笔记(一)——基本概念和设计原则
建议:学前保证学过UML建模中的类图,没学过也可以在我的博客文章中适当了解
目录
设计模式简介
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。
使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。
设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。
设计模式七大原则
设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模式的基础(即:设计模式为什么这样设计的依据)
设计模式常的七大原则有:
① 开闭原则
开闭原则是编程中最基础、最重要的原则。一个软件实体如类,模块和函数应该对扩展开发(对提供方),对修改关闭(对使用方)。用抽象构成框架,用实现扩展细节。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有代码来实现变化。
编程中遵循其他原则,以及使用设计模式的目的就是遵循开闭原则。
示例分析
需求: 有圆形, 有椭圆形, 根据要求画出相应的形状//这是一个用来绘图的类
public class GraphicEditor {
//接收Shape对象,然后根据type,来绘制不同的图形
public void draw(Shape s) {
if (s.m_type == 1) {
drawRectangle();
} else if(s.m_type == 2) {
drawCircle();
}
}
//绘制矩形
public void drawRectangle() {
System.out.println("绘制矩形");
}
//绘制圆形
public void drawCircle() {
System.out.println("绘制圆形");
}
//Shape类,基类
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type=1;
}
}
class Circle extends Shape {
Circle() {
super.m_type=2;
}
}
}
然后我们来使用一下看看有什么问题
public static void main(String[] args){
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
}
首先这样写的好处是比较好理解,简单易操作。缺点就是违反了设计模式的ocp原则,即对扩展开发,对修改关闭。即当我们给类增加新功能的时候,尽量不修改代码,或者尽可能少修改代码。比如我们这时要新增一个图形种类,需哟要修改的地方太多了。
比如现在我要新增一个画三角形功能
//新增画三角形
class Triangle extends Shape{
Triangle(){
super.m_type = 3;
}
}
//使用方
public class GraphicEditor {
//接收Shape对象,然后根据type,来绘制不同的图形
public void draw(Shape shape) {
if (s.m_type == 1) {
drawRectangle();
} else if(s.m_type == 2) {
drawCircle();
} else if(s.m_type == 3){
drawTriangle(s);
}
}
//绘制矩形
public void drawRectangle() {
System.out.println("绘制矩形");
}
//绘制圆形
public void drawCircle() {
System.out.println("绘制圆形");
}
//绘制三角形
public void drawTriangle(s){
System.out.println("绘制三角形");
}
如你所见,我们单单只是添加一个新的绘制三角形的功能,就要修改三处地方。
现在我们通过开闭原则对代码进行一些修改。把创建Shape类做成抽象类,并提供一个抽象的draw方法,让子类去实现即可,这样我们有新的图形种类时,只需要让新的图形类继承Shape,并实现draw方法即可,使用方的代码就不需要修改,满足了开闭原则
//这是一个用来绘图的类
public class GraphicEditor {
//接收Shape对象,然后根据type,来绘制不同的图形
public void draw(Shape s) {
s.draw();
}
abstract class Shape {
int m_type;
public abstract void draw();//抽象方法
}
class Rectangle extends Shape {
Rectangle() {
super.m_type=1;
}
@Oerride
public void draw(){
System.out.println("绘制圆形");
}
}
class Circle extends Shape {
Circle() {
super.m_type=2;
}
@Oerride
public void draw(){
System.out.println("绘制矩形");
}
}
// 新绘制三角形
class Triangle extends Shape{
Triangle() {
super.m_type = 3;
}
@Override
public void draw() {
System.out.println("绘制三角形");
}
}
}
像这样假如我们想再扩展一个画菱形画xxx形,只需要继承抽象类并重写抽象方法即可
② 接口隔离原则
使用多个隔离的接口,比使用单个接口要好。客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立再最小的接口上。他还有另一个意思:降低类之间的耦合度。
代码示例
interface Interface1{
void operation1();
void operation2();
void operation3();
void operation4();
void operation5();
}
class B implements Interface1{
public void operation1(){
System.out.println("B 实现了 operation1");
}
public void operation2(){
System.out.println("B 实现了 operation2");
}
public void operation3(){
System.out.println("B 实现了 operation3");
}
public void operation4(){
System.out.println("B 实现了 operation4");
}
public void operation5(){
System.out.println("B 实现了 operation5");
}
}
class D implements Interface1{
public void operation1(){
System.out.println("D 实现了 operation1");
}
public void operation2(){
System.out.println("D 实现了 operation2");
}
public void operation3(){
System.out.println("D 实现了 operation3");
}
public void operation4(){
System.out.println("D 实现了 operation4");
}
public void operation5(){
System.out.println("D 实现了 operation5");
}
}
class A{ // A类通过接口Interface1 依赖(使用)B类,但只会使用1,2,3方法
public void depend1(Interface1 i){
i.operation1();
}
public void depend2(Interface1 i){
i.operation2();
}
public void depend3(Interface1 i){
i.operation3();
}
}
class C{ // C类通过接口Interface1 依赖(使用)D类,但只会使用1,4,5方法
public void depend1(Interface1 i){
i.operation1();
}
public void depend4(Interface1 i){
i.operation4();
}
public void depend5(Interface1 i){
i.operation5();
}
}
可以看到其实A类依赖B类,C类依赖D类都只使用了部分的方法,但是B类和D类都实现了接口Interface1的五个方法,有部分方法是没用到的。按照接口隔离原则应当将接口Interface1拆成几个接口,类A和类C分别与他们需要的接口建立依赖关系。
//接口1
interface Interface1{
void operation1();
}
//接口2
interface Interface2{
void operation2();
void operation3();
}
//接口3
interface Interface3{
void operation4();
void operation5();
}
class B implements Interface1,Interface2{
public void operation1(){
System.out.println("B 实现了 operation1");
}
public void operation2(){
System.out.println("B 实现了 operation4");
}
public void operation3(){
System.out.println("B 实现了 operation5");
}
}
class D implements Interface1,Interface3{
public void operation1(){
System.out.println("D 实现了 operation1");
}
public void operation4(){
System.out.println("D 实现了 operation2");
}
public void operation5(){
System.out.println("D 实现了 operation3");
}
}
class A{ // A类通过接口Interface1,Interface2 依赖(使用)B类,但只会使用1,2,3方法
public void depend1(Interface1 i){
i.operation1();
}
public void depend2(Interface2 i){
i.operation2();
}
public void depend3(Interface2 i){
i.operation3();
}
}
class C{ // C类通过接口Interface1,Interface3 依赖(使用)D类,但只会使用1,4,5方法
public void depend1(Interface1 i){
i.operation1();
}
public void depend4(Interface3 i){
i.operation4();
}
public void depend5(Interface3 i){
i.operation5();
}
}
使用一下
public class Segregation1 {
public static void main(String[] args){
//使用一下
A a=new A();
a.depend1(new B());// A类通过接口去依赖B
a.depend2(new B());
a.depend3(new B());
C c=new C();
c.depend1(new D());// C类通过接口去依赖(使用)D类
c.depend4(new D());
c.depend5(new D());
}
}
③ 依赖倒转(倒置)原则
要针对接口编程,不要针对实现编程。高层模块不应该依赖低层模块,两者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖于抽象。
代码示例
概念描述:
- 低层模块:不可分割的原子逻辑,可能会根据业务逻辑经常变化。
- 高层模块:低层模块的再组合,对低层模块的抽象。
- 抽象: 接口或抽象类(是底层模块的抽象,特点:不能直接被实例化)
- 与接口或抽象类对应的实现类:低层模块的具体实现(特点:可以直拉被实例化)
依赖的关系种类
-
零耦合关系:如果两个类之间没有耦合关系,称之为零耦合
-
直接耦合关系: 具体耦合发生在两个具体类(可实例化的)之间,经由一个类对另一个类的直接引用造成。
-
抽象耦合关系: 抽象耦合关系发生在一个具体类和一个抽象类(或者java接口)之间,使两个必须发生关系的类之间存在最大的灵活性。
依赖倒转原则就是要针对接口编程,不要针对实现编程。这就是说,应当使用接口或者抽象类进行变量的类型声明,参数的类型声明,方法的返回类型说明,以及数据类型的转换等。
优点
- 可以通过抽象使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合(也是本质)
- 可以规避一些非技术因素引起的问题,减低并行开发风险
项目大时,需求变化的概率也越大,通过采用依赖倒置原则设计的接口或抽象类对实现类进行约束,可以减少需求变化引起的工作量剧增情况。同时,发生人员变动,只要文档完善,也可让维护人员轻松地扩展和维护
- 提高系统的稳定性、提高代码的可读性和可维护性
如,两个类之间有依赖关系,只要制定出两者之间的接口(或抽象类)就可以独立开发了,规范已经定好了,而且项目之间的单元测试也可以独立地运行,而TDD开发模式更是DIP的最高级应用(特别适合项目人员整体水平较低时使用
④ 里氏替换原则
里氏替换原则所描述的其实就是如何正确的使用面向对象中的继承关系:
在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。继承实际上让两个类耦合性增强了,在适当情况下,可以通过聚合、组合、依赖来解决问题...
当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。任何基类可以出现的地方,子类一定可以出现。
代码示例
我们发现原来运行正常的相减功能发生了错误。原因就是类B无意中重写了父类的方法造成原有功能出现错误。再实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性比较差。特别是运行多态比较频繁的时候
通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等关系代替。
public class Liskov{
public static void main(String[] args){
A a=new A();
System.out.println("11-3="+a.func1(11,3));
System.out.println("1-8="+a.func1(1,8));
System.out.println("-----------------------");
B b=new B();
//因为B类不再继承A类,因此调用者,不会再认为func1是求减法了
//调用完成的功能就会很明确
System.out.println("11+3="+b.func1(11,3));//这里本意是求出11+3
System.out.println("1+8="+b.func1(1,8));//11+8
System.out.println("11+3+9="+b.func2(11,3));
//使用组合任然可以使用到A类相关方法
System.out.println("11+3=" + b.func3(11,3));
}
}
//首先我们创建一个更加继承的基类
class Base{
// 把更加基础的方法和成员写到Base类中
}
//A类
class A extends Base{
//放回两个数的差
public int func1(int num1,int num2){
return num1-num2;
}
}
//B类继承了A类
//增加了一个新的功能:完成两个数相加,然后和+9
class B extends Base{
//如果B需要使用类A的方法,使用组合关系
private A a = new A();
public int func1(int a,int b){
return a+b;
}
public int func2(int a,int b){
return func1(a,b)+9;
}
//我们仍然想使用A的方法
public int func3(int a,int b) { return this.a.func1(a,b); }
}
⑤ 单一职责原则
就一个类而言,应该仅有一个引起它变化的原因。(应该只有一个职责)。如果一个类有一个以上的职责,这些职责就耦合在了一起。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这会导致脆弱的设计。
单一职责原则的核心就是解耦和增强内聚性。
优点
- 提高类的可维护性和可读写性
- 提高系统的可维护性
- 降低变更的风险
一个类的职责少了,复杂度降低了,代码就少了,可读性也就好了,可维护性自然就高了。
系统是由类组成的,每个类的可维护性高,相对来讲整个系统的可维护性就高。当然,前提是系统的架构没有问题。
一个类的职责越多,变更的可能性就越大,变更带来的风险也就越大
如果在一个类中可能会有多个发生变化的东西,这样的设计会带来风险, 我们尽量保证只有一个可以变化,其他变化的就放在其他类中,这样的好处就是 提高内聚,降低耦合。
代码示例
**单一职责原则应用的范围**
---
单一职责原则适用的范围有接口、方法、类。按大家的说法,接口和方法必须保证单一职责,类就不必保证,只要符合业务就行。
**【方法层面】单一职责原则的应用**
现在有一个场景,需要修改用户的用户名和密码,就针对这个功能我们可以有多种实现:
<details>
<summary>方式一</summary>
/**
- 操作的类型
*/
public enum OperateEnum {
UPDATE_USERNAME,
UPDATE_PASSWORD;
}
public interface UserOperate {
void updateUserInfo(OperateEnum type, UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate{
@Override
public void updateUserInfo(OperateEnum type, UserInfo userInfo) {
if (type == OperateEnum.UPDATE_PASSWORD) {
// 修改密码
} else if(type == OperateEnum.UPDATE_USERNAME) {
// 修改用户名
}
}
}
</details>
<details>
<summary>方式二</summary>
public interface UserOperate {
void updateUserName(UserInfo userInfo);
void updateUserPassword(UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate {
@Override
public void updateUserName(UserInfo userInfo) {
// 修改用户名逻辑
}
@Override
public void updateUserPassword(UserInfo userInfo) {
// 修改密码逻辑
}
}
</details>
来看看这两种实现的区别:
方式一是根据操作类型进行区分,不同类型执行不同的逻辑,把修改用户名和修改密码这两件事耦合在一起了,如果客户端在操作的时候传错了类型,那么就会发生错误。
方式二是我们推荐的实现方式,修改用户名和修改密码逻辑分开,各自执行各自的职责,互不干扰,功能清晰明了。
由此可见,第二种设计是符合单一职责原则的,这是在方法层面实现单一职责原则:
> 每个方法只负责一项工作。
**【接口层面】单一职责原则的应用**
<details>
<summary>方式一</summary>
/**
-
做家务
*/
public interface HouseWork {
// 扫地
void sweepFloor();// 购物
void shopping();
}
public class Xiaocheng implements HouseWork{
@Override
public void sweepFloor() {
// 扫地
}
@Override
public void shopping() {
}
}
public class Xiaoming implements HouseWork{
@Override
public void sweepFloor() {
}
@Override
public void shopping() {
// 买菜
}
}
</details>
说是妈妈在出门前嘱咐小明和小陈做家务(定义一个做家务接口),小明去买菜(实现买菜接口),小陈去扫地(实现扫地接口)。到这里其实你就发现,小明不需要扫地却要重写扫地的方法,小陈不需要买菜但因为实现了做家务的接口,就不得不也重写买菜方法。
这样的设计是不合理的,他违背了单一原则,也不符合开闭原则。
假如现在小明去买完菜回来得做饭,就得修改三个类,这样当逻辑很复杂的时候就容易引起错误
<details>
<summary>方式二</summary>
/**
- 做家务
*/
public interface Hoursework {
}
public interface Shopping extends Hoursework{
// 买菜
void shopping();
}
public interface SweepFloor extends Hoursework{
// 扫地
void sweepFlooring();
}
public class Xiaocheng implements SweepFloor{
@Override
public void sweepFlooring() {
// 小陈扫地
}
}
public class Xiaoming implements Shopping{
@Override
public void shopping() {
// 小明买菜
}
}
</details>
现在我们将扫地和做家务这个接口拆分,小陈扫地,那就实现扫地接口,小明购物就实现买菜接口。
接下来小明买完菜回来做饭,那么就只需要新增一个做法接口,让小明实现就可以了
public interface Cooking extends Hoursework{
void cooking();
}
public class Xiaoming implements Shopping, Cooking{
@Override
public void shopping() {
// 小明购物
}
@Override
public void cooking() {
// 小明做饭
}
}
> 在接口层面,子类不实现多余的接口,这就符合单一原则:一个类只做一件事,并且修改它不会带来其他变化
#### 【类层面】单一职责原则的应用
从类的层面来讲,没有办法完全按照单一职责原来来拆分,换种说法,类的职责可大可小,不想接口那样可以很明确的按照单一职责原则拆分,只要符合逻辑有道理即可。
比如, 我们在网站首页可以注册、登录、微信登录、注册登录等操作。我们通常的做法:
public interface UserOperate {
void login(UserInfo userInfo);
void register(UserInfo userInfo);
void logout(UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate{
@Override
public void login(UserInfo userInfo) {
// 用户登录
}
@Override
public void register(UserInfo userInfo) {
// 用户注册
}
@Override
public void logout(UserInfo userInfo) {
// 用户登出
}
}
那如果按照单一职责原则拆分, 也可以拆分为下面的形式
public interface Register {
void register();
}
public interface Login {
void login();
}
public interface Logout {
void logout();
}
public class RegisterImpl implements Register{
@Override
public void register() {
}
}
public class LoginImpl implements Login{
@Override
public void login() {
// 用户登录
}
}
public class LogoutImpl implements Logout{
@Override
public void logout() {
}
}
像上面这样写可不可以呢? 其实也可以,就是类很多,如果登录、注册、注销操作代码很多,那么可以这么写。
**合理的职责分解**
相同的职责放到一起,不同的职责分解到不同的接口和实现中去,这个是最容易也是最难运用的原则,关键还是要从业务出发,从需求出发,识别出同一种类型的职责。
⑥ 迪米特法则
又称最少知道原则,是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
⑦ 合成复用原则
合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
设计模式分类
创建型模式
用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。包括了以下模式
点击查看
- 工厂模式(Factory Pattern)
- 抽象工厂模式(Abstract Factory Pattern)
- 单例模式(Singleton Pattern)
- 建造者模式(Builder Pattern)
- 原型模式(Prototype Pattern)
结构型模式
用于描述如果将类或对象按某种布局组成更大的结构。包括以下模式:
点击查看
- 适配器模式(Adapter Pattern)
- 桥接模式(Bridge Pattern)
- 过滤器模式(Filter、Criteria Pattern)
- 组合模式(Composite Pattern)
- 装饰器模式(Decorator Pattern)
- 外观模式(Facade Pattern)
- 享元模式(Flyweight Pattern)
- 代理模式(Proxy Pattern)
行为型模式
用于描述类或对象之间怎样互相协作共同完成单个对象无法单独完成的任务,以及怎么分配职责。包括以下模式
点击查看
- 责任链模式(Chain of Responsibility Pattern)
- 命令模式(Command Pattern)
- 解释器模式(Interpreter Pattern)
- 迭代器模式(Iterator Pattern)
- 中介者模式(Mediator Pattern)
- 备忘录模式(Memento Pattern)
- 观察者模式(Observer Pattern)
- 状态模式(State Pattern)
- 空对象模式(Null Object Pattern)
- 策略模式(Strategy Pattern)
- 模板模式(Template Pattern)
- 访问者模式(Visitor Pattern)
J2EE 模式
这些设计模式特别关注表示层。这些模式是由 Sun Java Center 鉴定的。
点击查看
- MVC 模式(MVC Pattern)
- 业务代表模式(Business Delegate Pattern)
- 组合实体模式(Composite Entity Pattern)
- 数据访问对象模式(Data Access Object Pattern)
- 前端控制器模式(Front Controller Pattern)
- 拦截过滤器模式(Intercepting Filter Pattern)
- 服务定位器模式(Service Locator Pattern)
- 传输对象模式(Transfer Object Pattern)
设计模式书籍
-
《大话设计模式》
以情景对话的形式,用多个小故事或编程示例来组织讲解设计模式 -
《图解设计模式》
以图文的方式讲解设计模式,建议熟悉Java语言的同学观看,因为他的实现就是用到Java语言
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下