单一职责原则
单一职责原则强调类的职责单一性和变化的原因唯一性,通过将职责分离到不同的类中,减少类的复杂度,提高代码的可读性、可维护性和可复用性。在实际开发中,遵循单一职责原则有助于构建高内聚、低耦合的系统。
核心理念是:一个类应该只有一个引起它变化的原因,或者说一个类应该仅有一个职责。换句话说,一个类只负责一个功能领域中的一项职责。
- 职责单一:
- 一个类只负责一项职责,这样可以使类的实现更加清晰和简洁。
- 如果一个类承担了过多的职责,它就会变得臃肿且难以维护。
- 变化的原因:
- 一个类只应该有一个引起它变化的原因。
- 当某个类承担了多个职责时,这些职责可能会因为不同的原因发生变化,从而导致该类需要频繁修改,增加了出错的可能性。
例子
假设我们有一个类Employee
,它负责员工的基本信息管理和薪资计算:
在这个例子中,Employee
类同时负责员工信息管理和薪资计算,违反了单一职责原则。如果我们需要修改薪资计算逻辑或修改员工信息管理逻辑,都需要修改这个类,这增加了代码的耦合性和维护难度。
为了遵循单一职责原则,我们可以将Employee
类的职责分离为两个类:一个类负责员工信息管理,另一个类负责薪资计算。
通过这样的重构,Employee
类和SalaryCalculator
类各自只负责一个职责,从而遵循了单一职责原则。这使得每个类的变化只会受到一种原因的影响,增加了代码的可维护性和可扩展性。
开放封闭原则
开放封闭原则旨在通过对系统进行扩展而不是修改,来提高代码的可维护性、可扩展性和稳定性。要求软件系统在增加新功能时,不需要修改现有的代码,而是通过增加新代码来实现。
开放-封闭原则的核心理念是:软件实体(如类、模块和函数)应该对扩展开放,对修改封闭。
- 对扩展开放:
- 软件实体应该可以被扩展以支持新的行为或功能。
- 这种扩展通常通过继承(继承类)或实现接口(多态)来实现。
- 对修改封闭:
- 一旦软件实体完成开发并经过测试,就不应该轻易修改它。
- 避免对现有代码的修改可以减少引入新bug的风险,确保系统的稳定性。
例子 计算图形面积
假设有一个用于计算图形面积的程序,最初只支持矩形。
现在如果我们想要扩展该程序,使其支持圆形,我们应该如何实现?
按照开放封闭原则,我们不应该修改现有的Rectangle
类和AreaCalculator
类,而是通过扩展系统来支持新功能。
我们可以通过引入一个抽象基类Shape
来表示图形,然后让Rectangle
和新引入的Circle
类继承Shape
,并修改AreaCalculator
以支持新的形状类型。
通过上述改动,我们实现了对系统的扩展(添加了对圆形的支持),而没有修改原有的Rectangle
类和AreaCalculator
类的实现。这样做符合开放封闭原则。
依赖倒转原则
依赖倒转原则强调,高层模块和低层模块都应该依赖于抽象,而不是具体实现。
依赖倒转原则旨在减少高层模块和低层模块之间的耦合,使得系统更容易扩展和维护。
通过引入接口或抽象类,并让具体实现依赖于这些抽象,系统可以更灵活地应对变化和扩展需求。
基本思想:高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
高层模块和低层模块:
- 高层模块通常是指那些实现复杂逻辑或业务规则的模块。可以理解为上层应用、业务层的实现。
- 低层模块通常是指那些实现基础服务或细节功能的模块。可以理解为底层接口,比如封装好的API、动态库等。
抽象和具体实现:
- 抽象通常是指接口或抽象类,它定义了行为的契约,而不关心具体的实现细节。
- 具体实现是指实现了这些接口或抽象类的具体类。
例子 一个高层模块依赖低层模块的例子
某项目低层使用的是 MySql 的数据库接口,高层基于这套接口对数据库表进行了添删查改,实现了对业务层数据的处理。
而后由于某些原因,数据量暴增,因此计划更换为 Oracle 数据库。
高层代码的数据库操作部分直接调用了低层的接口,由于低层的数据库接口变了,因此高层代码也需要进行对应的修改,无法直接复用高层代码,开发者欲哭无泪。
通过上面的例子得知,当依赖的低层模块变了,就会牵一发而动全身。
例子
假设我们有一个简单的日志系统,其中FileLogger
类用于将日志信息写入文件,而Application
类则是高层模块,负责业务逻辑,并需要使用FileLogger
来记录日志。
在这个例子中,Application
类依赖于具体的FileLogger
类,这违反了依赖倒转原则。如果我们需要更换日志记录方式,比如改为记录到数据库或远程服务器,则需要修改Application
类,增加了耦合性和维护难度。
为了遵循依赖倒转原则,我们可以引入一个抽象的日志接口,并让Application
依赖这个抽象接口,而不是具体实现。
通过这种方式,Application
类只依赖于ILogger
接口,而不依赖于具体的FileLogger
类。这样一来,如果我们需要更换日志记录方式,只需创建新的实现类,而无需修改Application
类。
里氏代换原则
里氏代换原则要求子类必须能够替换父类,而不会改变程序的正确性和行为。遵守这个原则有助于构建可靠和稳定的继承层次结构,提高代码的可维护性和可扩展性。
核心理念是:如果S
是T
的子类,那么在任何可以使用T
的地方都可以使用S
而不会因此发生错误。即:子类必须能够替换掉它们的父类。
子类型替换父类型:
- 子类对象可以替换父类对象,程序的行为不会改变。
- 子类应保证不改变父类的预期行为。
行为一致性:
- 子类必须确保不破坏父类所具有的行为特征。
- 子类可以扩展父类的行为,但不能改变原有的行为。
契约:
- 子类应遵守父类的“契约”(接口或抽象类中的方法),不能违反这些契约。
例子
假设我们有一个基类Bird
,表示鸟类,并有一个子类Penguin
,表示企鹅。Bird
类有一个fly
方法,因为大多数鸟都会飞:
在这个例子中,如果我们在代码的某些地方使用了Bird
类,并调用了它的fly
方法,我们期望所有鸟类对象都能飞。但是,如果我们用Penguin
替换Bird
,企鹅不会飞,这就违反了里氏代换原则。
为了遵守里氏代换原则,我们应该重新设计类层次结构,使得子类不会违背父类的行为。一个可能的解决方案是将飞行行为分离到一个独立的接口中,而不是在基类中定义:
在这种设计中,只有会飞的鸟类才实现Flyable
接口,而Penguin
类则不会实现该接口: