详解设计模式六大原则
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
借用并改编一下鲁迅老师《故乡》中的一句话,一句话概括设计模式: 希望本无所谓有,无所谓无.这正如coding的设计模式,其实coding本没有设计模式,用的人多了,也便成了设计模式
设计模式(面向对象)有六大原则:
- 开闭原则(Open Closed Principle,OCP)
- 里氏代换原则(Liskov Substitution Principle,LSP)
- 依赖倒转原则(Dependency Inversion Principle,DIP)
- 接口隔离原则(Interface Segregation Principle,ISP)
- 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
- 最小知识原则(Principle of Least Knowledge,PLK,也叫迪米特法则)
开闭原则具有理想主义的色彩,它是面向对象设计的终极目标。其他几条,则可以看做是开闭原则的实现方法。 设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。
1.概念:
2.模拟场景:
3.Solution:
4.注意事项:
- 通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法
- 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类
- 抽象层尽量保持稳定,一旦确定即不允许修改
5.开闭原则的优点:
- 可复用性
- 可维护性
6.开闭原则图解:
1.概述: 派生类(子类)对象能够替换其基类(父类)对象被调用
2.概念:
3.子类为什么可以替换父类的位置?:
4.里氏代换原则优点:
5.里氏代换原则Demo:
代码正文:
/** * @Author toutou * @Date 2023/6 * @Des */ public class Transportation { public Transportation(){ System.out.println("Transportation"); } public void say(){ System.out.println("121"); } } /** * @Author toutou * @Date 2023/6 * @Des */ public class Sedan extends Transportation { public Sedan(){ System.out.println("Transportation:Sedan"); } @Override public void say(){ System.out.println("Sedan"); } } /** * @Author toutou * @Date 2023/6 * @Des */ public class Bicycles extends Transportation { public Bicycles(){ System.out.println("Transportation:Bicycles"); } @Override public void say(){ System.out.println("Bicycles"); } }
代码效果:
6.里氏代换原则图解:
1.概念:
2.依赖倒转原则用处:
3.注意事项:
- 高层模块不应该依赖低层模块。两个都应该依赖抽象。
- 抽象不应该依赖细节。细节应该依赖抽象。
4.模拟场景:
场景:
假设现在需要一个Monitor工具,去运行一些已有的APP,自动化来完成我们的工作。Monitor工具需要启动这些已有的APP,并且写下Log。
代码实现1:
/** * @Author toutou * @Date 2023/6 * @Des */ public class AppOne { public boolean start() { System.out.println("1号APP开始启动"); return true; } public boolean exportLog() { System.out.println("1号APP输出日志"); return true; } } /** * @Author toutou * @Date 2023/6 * @Des */ public class AppTwo { public boolean start() { System.out.println("2号APP开始启动"); return true; } public boolean exportLog() { System.out.println("2号APP输出日志"); return true; } } /** * @Author toutou * @Date 2023/6 * @Des */ public class Monitor { private AppOne appOne = new AppOne(); private AppTwo appTwo = new AppTwo(); private AppNumber number; public Monitor(AppNumber number) { this.number = number; } public boolean startApp() { return number == AppNumber.AppOne ? appOne.start() : appTwo.start(); } public boolean ExportAppLog() { return number == AppNumber.AppOne ? appOne.exportLog() : appTwo.exportLog(); } }
代码解析1:
在代码实现1中我们已经轻松实现了Monitor去运行已有APP并且写下LOG的需求。并且代码已经上线了.
春...夏...秋...冬...
春...夏...秋...冬...
春...夏...秋...冬...
就这样,三年过去了。
一天客户找上门了,公司业务扩展了,现在需要新加3个APP用Monitor自动化。这样我们就必须得改Monitor。
代码实现2:
/** * @Author toutou * @Date 2023/6 * @Des */ public enum SecondAppNumber { AppOne(1), AppTwo(2), AppThree(3), AppFour(4), AppFive(5); private int code; SecondAppNumber(int code){ this.code = code; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } } /** * @Author toutou * @Date 2023/6 * @Des */ public class SecondMonitor { private AppOne appOne = new AppOne(); private AppTwo appTwo = new AppTwo(); private App3 app3 = new App3(); private App4 app4 = new App4(); private App5 app5 = new App5(); private SecondAppNumber number; public SecondMonitor(SecondAppNumber number) { this.number = number; } public boolean StartApp() { boolean result = false; if (number == SecondAppNumber.AppOne) { result = appOne.start(); } else if (number == SecondAppNumber.AppTwo) { result = appTwo.start(); } else if (number == SecondAppNumber.AppThree) { result = app3.start(); } else if (number == SecondAppNumber.AppFour) { result = app4.start(); } else if (number == SecondAppNumber.AppFive) { result = app5.start(); } return result; } public boolean ExportAppLog() { boolean result = false; if (number == SecondAppNumber.AppOne) { result = appOne.exportLog(); } else if (number == SecondAppNumber.AppTwo) { result = appTwo.exportLog(); } else if (number == SecondAppNumber.AppThree) { result = app3.exportLog(); } else if (number == SecondAppNumber.AppFour) { result = app4.exportLog(); } else if (number == SecondAppNumber.AppFive) { result = app5.exportLog(); } return result; } }
代码解析2:
这样会给系统添加新的相互依赖。并且随着时间和需求的推移,会有更多的APP需要用Monitor来监测,这个Monitor工具也会被越来越对的if...else撑爆炸,而且代码随着APP越多,越难维护。最终会导致Monitor走向灭亡(下线)。
介于这种情况,可以用Monitor这个模块来生成其它的程序,使得系统能够用在需要的APP上。OOD给我们提供了一种机制来实现这种“依赖倒置”。
代码实现3:
/** * @Author toutou * @Date 2023/6 * @Des */ public interface App { boolean start(); boolean exportLog(); } /** * @Author toutou * @Date 2023/6 * @Des */ public class AppOne implements App { @Override public boolean start(){ System.out.println("1号APP开始启动"); return true; } @Override public boolean exportLog(){ System.out.println("1号APP输出日志"); return true; } } /** * @Author toutou * @Date 2023/6 * @Des */ public class AppTwo implements App { @Override public boolean start(){ System.out.println("2号APP开始启动"); return true; } @Override public boolean exportLog(){ System.out.println("2号APP输出日志"); return true; } } /** * @Author toutou * @Date 2023/6 * @Des */ public class Monitor { private App app; public Monitor(App app) { this.app = app; } public boolean StartApp() { return app.start(); } public boolean ExportAppLog() { return app.exportLog(); } }
代码解析3:
现在Monitor依赖于IApp这个接口,而与具体实现的APP类没有关系,所以无论再怎么添加APP都不会影响到Monitor本身,只需要去添加一个实现IApp接口的APP类就可以了。
1.概念:
2.含义:
3.模拟场景:
4.代码演示:
/** * @Author toutou * @Date 2023/6 * @Des */ public interface Review { void reviewWorkFlow(); void rejectWorkFlow(); } /** * @Author toutou * @Date 2023/6 * @Des */ public class ReviewService implements Review { @Override public void reviewWorkFlow() { System.out.println("开始审核工作流"); } @Override public void rejectWorkFlow() { System.out.println("已经驳回工作流"); } } /** * @Author toutou * @Date 2023/6 * @Des */ public interface Submit { void submitWorkFlow(); void cancelWorkFlow(); } /** * @Author toutou * @Date 2023/6 * @Des */ public class SubmitService implements Submit { @Override public void submitWorkFlow(){ System.out.println("开始提交工作流"); } @Override public void cancelWorkFlow(){ System.out.println("已经撤销工作流"); } }
5.代码解析:
其实接口隔离原则很好理解,在上面的例子里可以看出来,如果把OA的外部和内部都定义一个接口的话,那这个接口会很大,而且实现接口的类也会变得臃肿。
1.概念:
2.合成/聚合解析:
-
聚合概念:
聚合用来表示“拥有”关系或者整体与部分的关系。代表部分的对象有可能会被多个代表整体的对象所共享,而且不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏,部分的生命周期可以超越整体。例如,Iphone5和IOS,当Iphone5删除后,IOS还能存在,IOS可以被Iphone6引用。
聚合关系UML类图:
代码演示:
/** * @Author toutou * @Date 2023/6 * @Des */ public class IOS { } /** * @Author toutou * @Date 2023/6 * @Des */ public class Iphone { private IOS ios; public Iphone(IOS ios){ this.ios = ios; } }
-
合成概念:
合成用来表示一种强得多的“拥有”关系。在一个合成关系里,部分和整体的生命周期是一样的。一个合成的新对象完全拥有对其组成部分的支配权,包括它们的创建和湮灭等。使用程序语言的术语来说,合成而成的新对象对组成部分的内存分配、内存释放有绝对的责任。一个合成关系中的成分对象是不能与另一个合成关系共享的。一个成分对象在同一个时间内只能属于一个合成关系。如果一个合成关系湮灭了,那么所有的成分对象要么自己湮灭所有的成分对象(这种情况较为普遍)要么就得将这一责任交给别人(较为罕见)。例如:水和鱼的关系,当水没了,鱼也不可能独立存在。
合成关系UML类图:
代码演示:
/** * @Author toutou * @Date 2023/6 * @Des */ public class Fish { public Fish createFish() { System.out.println("一条小鱼儿"); return new Fish(); } } /** * @Author toutou * @Date 2023/6 * @Des */ public class Water { private Fish fish; public Water() { fish = new Fish(); } public void createWater() { // 当创建了一个水的地方,那这个地方也得放点鱼进去 fish.createFish(); } }
3.模拟场景:
比如说我们先摇到号(这个比较困难)了,需要为自己买一辆车,如果4S店里的车默认的配置都是一样的。那么我们只要买车就会有这些配置,这时使用了继承关系:
不可能所有汽车的配置都是一样的,所以就有SUV和小轿车两种(只列举两种比较热门的车型),并且使用机动车对它们进行聚合使用。这时采用了合成/聚合的原则:
1.概念:
2.模拟场景:
场景:公司财务总监发出指令,让财务部门的人去统计公司已发公司的人数。
一个常态的编程:(肯定是不符LoD的反例)
UML类图:
代码演示:
/** * @Author toutou * @Date 2023/6 * @Des */ public class Employee { } /** * @Author toutou * @Date 2023/6 * @Des */ public class Finance { public void settlementSalary(List<Employee> employeeList) { System.out.println(String.format("已结算工资人数:{0}", employeeList.size())); } } /** * @Author toutou * @Date 2023/6 * @Des */ public class CFO { public void directive(Finance finance) { List<Employee> employeeList = new ArrayList<>(); // 初始化已发工资人数 for (int i = 0; i < 500; i++) { employeeList.add(new Employee()); } // 转告财务部门开始统计已结算公司的员工 finance.settlementSalary(employeeList); } } /** * @Author toutou * @Date 2023/6 * @Des */ public class Runner { public static void main(String[] args) { CFO cfo = new CFO(); // 财务总监发出指令 cfo.directive(new Finance()); } }
根据模拟的场景:财务总监让财务部门总结已发工资的人数。 财务总监和员工是陌生关系(即总监不需要对员工执行任何操作)。根据上述UML图和代码解决办法显然可以看出,上述做法违背了LoD法则。
依据LoD法则解耦:(符合LoD的例子)
UML类图:
代码演示:
/** * @Author toutou * @Date 2023/6 * @Des */ public class Employee { } /** * @Author toutou * @Date 2023/6 * @Des */ public class Finance { private List<Employee> employeeList; /** * 传递公司已工资的人 * @param _employeeList */ public Finance(List<Employee> _employeeList) { this.employeeList = _employeeList; } /** * 统计已结算公司的员工 */ public void settlementSalary() { System.out.println(String.format("已结算工资人数:{}", employeeList.size())); } } /** * @Author toutou * @Date 2023/6 * @Des */ public class CFO { /** * 财务总监发出指令,让财务部门统计已发工资人数 * @param finance */ public void directive(Finance finance) { // 通知财务部门开始统计已结算公司的员工 finance.settlementSalary(); } } /** * @Author toutou * @Date 2023/6 * @Des */ public class Runner { public static void main(String[] args) { List<Employee> employeeList = new ArrayList<>(); // 初始化已发工资人数 for (int i = 0; i < 500; i++){ employeeList.add(new Employee()); } CFO cfo = new CFO(); // 财务总监发出指令 cfo.directive(new Finance(employeeList)); } }
根据LoD原则我们需要让财务总监和员工之间没有之间的联系。这样才是遵守了迪米特法则。
想搞懂设计模式,必须先知道设计模式遵循的六大原则,无论是哪种设计模式都会遵循一种或者多种原则。这是面向对象不变的法则。本文针对的是设计模式(面向对象)主要的六大原则展开的讲解,并尽量做到结合实例和UML类图,帮助大家理解。在后续的博文中还会跟进一些设计模式的实例。
作 者:请叫我头头哥
出 处:http://www.cnblogs.com/toutou/
关于作者:专注于基础平台的项目开发。如有问题或建议,请多多赐教!
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
特此声明:所有评论和私信都会在第一时间回复。也欢迎园子的大大们指正错误,共同进步。或者直接私信我
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是作者坚持原创和持续写作的最大动力!