概述
刚开始进行软件开发的时候,只要会功能开发就行了,不太要考虑性能问题,项目的可读性,可维护性,但随着时间的推移,这些东西也是你面向高级工程师的必经之路,为了提高项目的可维护性和可复用性,增加系统的可扩展性和灵活性,我们要尽量根据后面要将的六大设计原则来进行程序开发,从而提高软件的开发效率,节约开发成本和维护成本,只有将这些原则烂熟于心,在平常的开发过程中才能做到灵活应用
原件开发过程中的六大设计原则有
- 单一职责原则
- 里氏替换原则
- 依赖倒置原则
- 接口隔离原则
- 迪米特法则
- 开闭原则
下面我们就具体看下这六大原则
六大设计原则(1):单一职责原则
单一职责原则 (Single Responsibility Principle - SRP)
There should never be more than one reason for a class to change, 导致一个类变化的原因(事情)不能多于一个 ,通俗讲就是一个类只负责一件事情或一项职责
问题由来:假如一个类T负责职责P1和P2 ,假如由于p1职责发生变化修改类T时,此时可能会使原本正常的P2职责发生变化
解决方案:把原来的T类查分成T1和T2 ,T1负责P1 ,T2负责P2 ,这样当P1发生变化修改T1时,它本身对P2是隔离的,不会对P2造成任何影响
看一个例子 :一个学校的学生工作,分为生活辅导和学习辅导,假如由一个老师全部负责的话有点太重了,正确的做法是生活辅导有生活老师进行辅导,学习辅导由学习老师进行辅导

单一职责原则同样适用于方法,一个方法负责一项职责,如果一个方法负责的职责太多,那么它的颗粒度就会变粗,不利用重用,各个职责间的耦合度也会增大
备注
- 在实际的开发过程中职责拆分的颗粒度可有由具体的业务场景进行拆分,没有必要为了拆分而拆分,特别简单的功能也把颗粒度弄的特别细,只要整体符合单一职责原则就OK
- 在实际的开发过程中没有什么事一成不变的,随着业务的变化有可能会发生职责扩散,所谓职责扩散,就是随着业务变化P职责划分为P1和P2两个职责。这时候就违背的单一职责原则,但此时是不是要进行拆分(感觉都可以),但这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构
六大设计原则(2):里氏替换原则
里氏替换原则(Liskov Substitution Principle - LSP)
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it 使用基类的指针或引用的函数,必须是在不知情的情况下,能够使用派生类的对象 ,通俗讲就是父类能够替换成子类,子类不能替换成父类 ,所有引用基类的地方必须能透明地使用其子类的对象
问题由来:有一个问题q有A的p1方法完成,现在A的子类B进行了功能扩展增加的P2方法,此时又B来处理问题q时可能会出错 解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法
例子: 我们需要一个功能两个数相加有A类完成 ,后来,我们需要增加一个新的功能:完成两数相减再减1,然后再翻倍,由类B来负责
public void test(){
int n1 = 10;
int n2 = 2;
A a1 = new A();
Log.e(TAG, n1 +" - "+n2 + " = "+a1.fun1(n1,n2));
A a2 = new B();
Log.e(TAG, n1 +" - "+n2 + " = "+a2.fun1(n1,n2));
}
class A {
//两个数相减
public int fun1(int a ,int b){
return a-b;
}
}
class B extends A {
@Override
public int fun1(int a, int b) {
return super.fun1(a, b) -1;
}
//实现 (a-b-1)*2
public int fun2(int a, int b){
return fun1(a,b)*2;
}
}

