零基础设计模式——设计模式入门
第一部分:设计模式入门
欢迎来到设计模式的世界!别担心,这听起来可能很“高大上”,但我们会用最生活化的例子来帮助你理解。
1. 什么是设计模式?
想象一下,你是个大厨,每天都要做很多菜。有些菜的制作流程是固定的,比如“炒”这个动作:热锅、倒油、放葱姜蒜爆香、放主料、翻炒、加调料、出锅。这个“炒”的流程,就是一种“模式”。
设计模式 (Design Pattern) 在软件开发中,就像是大厨做菜的“秘方”或“固定流程”。它是前辈们在解决各种软件设计问题时,总结出来的、可重复使用的、高效的解决方案。
-
设计模式的定义与重要性
- 定义:在特定情境下,针对特定问题的、经过验证的、优雅的解决方案。
- 重要性:
- 经验的沉淀:它们是无数程序员智慧的结晶,避免我们重复造轮子,少走弯路。
- 沟通的桥梁:程序员之间可以用“单例模式”、“工厂模式”这样的术语快速准确地交流设计思想,就像医生说“阑尾炎手术”一样,大家都明白是什么意思。
- 代码质量的保证:使用设计模式通常能写出更健壮、更易维护、更灵活的代码。
-
设计模式的分类
- 创建型模式 (Creational Patterns):关注如何“造东西”(创建对象)。
- 生活例子:你想喝一杯饮料。可以直接去冰箱拿(简单直接),也可以去饮料机选择口味然后机器制作一杯(工厂模式),或者你有一个特别的杯子,全世界独一无二,只能用它喝水(单例模式)。
- 结构型模式 (Structural Patterns):关注如何把“东西”组合起来形成更大的结构。
- 生活例子:你买了一堆乐高积木。怎么把这些小积木搭成一个城堡?这就是结构的问题。你可能需要一个“万能转换头”(适配器模式)来连接两种不兼容的积木。
- 行为型模式 (Behavioral Patterns):关注“东西”之间如何互动和分配责任。
- 生活例子:你去餐厅吃饭。你点菜(请求),服务员(中介者)把你的菜单传给厨师,厨师做菜,服务员再把菜端给你。这个过程中,你、服务员、厨师之间的互动和各自的职责,就涉及到行为模式。
- 创建型模式 (Creational Patterns):关注如何“造东西”(创建对象)。
-
学习设计模式的好处
- 代码复用性:写好的“模块”可以在不同地方重复使用,不用每次都重写。
- 生活例子:你有一个做“红烧肉”的秘方(一个设计模式)。无论是做红烧排骨还是红烧鱼,这个“红烧”的步骤和调料配比大同小异,可以复用。
- 可读性:代码更容易被其他人(或者未来的你)理解。
- 生活例子:一份清晰的菜谱(使用了设计模式的代码)比随手涂鸦的笔记(没有章法的代码)更容易让人看懂如何做菜。
- 可维护性:当需求变化时,修改代码更容易,不容易“牵一发而动全身”。
- 生活例子:模块化的厨房电器,如果微波炉坏了,直接修或换微波炉就行,不会影响冰箱和烤箱(高内聚低耦合)。
- 灵活性/可扩展性:更容易增加新功能。
- 生活例子:你的手机有很多App。每个App都是独立的功能模块。你想增加一个新功能,比如“手电筒”,直接下载一个手电筒App就行,不需要把整个手机系统重写一遍。
- 健壮性:代码更稳定,不容易出错。
- 生活例子:标准化的生产流程(设计模式)生产出来的产品,质量更稳定,次品率更低。
- 代码复用性:写好的“模块”可以在不同地方重复使用,不用每次都重写。
2. 面向对象设计原则 (SOLID)
在学习具体的“武功招式”(设计模式)之前,我们先要修炼“内功心法”(设计原则)。SOLID 原则是面向对象设计的五大基本原则,它们是写出优秀代码的指导方针,也是理解设计模式的基础。
-
S - 单一职责原则 (Single Responsibility Principle - SRP)
- 定义:一个类(或模块、函数)应该只有一个引起它变化的原因。通俗地说,一个类只做一件相关的事情。
- 生活例子:
- 瑞士军刀 vs. 专职工具:一把瑞士军刀功能很多,能开瓶盖、剪指甲、当螺丝刀。但如果只是想拧螺丝,一把专门的螺丝刀可能更好用,也更专业。如果瑞士军刀的某个功能坏了,可能整个刀都受影响。
- 餐厅里的角色:厨师负责做菜,服务员负责点菜和上菜,收银员负责结账。如果一个厨师既要做菜,又要点菜,还要收银,那他会非常忙乱,效率低下,而且任何一个环节出问题都可能影响其他环节。
- 软件中的体现:一个类叫
UserAuth
,它只负责用户的登录、注册、权限验证。另一个类叫UserProfile
,它只负责管理用户的个人信息(昵称、头像等)。这样职责分明,修改用户验证逻辑不会影响到个人信息管理。
-
O - 开闭原则 (Open/Closed Principle - OCP)
- 定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 生活例子:
- USB接口:你的电脑有很多USB接口。你可以插U盘、鼠标、键盘、打印机等等。电脑的USB功能是“开放”给这些新设备的(扩展开放)。但你不需要为了插一个新的U盘而把电脑拆开,修改主板电路(修改关闭)。
- 商场促销活动:商场经常搞各种促销活动,比如“满100减20”、“打8折”、“买一送一”。对于收银系统来说,它应该能够很容易地增加新的促销规则(对扩展开放),而不需要去修改原来已经写好的计算价格的核心代码(对修改关闭)。
- 软件中的体现:通过接口、抽象类、回调等机制,当需要增加新功能时,我们倾向于增加新的类或模块来实现,而不是去改动已有的、稳定的代码。
-
L - 里氏替换原则 (Liskov Substitution Principle - LSP)
- 定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。也就是说,子类对象能够替换其父类对象,而程序行为不发生改变。
- 生活例子:
- 鸟会飞吗?:我们说“鸟会飞”。燕子是鸟,它会飞,没问题。企鹅也是鸟,但它不会飞。如果一个程序期望所有“鸟”都能飞,那么把企鹅传进去就会出问题。这就不符合里氏替换原则。更好的设计可能是,并非所有鸟都会飞,或者有一个“会飞的鸟”的子分类。
- 遥控器:你有一个电视遥控器(父类),它可以控制电视。现在你买了一个新的智能电视,它的遥控器(子类)除了能控制电视,还能语音输入。在所有只需要“控制电视”的地方,这个新的智能遥控器应该也能正常工作,就像旧遥控器一样。
- 软件中的体现:子类继承父类时,不应该重写父类的方法并改变其原有的行为逻辑,尤其是不要缩小父类方法的能力范围或抛出父类没有声明的异常。
-
I - 接口隔离原则 (Interface Segregation Principle - ISP)
- 定义:客户端不应该被迫依赖于它不使用的方法。一个类对另一个类的依赖应该建立在最小的接口上。
- 生活例子:
- 多功能打印机 vs. 专用打印机:一台多功能一体机,有打印、复印、扫描、传真功能。如果你只是想打印文件,但这个一体机的操作界面非常复杂,把所有功能都堆在一起,你可能需要找半天打印按钮。更好的方式是,即使是多功能一体机,也应该提供简洁的“打印”专用界面或按钮。
- 餐厅菜单:一个大而全的菜单,包含了中餐、西餐、日料、韩料。如果你只想吃中餐,却要翻阅所有菜品,就很麻烦。更好的做法是提供分类菜单,比如“中餐菜单”、“西餐菜单”。
- 软件中的体现:定义小而专的接口,而不是大而全的接口。如果一个类需要实现很多功能,可以考虑将这些功能拆分到不同的接口中。
例如,一个IWorker
接口有work()
和eat()
方法。对于机器人来说,它只需要work()
,不需要eat()
。那么更好的设计是拆分成IWorkable
(有work()
) 和IEatable
(有eat()
) 两个接口,人类实现这两个接口,机器人只实现IWorkable
。
-
D - 依赖倒置原则 (Dependency Inversion Principle - DIP)
- 定义:高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
- 生活例子:
- 插座和电器:你家里的墙上有插座(抽象)。你的台灯、电脑、手机充电器(细节/低层模块)都是按照插座的标准设计的,可以直接插到任何符合标准的插座上使用。你不需要为每一种电器都准备一种特定的墙壁接口。高层模块(你使用电器的需求)和低层模块(各种电器)都依赖于“插座标准”这个抽象。
- 老板和员工:老板(高层模块)不会直接关心每个员工(低层模块)具体是怎么完成任务的。老板只关心任务是否完成(依赖于“任务完成”这个抽象)。员工则按照任务要求(抽象)去具体执行。
- 软件中的体现:多使用接口或抽象类进行编程。高层业务逻辑(比如订单处理流程)不直接依赖于具体的数据库实现(比如MySQL或Oracle),而是依赖于一个通用的数据访问接口。这样,如果以后想换数据库,只需要提供一个新的实现了该接口的数据库访问类即可,高层业务逻辑不需要改动。
-
其他相关原则:
-
迪米特法则 (Law of Demeter - LoD) / 最少知识原则 (Least Knowledge Principle - LKP)
- 定义:一个对象应该对其他对象保持最少的了解。通俗地说,只和你的直接朋友交谈,不要和朋友的朋友交谈。
- 生活例子:你去ATM机取钱。你只需要和ATM机交互(插入银行卡、输入密码、选择金额)。你不需要知道ATM机内部是如何连接到银行系统、如何验证密码、如何吐钞的。ATM机帮你处理了这些复杂性。
- 软件中的体现:一个类的方法应该尽量少地调用其他类的方法,尤其是那些通过好几层关系才能访问到的对象的方法。这有助于降低类之间的耦合度。
-
组合/聚合复用原则 (Composition/Aggregation Reuse Principle - CARP)
- 定义:尽量使用对象组合/聚合,而不是继承来达到复用的目的。
- 生活例子:
- 汽车:一辆汽车是由发动机、轮胎、方向盘、座椅等部件“组合”而成的。汽车“拥有”一个发动机,而不是汽车“是”一个发动机(继承)。如果发动机坏了,可以换一个新的发动机,而不需要把整辆车都换掉。这种组合关系更灵活。
- 电脑:你的电脑由CPU、内存条、硬盘、显卡等组件构成。你可以升级内存条,或者换一个更大的硬盘,而不需要换掉整个电脑。
- 软件中的体现:当一个类需要另一个类的功能时,优先考虑将另一个类的对象作为当前类的一个成员变量(组合),而不是让当前类去继承那个类。组合比继承更灵活,耦合度更低。
-
理解并运用好这些设计原则,是掌握设计模式、写出高质量代码的关键第一步。它们就像武侠小说里的内功心法,虽然不直接教你怎么打败敌人,但深厚的内力能让你学习任何招式都事半功倍!