1、为什么遵守原则
编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性、可扩展性、重用性、灵活性等多方面的挑战,设计模式是为了让程序,具有更好代码重用性、可读性、可扩展性、可靠性,使程序呈现高内聚,低耦合的特性。设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模式的基础,即设计模式为什么这样设计的依据。
在程序设计领域,SOLID(单一职责、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。SOLID所包含的原则是通过引发编程者进行软件源代码的代码重构进行软件的代码异味清扫,从而使得软件清晰可读以及可扩展时可以应用的指南。
除了以上五个经典的原则以外,其实还有两个常被提到的原则:合成复用原则、最少知识原则(又称迪米特法则)。
2、单一职责原则(SRP)
-
定义:一个类应该仅有一个引起它变化的原因,即一个类只负责一项职责。
-
好处:降低类的复杂度,提升代码可读性,便于维护与扩展,增强代码的可测试性。
-
遵循原因:职责单一的类更易于理解和维护,当需求变动时,仅需在对应的类中修改,不会影响其他功能,降低了代码出错的风险,提高开发效率。
-
示例:在文件处理系统中,文件读取类只负责从文件中读取数据,而不涉及文件内容的解析、存储等其他功能。
// 不遵循单一职责原则 class FileProcessor { public String readFile(String file_path) { // 读取文件逻辑 return "文件内容"; } public String processData(String data) { // 处理数据逻辑 return data.toUpperCase(); } public void writeFile(String file_path, String data) { // 写入文件逻辑 } } // 遵循单一职责原则 class FileReader { public String readFile(String file_path) { // 读取文件逻辑 return "文件内容"; } } class DataProcessor { public String processData(String data) { return data.toUpperCase(); } } class FileWriter { public void writeFile(String file_path, String data) { // 写入文件逻辑 } }
3、开闭原则(OCP)
-
定义:软件实体(类、模块、函数等)应当对扩展开放,对修改关闭。当软件需求发生变化时,应通过扩展现有代码来实现,而非修改已有的稳定代码。
-
好处:提高软件的可维护性和可扩展性,减少因修改代码而引入的风险,降低维护成本。
-
遵循原因:随着业务发展,软件需求不断变化,遵循开闭原则能在不破坏原有稳定功能的基础上,通过添加新代码实现新功能,保证软件的稳定性和可靠性。
-
示例:在图形绘制系统中,若要新增一种图形(如菱形),不改变原有的绘制圆形、矩形等图形的代码,而是创建新的菱形绘制类来实现。
abstract class Shape { public abstract void draw(); } class Circle extends Shape { @Override public void draw() { System.out.println("绘制圆形"); } } class Rectangle extends Shape { @Override public void draw() { System.out.println("绘制矩形"); } } // 新增三角形,不修改原有代码 class Triangle extends Shape { @Override public void draw() { System.out.println("绘制三角形"); } } class ShapeDrawer { public static void drawShapes(Shape[] shapes) { for (Shape shape : shapes) { shape.draw(); } } } public class Main { public static void main(String[] args) { Shape[] shapes = {new Circle(), new Rectangle(), new Triangle()}; ShapeDrawer.drawShapes(shapes); } }
4、里氏替换原则(LSP)
-
定义:所有引用基类的地方必须能透明地使用其子类的对象,子类对象能够完全替换掉基类对象,且程序的行为不会发生改变。
-
好处:确保继承体系的正确性和稳定性,提高软件的可维护性和可扩展性,增强代码的复用性。
-
遵循原因:它保证了继承关系的合理性,使得基于基类编写的代码可以安全地使用子类对象,避免因子类行为与基类不一致导致的程序错误,提高代码的健壮性。
-
示例:鸟类都有飞行的方法,若企鹅类继承自鸟类,但企鹅不会飞行,当在需要鸟类对象的地方使用企鹅对象时,就会出现错误,违反了里氏替换原则。
class Bird { public void fly() { System.out.println("鸟在飞行"); } } class Sparrow extends Bird { @Override public void fly() { System.out.println("麻雀在飞行"); } } class Penguin extends Bird { // 企鹅不会飞,违反里氏替换原则 @Override public void fly() { throw new UnsupportedOperationException("企鹅不会飞"); } } class BirdFlyer { public static void makeBirdFly(Bird bird) { bird.fly(); } } public class Main { public static void main(String[] args) { Sparrow sparrow = new Sparrow(); Penguin penguin = new Penguin(); BirdFlyer.makeBirdFly(sparrow); // 这里调用会抛出异常,体现违反里氏替换原则 // BirdFlyer.makeBirdFly(penguin); } }
5、接口隔离原则(ISP)
-
定义:客户端不应该被迫依赖那些它不使用的接口,一个类对另一个类的依赖应该建立在最小的接口上。
-
好处:降低类之间的耦合度,提高软件的灵活性和可维护性,避免接口的滥用。
-
遵循原因:避免接口过于庞大复杂,使类只依赖实际需要的接口,减少不必要的依赖关系,降低维护难度,提高代码的可维护性和灵活性。
-
示例:有一个动物接口,包含飞行、游泳、奔跑等方法。但猫类只需要奔跑功能,若强制实现其他方法就不合理。应将动物接口拆分为多个小接口,如飞行接口、游泳接口、奔跑接口等,猫类只需实现奔跑接口。
// 不遵循接口隔离原则 interface AnimalActions { void fly(); void swim(); void run(); } class Cat implements AnimalActions { @Override public void fly() { throw new UnsupportedOperationException("猫不会飞"); } @Override public void swim() { throw new UnsupportedOperationException("猫不擅长游泳"); } @Override public void run() { System.out.println("猫在奔跑"); } } // 遵循接口隔离原则 interface Flyable { void fly(); } interface Swimmable { void swim(); } interface Runnable { void run(); } class NewCat implements Runnable { @Override public void run() { System.out.println("新猫在奔跑"); } }
6、依赖倒置原则(DIP)
-
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。也就是要面向接口编程,而非面向实现编程。
-
好处:降低模块之间的耦合度,提高系统的稳定性和可维护性,增强代码的可扩展性和可复用性。
-
遵循原因:依赖抽象而非具体实现,使得模块之间的依赖关系更加松散,当底层实现发生变化时,高层模块不受影响,便于系统的升级和维护,也有利于代码的复用。
-
示例:在电商系统中,订单模块(高层模块)不直接依赖支付模块(低层模块)的具体实现,而是依赖支付接口,支付模块实现该接口,这样当支付方式发生变化时,订单模块不需要修改。
interface Payment { void pay(double amount); } class Alipay implements Payment { @Override public void pay(double amount) { System.out.println("使用支付宝支付" + amount + "元"); } } class WeChatPay implements Payment { @Override public void pay(double amount) { System.out.println("使用微信支付" + amount + "元"); } } class Order { private Payment payment; public Order(Payment payment) { this.payment = payment; } public void placeOrder(double amount) { payment.pay(amount); } } public class Main { public static void main(String[] args) { Payment alipay = new Alipay(); Order order = new Order(alipay); order.placeOrder(100); Payment weChatPay = new WeChatPay(); order = new Order(weChatPay); order.placeOrder(200); } }
7、迪米特法则(LoD)
-
定义:一个对象应该对其他对象有最少的了解,即一个软件实体应当尽可能少地与其他实体发生相互作用。
-
好处:降低系统的耦合度,提高系统的可维护性和可扩展性,增强系统的稳定性。
-
遵循原因:减少对象间的交互,使得系统各部分相对独立,当某个部分发生变化时,对其他部分的影响最小化,提高系统的可维护性和扩展性。
-
示例:在公司管理系统中,员工类只需要了解自己的工作任务和直属上级,而不需要了解公司其他部门的详细信息和其他员工的具体工作内容。
class TeamMember { private String name; public TeamMember(String name) { this.name = name; } public void doTask() { System.out.println(name + "正在执行任务"); } } class TeamLead { private TeamMember[] members; public TeamLead() { members = new TeamMember[]{new TeamMember("成员1"), new TeamMember("成员2")}; } public void assignTask() { for (TeamMember member : members) { member.doTask(); } } } public class Main { public static void main(String[] args) { TeamLead teamLead = new TeamLead(); teamLead.assignTask(); } }
8、合成复用原则(CRP)
-
定义:尽量使用对象组合,而不是继承来达到复用的目的。
-
好处:降低类之间的耦合度,提高软件的可维护性和可扩展性,使代码更加灵活。
-
遵循原因:继承会导致类之间的关系紧密,不利于维护和扩展。而对象组合更加灵活,当需要修改或替换某个功能时,只需更换组合的对象,而不会影响整个类的结构。
-
示例:若要给汽车添加导航功能,可创建一个导航类,在汽车类中组合导航类的对象,而不是让汽车类继承导航类。
// 导航类 class Navigation { public void navigate() { System.out.println("正在导航,为您规划最佳路线"); } } // 汽车类,通过组合导航类实现导航功能 class Car { private Navigation navigation; public Car() { this.navigation = new Navigation(); } // 提供使用导航的方法 public void useNavigation() { navigation.navigate(); } } public class Main { public static void main(String[] args) { Car car = new Car(); car.useNavigation(); } }