设计原则与设计模式
目录
引言
六大设计原则
单一职责原则(SRP - Single Responsibility Principle)
里氏替换原则(LSP - Liskov Substitution Principle)
依赖倒置原则(DIP - Dependency Inversion Principle)
接口隔离原则(ISP - Interface Segregation Principle)
迪米特法则(LoD - Law of Demeter)
开闭原则(OCP - Open - Closed Principle)
23种设计模式
创建型模式:对象创建的艺术
结构型模式:优化软件结构的策略
行为型模式:优化软件行为的技巧
总结
引言
在软件开发的广袤领域中,构建高质量、可维护且灵活的系统是每个开发者的追求。关于设计模式大概分为3种不同类型下共23种设计模式,在说设计模式之前也不得不先看一下六大设计原则而六大设计原则与 23 种设计模式,就如同灯塔,为我们在复杂的代码海洋中指引方向。它们是前辈们在无数项目实践中总结出的宝贵经验,是软件设计的基石。无论是初入编程世界的新手,还是经验丰富的技术专家,深入理解并应用这些原则和模式,都能极大地提升软件的架构水平,让代码更加优雅、健壮。
六大设计原则
单一职责原则(SRP - Single Responsibility Principle)
想象一下,你手中的多功能瑞士军刀,虽功能多样,但每个刀片都有其明确的单一用途。软件设计亦是如此,单一职责原则强调一个类应该仅有一个引起它变化的原因。例如,在一个电商系统中,订单处理类如果既负责订单创建、又处理订单的支付,还管理订单的物流跟踪,那么当支付规则发生变化时可能会影响到订单创建和物流跟踪相关的代码,增加了代码的耦合度和维护难度。而将这些职责分离,每个类专注于一项任务,不仅让代码结构更加清晰,也更易于维护和扩展。
- 单一职责原则(一个小老板的发家史)
*里氏替换原则(LSP - Liskov Substitution Principle)
这一原则如同父子之间的传承与规范。就像父亲期望儿子在遵循家族传统(接口或基类定义)的基础上,能有自己的发展,但不能违背家族的核心原则。在代码中,它要求子类必须能够替换其基类,并且程序的行为不会发生改变。比如,鸟类是一个基类,有飞行的方法。企鹅作为鸟类的子类,如果企鹅不能飞行(即不能按照基类飞行方法的预期行为执行),那么在使用鸟类的地方替换为企鹅就会导致程序出错。遵循里氏替换原则,能确保继承体系的稳定性和可靠性。
- ※里氏替换原则(我买了宝马为啥不让我停这里)
定义:所有使用父类的地方可以使用子类的对象,子类可以扩展父类的功能,但是不能替换父类的的功能。如果需要替换父类功能,建议——多用组合,少用继承。
含义:1> 里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。
2> 如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。
依赖倒置原则(DIP - Dependency Inversion Principle)
传统的依赖关系就像我们依赖具体的交通工具去上班,如果是依赖公交车,一旦公交车线路调整,我们的出行就会受到影响。依赖倒置原则倡导我们依赖抽象而非具体实现。在软件中,高层模块不应该依赖低层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。比如在一个游戏开发中,游戏角色的移动模块不应该直接依赖具体的地面类型(如草地、沙地),而是依赖一个抽象的地形接口,这样当新增一种地形时,只需要实现这个接口,而不需要修改角色移动模块的代码,增强了代码的可维护性和扩展性。
- 依赖倒置原则(我国出租车行业发展伪史)
接口隔离原则(ISP - Interface Segregation Principle)
这就好比去餐厅点菜,你不会希望拿到一份包含所有菜品(无论你是否需要)的大菜单,而是希望有细分的菜单,如主食菜单、菜品菜单、饮品菜单等,这样你可以按需选择。在软件设计里,接口隔离原则主张客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。例如,一个图形绘制系统,如果有一个通用的图形接口,包含了绘制圆形、方形、三角形以及打印图形信息等方法,但某个具体的图形类(如圆形)只需要绘制圆形的方法,那么这个通用接口就不符合接口隔离原则,应该将接口细分,让圆形类只依赖绘制圆形相关的接口,减少不必要的依赖。
- 接口隔离原则——《做个Rapper咋这么难?》
*迪米特法则(LoD - Law of Demeter)
想象你在一个大型公司工作,你只需要和自己部门的同事以及直接上级打交道,对于其他部门的具体人员和工作细节并不需要了解太多,这就是迪米特法则在现实中的体现。在软件设计中,它也被称为最少知识原则,一个对象应该对其他对象有最少的了解。类与类之间的关系越简单越好,尽量降低对象之间的耦合度。例如,在一个学校管理系统中,学生类只需要和班主任类进行交互来获取课程信息等,而不需要直接和教务处、后勤处等其他众多部门类进行交互,通过减少不必要的交互,让系统更加松散耦合,易于维护。
- ※迪米特法则/最少知识原则——《只是买台咖啡机,竟然要学习咖啡器的运行原理?》
开闭原则(OCP - Open - Closed Principle)
开闭原则就像是一个可扩展的积木城堡,城堡已经搭建好(现有代码),当我们想要增加新的功能时,不需要去破坏现有的城堡结构(修改现有代码),而是通过添加新的积木(扩展代码)来实现。它强调软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。例如,在一个报表生成系统中,已经实现了生成 Excel 报表的功能。当需要新增生成 PDF 报表功能时,我们不应该去修改生成 Excel 报表的代码,而是通过创建一个新的 PDF 报表生成类来实现扩展,遵循开闭原则能让我们的软件在面对不断变化的需求时更加稳健。
- 开闭原则——《我发誓!再也不买一体机了》
23种设计模式
本篇设计模式先有概论熟悉一下,下篇将对每一种的实现进行详解揭秘23种设计模式的艺术与技巧-CSDN博客
创建型模式:对象创建的艺术
- 单例模式(Singleton Pattern)
在软件系统中,有些资源是唯一的,比如数据库连接池、线程池等,只需要一个实例来管理,这时候单例模式就派上用场了。它确保一个类只有一个实例,并提供一个全局访问点。实现单例模式有多种方式,如饿汉式、懒汉式、双重检查锁等。饿汉式在类加载时就创建实例,优点是简单直接,线程安全;懒汉式则是在第一次使用时才创建实例,实现了延迟加载,但需要注意线程安全问题;双重检查锁方式既实现了延迟加载又保证了线程安全,是一种较为常用的实现方式。 - 工厂模式(Factory Pattern)
工厂模式就像一个产品工厂,将对象的创建和使用分离。简单工厂模式是工厂模式的基础,它定义了一个工厂类,负责创建产品对象。例如,有一个汽车工厂类,根据传入的参数来创建不同品牌的汽车对象。工厂方法模式在简单工厂模式的基础上,将工厂方法抽象出来,由具体的子类实现,这样每个子类可以创建不同类型的产品。抽象工厂模式则更为复杂,它提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类,适用于创建对象家族的场景。 - 抽象工厂模式(Abstract Factory Pattern)
继续以汽车生产为例,如果不仅要生产不同品牌的汽车,还要为这些汽车搭配不同品牌的轮胎、座椅等配件,这就需要抽象工厂模式。抽象工厂模式允许客户端通过一个抽象的工厂接口来创建一系列相关的产品对象,而具体的创建工作由具体的工厂子类完成。这样,当需要新增一种品牌的汽车及其配件时,只需要创建一个新的具体工厂子类,而不需要修改客户端代码,提高了系统的可维护性和扩展性。 - 建造者模式(Builder Pattern)
建造者模式关注的是对象的创建过程。比如建造一座房子,需要先打地基、然后建造框架、再进行内部装修等一系列步骤。建造者模式将复杂对象的构建过程和表示分离,使得同样的构建过程可以创建不同的表示。在代码实现中,有一个指挥者类来控制建造过程,多个具体建造者类负责具体的建造步骤,最后由指挥者调用具体建造者来完成对象的构建。 - 原型模式(Prototype Pattern)
原型模式就像使用复印机复印文件,通过复制一个已有的对象来创建新的对象。当创建新对象的过程比较复杂或者成本较高时,原型模式就非常有用。例如,在游戏开发中,创建一个复杂的角色对象可能涉及到很多属性的初始化。如果使用原型模式,只需要先创建一个原型角色对象,然后通过克隆这个原型来快速创建多个相似的角色对象,提高了对象创建的效率。
结构型模式:优化软件结构的策略
- 代理模式(Proxy Pattern)
代理模式就像一个经纪人,代表真实对象进行操作。比如,在网络访问中,我们可能会通过代理服务器来访问外部网站。在软件中,当一个对象由于某些原因(如访问权限限制、创建开销大等)不能直接访问另一个对象时,可以通过代理对象来间接访问。代理对象可以在访问真实对象前后添加一些额外的逻辑,如权限验证、缓存处理等。 - 适配器模式(Adapter Pattern)
想象你有一个欧式插头的电器,但家里只有美式插座,这时候就需要一个转换插头(适配器)来让电器正常使用。在软件中,适配器模式用于将一个类的接口转换成客户希望的另一个接口。当我们需要使用一个现有的类,但它的接口与我们的需求不兼容时,就可以使用适配器模式。例如,有一个旧的支付接口,新的业务系统需要与之集成,但新系统使用的是不同的接口规范,这时候就可以创建一个适配器类,将旧支付接口适配成新系统可以使用的接口。 - 桥接模式(Bridge Pattern)
桥接模式将抽象部分与实现部分分离,使它们可以独立变化。以手机为例,手机有不同的品牌(抽象部分),每个品牌又可以有不同的操作系统(实现部分)。如果不使用桥接模式,可能每个品牌的手机都要和每个操作系统进行组合,导致类的数量急剧增加。而使用桥接模式,将品牌和操作系统分离,通过一个桥接接口进行关联,这样当新增一个品牌或者一个操作系统时,只需要添加相应的类,而不需要修改其他类的代码,提高了系统的可维护性和扩展性。 - 装饰器模式(Decorator Pattern)
装饰器模式就像给蛋糕添加不同的装饰,在不改变对象本身的基础上,动态地给对象添加新的功能。比如,一个简单的咖啡对象,我们可以通过装饰器为它添加牛奶、糖、奶油等不同的配料,从而得到不同口味的咖啡。在软件中,当需要在运行时给对象添加功能,而又不想通过继承的方式来扩展类时,装饰器模式是一个很好的选择。它通过创建一个装饰器类,将被装饰的对象作为成员变量,在装饰器类中可以调用被装饰对象的方法,并添加新的功能。 - 外观模式(Facade Pattern)
外观模式提供了一个统一的接口,用来访问子系统中的一群接口。就像电脑的开机键,按下它就可以启动电脑的各个硬件组件,而用户不需要了解每个硬件组件是如何启动的。在软件中,当一个系统由多个复杂的子系统组成,客户端需要与这些子系统进行交互时,使用外观模式可以提供一个简单的接口,隐藏子系统的复杂性,降低客户端与子系统之间的耦合度。 - 享元模式(Flyweight Pattern)
享元模式旨在复用对象,减少对象的创建数量,以提高系统的性能。比如在一个围棋游戏中,棋盘上有大量的棋子,每个棋子的颜色、形状等属性是相同的,只是位置不同。我们可以将这些相同属性的棋子共享,只创建少量的棋子对象,通过改变它们的位置来表示不同的棋子。在软件中,当有大量相似对象存在,且这些对象的创建和销毁开销较大时,享元模式可以有效地减少内存占用,提高系统的运行效率。 - 组合模式(Composite Pattern)
组合模式用于将对象组合成树形结构,以表示 “部分 - 整体” 的层次关系。例如,在一个文件系统中,文件夹可以包含文件和子文件夹,文件和文件夹都可以看作是节点,通过组合模式可以方便地对整个文件系统进行遍历、操作等。组合模式使得客户端可以统一对待单个对象和组合对象,简化了代码的实现。
行为型模式:优化软件行为的技巧
- 策略模式(Strategy Pattern)
策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。比如,在一个电商系统中,计算商品运费可以有不同的算法,如按重量计算、按距离计算、按固定金额计算等。通过策略模式,将这些算法封装成不同的策略类,客户端可以根据不同的需求选择不同的运费计算策略,使得系统更加灵活,易于扩展新的算法。 - 模板方法模式(Template Method Pattern)
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中实现。比如,制作咖啡和制作茶都有一些相似的步骤,如烧水、冲泡等,但具体的冲泡方式不同。可以在一个抽象类中定义一个制作饮品的模板方法,包含烧水、冲泡等步骤,其中冲泡步骤由具体的咖啡类和茶类子类来实现,这样既复用了相同的代码逻辑,又能根据不同的需求进行定制。 - 观察者模式(Observer Pattern)
观察者模式定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。例如,在一个新闻发布系统中,当有新的新闻发布时,订阅该新闻频道的所有用户都会收到通知。在软件中,被观察的对象(主题)维护一个观察者列表,当主题状态改变时,遍历列表通知所有观察者。 - 迭代器模式(Iterator Pattern)
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。比如,在一个集合类中,我们可以通过迭代器来遍历集合中的元素,而不需要了解集合的具体实现是数组还是链表。迭代器模式使得集合的遍历操作更加统一,并且可以在不改变集合结构的情况下增加新的遍历方式。 - 责任链模式(Chain of Responsibility Pattern)
责任链模式将请求的发送者和接收者解耦,使多个对象都有机会处理这个请求。这些对象连成一条链,请求沿着链传递,直到有一个对象处理它为止。例如,在一个请假审批系统中,请假请求可能先由小组长审批,然后如果超过一定天数,再由部门经理审批,最后由总经理审批。通过责任链模式,将不同的审批者连接成一条链,请求在链上传递,每个审批者根据自己的职责决定是否处理该请求。 - 命令模式(Command Pattern)
命令模式将一个请求封装为一个对象,从而使我们可以用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。比如在一个游戏中,玩家的操作(如攻击、移动等)可以封装成命令对象,这样可以方便地对这些操作进行管理,如记录操作日志、实现撤销功能等。 - 备忘录模式(Memento Pattern)
备忘录模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。例如,在一个文本编辑器中,我们可以通过备忘录模式保存文本的某个历史版本,当需要撤销操作时,就可以从备忘录中恢复到之前的状态。 - 状态模式(State Pattern)
状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。比如,一个手机有不同的状态,如开机、关机、静音等,在不同的状态下,手机对相同的操作(如来电)会有不同的反应。通过状态模式,将每个状态封装成一个状态类,手机对象根据当前的状态来决定如何响应操作,使得代码更加清晰,易于维护和扩展新的状态。 - 访问者模式(Visitor Pattern)
访问者模式将数据结构与数据操作分离。当一个数据结构包含多种类型的元素,且需要对这些元素进行多种不同的操作时,使用访问者模式可以将这些操作封装到不同的访问者类中,而不需要在数据结构的类中添加大量的操作方法。例如,在一个图形系统中,有圆形、方形、三角形等不同的图形对象,现在需要对这些图形进行计算面积、计算周长等不同的操作。通过访问者模式,可以创建计算面积的访问者类和计算周长的访问者类,分别对不同的图形进行相应的操作。 - 中介者模式(Mediator Pattern)
中介者模式用一个中介对象来封装一系列的对象交互。对象之间不再直接交互,而是通过中介者进行交互,这样可以降低对象之间的耦合度。比如在一个聊天系统中,多个用户之间的聊天消息不是直接发送给对方,而是通过服务器(中介者)来转发。在软件中,当多个对象之间存在复杂的交互关系,导致代码难以维护时,使用中介者模式可以将这些交互逻辑集中到中介者对象中,简化对象之间的关系。 - 解释器模式(Interpreter Pattern)
解释器模式给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。例如,在一个简单的数学表达式解析系统中,我们可以定义一个文法来表示数学表达式,然后创建一个解释器来解析和计算这些表达式的值。解释器模式适用于一些特定的领域,当需要处理一些简单的语言解析任务时,可以使用这种模式。
总结
六大设计原则和 23 种设计模式构成了软件设计的核心知识体系。六大设计原则为我们提供了高层次的指导方针,帮助我们从宏观角度构建合理、可维护的软件架构。而 23 种设计模式则是具体的实践工具,针对不同的场景和问题,提供了经过验证的解决方案。
在实际的软件开发过程中,我们需要根据具体的业务需求和场景,灵活运用这些原则和模式。它们不是孤立存在的,而是相互关联、相互补充的。例如,在使用单例模式确保系统中某些资源唯一的同时,可能会遵循单一职责原则,让单例类专注于一项核心功能;在构建复杂系统结构时,结构型模式与行为型模式可能会协同工作,以优化系统的结构和行为。
深入理解并熟练掌握这些原则和模式,不仅能够提高我们的代码质量和开发效率,更能培养我们良好的软件设计思维。随着技术的不断发展和业务需求的日益复杂,软件设计的艺术也在不断演进,但六大设计原则和