七大软件设计原则
开闭原则
开闭原则(Open-Closed Principle, OCP)是指一个软件实体如类、模块和函数应该对扩展开放, 对修改关闭。所谓的开闭,也正是对扩展和修改两个行为的一个原则。强调的是用抽象构建框架,用实 现扩展细节。可以提高软件系统的可复用性及可维护性。开闭原则,是面向对象设计中最基础的设计原 则。它指导我们如何建立稳定灵活的系统,例如:我们版本更新,我尽可能不修改源代码,但是可以增 加新功能。
提取提一下重点:开:对扩展开放, 闭:对修改关闭。具体如何实现这种原则呢,就是用抽象构建框架,用实现扩展细节。核心思想其实就是面向抽象
代码示例
比如我们现在售卖一个课程,然后突然有个需求是打折出售课程 不考虑开闭原则可能我们会这样写
public interface ICourse { Double getPrice(); }
public class Course implements ICourse{ private Integer type; public Double getPrice() { if(type != null && type == 1){ return 100.0 * 0.6; } return 100.0; } public Integer getType() { return type; } public void setType(Integer type) { this.type = type; } }
public class Test { public static void main(String[] args) { printCoursePrice(new Course()); Course course = new Course(); course.setType(1); printCoursePrice(course); } private static void printCoursePrice(ICourse course){ System.out.println(course.getPrice() + "元"); } }
这样做会发现每次新添加新东西都需要更改老代码,那么老代码就会越来越臃肿不好维护,同时可复用性也变差了应为这里融入了很多其他的逻辑。
改良后:
public class Course implements ICourse{ public Double getPrice() { return 100.0; } }
public class DiscountCourse extends Course{ @Override public Double getPrice() { return super.getPrice() * 0.60; } }
public class Test { public static void main(String[] args) { printCoursePrice(new Course()); printCoursePrice(new DiscountCourse()); } private static void printCoursePrice(ICourse course){ System.out.println(course.getPrice() + "元"); } }
这样新增逻辑不会动到老代码,每个类的职责也很清晰易于维护
依赖倒置原则
依赖倒置原则(Dependence Inversion Principle,DIP)是指设计代码结构时,高层模块不应该依 赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节;细节应该依赖抽象。通过依赖倒置,可以 减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并能够降低修改程序所 造成的风险。
通俗点讲其实就是面向接口变成
代码示例
这里我们可以举个例子比如现在有个用户去购买课程,支付方式有微信支付和支付宝支付 不考虑依赖倒置原则可能我们会这样写
public class User { public void pay(Course course,Integer payType){ //获取到课程的价格 Double price = course.getPrice(); switch (payType) { case 1 -> //支付宝 //调用支付宝支付sdk System.out.println("支付宝支付了" + price + "元"); case 2 -> //微信支付 //调用微信支付sdk System.out.println("微信支付了" + price + "元"); default -> { } } } }
如果这样写的话想要添加银联支付需要在原来的代码基础上继续修改添加(增加一种类型),这样子做主要的缺点是:
- 导致代码会有出错的风险就有可能影响到以前已经接入好的功能,
- 整个user模块的pay方***越来越多代码的可阅读性也不高.
针对于上述的问题我们可以将支付提取抽象类,然后创建实体去实现,具体代码如下
public interface IPay { void pay(Course course); }
public class AliPay implements IPay{ @Override public void pay(Course course) { //调用支付宝支付sdk System.out.println("支付宝支付了" + course.getPrice() + "元"); } }
public class WeixinPay implements IPay{ @Override public void pay(Course course) { //调用微信支付sdk System.out.println("微信支付了" + course.getPrice() + "元"); } }
public class User { public void pay(IPay pay,Course course){ pay.pay(course); } }
public class Test { public static void main(String[] args) { User user = new User(); Course course = new Course(100.0); user.pay(new WeixinPay(),course); user.pay(new AliPay(),course); } }
这样如果支付宝支付出现问题了比如sdk需要升级,只需要修改 AliPay一个类就行了,完全不用担心微信支付会有问题,如果需要新添加银联支付,也只需要新添加一个实现类就好了,完全不会改到以前的旧代码,这个例子也充分体现了开闭原则。
注意:以抽象为基准比以细节为基准搭建起来的架构要稳定得多,因此大家在拿到需求之后, 要面向接口编程,先顶层再细节来设计代码结构
单一职责原则
单一职责(Simple Responsibility Pinciple,SRP)是指不要存在多于一个导致类变更的原因。假 设我们有一个 Class 负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能会导致 另一个职责的功能发生故障。这样一来,这个 Class 存在两个导致类变更的原因。如何解决这个问题呢? 我们就要给两个职责分别用两个 Class 来实现,进行解耦。后期需求变更维护互不影响。这样的设计, 可以降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。总体来说就是一 个 Class/Interface/Method 只负责一项职责。
还是拿课程来举例子比如我们现在有直播课和录播课都有获取课程信息的接口,但是会有些差异比如直播课有开始时间,录播课没有,如果不考虑单一职责的话我们可能会这样子设计:
public interface ICourse { Map<String,Object> getCourseInfo(Integer courseType); }
public class Course implements ICourse{ @Override public Map<String, Object> getCourseInfo(Integer courseType) { Map<String, Object> map = new HashMap<>(); map.put("title","课程"); if(courseType == 1){ //直播课有开始时间 map.put("startTime","2015-9-1"); } return map; } }
这样子写就会发现这个类干了很多事情,如果需求某天说直播课要改一点东西你就要改这个类,如果该录播课的东西一样要改这个类,这样这个类出现bug的几率就会增加.
修改一下如下:
public class Course implements ICourse{ @Override public Map<String, Object> getCourseInfo(Integer courseType) { Map<String, Object> map = new HashMap<>(); map.put("title","课程"); return map; } }
public class LiveCourse implements ICourse{ @Override public Map<String, Object> getCourseInfo(Integer courseType) { Map<String, Object> map = new HashMap<>(); map.put("title","课程"); map.put("startTime","2015-9-1"); return map; } }
新增一个直播课程的类,需求说改那个我们就改哪个类,降低了出现bug的风险
1、一个类对一类的依赖应该建立在最小的接口之上。 2、建立单一接口,不要建立庞大臃肿的接口。 3、尽量细化接口,接口中的方法尽量少(不是越少越好,一定要适度)。 对于接口也是这样,比如系统存在权限的问题,可以将需要权限判断的接口放在一个接口,不需要权限判断的放在一个接口。
对于方法的单一也是很重要的,必须修改用户信息方法,有些人可能会这样写修改的时候更多的是直接传入一个对象,这个对象里面有用户昵称、手机号、年龄等,需求有修改手机号、昵称、年龄,然后就是统一调用更新的方法如下:
public void updateUserInfo(String userName, String userEmail, String userPassword){ }
只要设计修改用户信息的都调用这个一个方法,最后发现很多地方都在调用,一旦某一个功能需要修改就需要动到这个方法,逻辑也会越来越多,这个方法就会变的臃肿。所以应该如下设计:
public void updateUserName(String userName, String userEmail, String userPassword){ } public void updateUserEmail(String userName, String userEmail, String userPassword){ } public void updateUserPassword(String userName, String userEmail, String userPassword){ }
每个方法都有明确的职责,那个有问题了改那个方法,也减小了出现bug的风险
接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP)是指用多个专门的接口,而不使用单一的 总接口,客户端不应该依赖它不需要的接口。这个原则指导我们在设计接口时应当注意一下几点:
- 一个类对一类的依赖应该建立在最小的接口之上。
- 建立单一接口,不要建立庞大臃肿的接口。
- 尽量细化接口,接口中的方法尽量少(不是越少越好,一定要适度)。
代码示例:
public interface IAnimal { void run(); void fly(); void swim(); }
public class bird implements IAnimal{ @Override public void run() { } @Override public void fly() { System.out.println("bird fly"); } @Override public void swim() { } }
public class Dog implements IAnimal{ @Override public void run() { System.out.println("Dog run"); } @Override public void fly() { } @Override public void swim() { } }
发现其实鸟和狗这两个类都只想实现一个方法但是应为所有的方法都写在了一个接口所以没办法只能实现一些不需要的方法,这里就可以将接口叉开如下:
public interface IFlyAnimal { void fly(); }
public interface IRunAnimal { void run(); }
public class bird implements IFlyAnimal{ @Override public void fly() { System.out.println("bird fly"); } }
public class Dog implements IRunAnimal{ @Override public void run() { System.out.println("Dog run"); } }
不同的动物实现不同的接口
迪米特法则
迪米特原则(Law of Demeter LoD)是指一个对象应该对其他对象保持最少的了解,又叫最少知 道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合。迪米特原则主要强调只和朋友交流,不和陌生人说话。
所谓的朋友可以是一下几种情况:
- 出现在成员变量
- 方法的入参
- 方法的出参
代码示例
比如湖人的总教练需要让助手统计对手最近10场比赛的总得分:
教练指定一个助手去统计:
public class Coach { public void printlnScore(Helper helper) { List<Game> games = new ArrayList<>(); for(int i = 0; i < 10; i++) { games.add(new Game()); } System.out.println(helper.returnScore(games)); } }
这个助手返回总得分
public class Helper { public Integer returnScore(List<Game> games){ Integer totalScore = 0; for (Game game : games) { totalScore += game.getScore(); } return totalScore; } }
比赛
public class Game { public Integer getScore() { return 100; } }
根据类图可以发现教练 这个类耦合了比赛这个类,但是其实教练不需要知道比赛的东西,只需要一个总得分,所以可以这样设计:
public class Coach { public void printlnScore(Helper helper) { System.out.println(helper.returnScore()); } }
public class Helper { public Integer returnScore(){ List<Game> games = new ArrayList<>(); for(int i = 0; i < 10; i++){ games.add(new Game()); } Integer totalScore = 0; for (Game game : games) { totalScore += game.getScore(); } return totalScore; } }
这样就降低了教练类和比赛类的耦合,比赛类你想怎么改跟我没关系只跟你的朋友有关系
里氏替换原则
里氏替换原则(Liskov Substitution Principle,LSP)是指如果对每一个类型为 T1 的对象 o1,都有 类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都替换成 o2 时,程序 P 的行为没 有发生变化,那么类型 T2 是类型 T1 的子类型。(个人感觉更像是开闭原则的一个补充)
用比较通俗电话说,就是如果你要创建一个A类的子类B,那么在原有的逻辑中会有引用A类的引用,此时拿B类的实例替换A类的实例原来的这个逻辑不会变,这就满足了里氏替换原则。
我们可以总结出一些几条结论:
- 如果实现集成是想共享代码,最好不要更改父类已有的方法,当然如果是抽象类是可以实现抽象方法的(应为抽象类不能实例化也就不存在替换的问题),如果想要扩展功能可以考虑添加新的方法。这样在原逻辑中用新创建的子类是可以替换父类的而原逻辑不会发生改变
- 如果是想重载/重写父类的方法(不改变逻辑的情况下)必须满足入参的范围子类要更大,出参的范围子类要更小(还是不建议重写或者重载父类方法,当然抽象方法除外)
- 如果是想实现多态建议父类使用抽象类,子类继承实现抽象方法
上文在说到开闭原则的时候就使用了一个错误的示例
public class DiscountCourse extends Course{ @Override public Double getPrice() { return super.getPrice() * 0.60; } }
此时如果想用DiscountCourse替换Course那么原有的程序逻辑就会出现问题,本来原价售卖的课程现在也变成了打折出售可以这样设计
public class DiscountCourse extends Course{ public Double getDiscountPrice() { return super.getPrice() * 0.60; } }
扩展一个获取打折后价格的方法这样就可以使用子类替换父类不影响原有的逻辑
合成复用原则
合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)/ 聚合(contanis-a),而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦 合度,一个类的变化对其他类造成的影响相对较少
举一个最常见的例子:连接数据库从数据库取数据:
public class DBConnection { public String getConnection(){ return "数据库链接"; } }
public class UserDao { private DBConnection dbConnection; public void setConnection(DBConnection dbConnection) { this.dbConnection = dbConnection; } public void getUserInfo(){ String connection = this.dbConnection.getConnection(); //然后获取数据 } }#Java##程序员#