此时我们会发现原本运行正常的相减功能发生了错误,这是因为B是进行功能扩展的时候无意中重写了父类的方法导致了原本运行正常的功能出现了错误,所以在子类进行功能扩展时需要遵循里氏替换原则
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能
六大设计原则(3):依赖倒置原则
依赖倒置原则 (Dependence Inversion Principle - DIP) High level modules should not depends upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions. 高层模块不应该依赖于低层模块,它们应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象
问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成,依赖倒置原则的核心思想是面向接口编程
例子:妈妈给小朋友讲书本上的故事
public void test(){
Book book = new Book("猫和老鼠的故事.......");
Mother mother = new Mother();
mother.readStory(book);
}
class Mother {
public void readStory(Book book){
Log.e(TAG,"开始讲故事了");
Log.e(TAG,book.getContent());
}
}
class Book {
private String content;
public Book(String content){
this.content = content;
}
public String getContent(){
return "读物内容:"+content;
}
}
如上所示已经完成了妈妈讲故事,但是如果哪一天妈妈想给小朋友读报纸,你会发现妈妈干不了,需要需改上面的内容,添加读报纸的功能,这是就非常不好,假如往后还要添加读手机内容的功能,岂不是要一直进行代码修改,此时就用到了依赖倒置原则(面向接口编程),我们设计也给IReader接口,让书,报纸....都继承这个接口
public void test(){
IReader book = new Book("猫和老鼠的故事.......");
IReader newPaper = new NewPaper("中美贸易战有了最近进展.......");
Mother mother = new Mother();
mother.readStory(book);
mother.readStory(newPaper);
}
class Mother {
public void readStory(IReader reader){
Log.e(TAG,"开始讲故事了");
Log.e(TAG,reader.getContent());
}
}
interface IReader{
abstract String getContent();
}
class NewPaper implements IReader{
public String content;
public NewPaper(String c){
this.content = c;
}
@Override
public String getContent() {
return "报纸内容:"+content;
}
}
class Book implements IReader{
private String content;
public Book(String content){
this.content = content;
}
@Override
public String getContent() {
return "书本内容:"+content;
}
}
如上所示妈妈就完成了读报纸的功能,且如果以后想读手机,读期刊,在不修改代码的情况下都能很便捷的扩展开发
六大设计原则(4):接口隔离原则
接口隔离原则 (Interface Segregation Principle - ISP)
The dependency of one class to another one should depend on the smallest possible interface.一个类对另一个类的依赖应该建立在最小的接口上 ,通俗的说就是一个类不应该依赖它不需要的接口

如上图所示类B只需要方法4和方法5两个功能,类D只需要方法2,方法3两个功能,但由于接口颗粒度比较多,导致类B不能不现实方法1,方法2,方法3 三个无用的功能,类D不能不现实方法1,方法4,方法5 三个无用的功能
接口隔离原则主要指:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。要为各个类建立专用的接口,而又建立一个接口供所有类使用
六大设计原则(5):迪米特法则 (最少知识原则)
迪米特法则 (Law of Demeter)
Only talk to you immediate friends,只与你最直接的朋友交流 ,一个对象应该对其他对象保持最少的了解
在开发过程中最常提到的就是高内聚,低耦合,只有耦合度变低,代码的复用率才能提高,这也是迪米特法则所要求的 迪米特法则又叫最少知道原则,也就是说对自己依赖的类知道越少越好,一个类只向外暴露必要的信息和方法,迪米特法则还有一个更简单的定义:只与直接的朋友通信 ,首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部
六大设计原则(6):开闭原则
开闭原则 (Open Closed Principle - OCP) Software entities like classes, modules and functions should be open for extension but closed for modifications.类、模块与方法,对于扩展开放对于修改封闭
开闭原则是面向对象程序设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。具体来说,其作用如下。
- 对软件测试的影响 软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
- 可以提高代码的可复用性 粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
- 可以提高软件的可维护性 遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。
思考:java中三大特性之一的多态是否违反了里氏替换原则??
里氏替换原则的定义大致是:子类可以扩展父类的功能,但不能改变父类原有的功能,就是进行不要重写父类的方法。 多态的定义是:在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。 多态是让子类去复写父类的方法从而到达同样的引用不同的行为的效果,这貌似是和里氏替换原则是相悖的 其实里氏替换原则是针对继承而言的,继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。 而多态更多的是用在父类是接口或者抽象类的情形下,子类覆盖并重新定义父类的方法。不同的子类有不同的实现,从而有不同的行为 Ps:纯属个人理解,如果哪位大神感觉我的理解有错误,欢迎指正