单一职责原则
单一职责原则SRP(Single Responsibility Principle): 一个类只干一件事。
有人不爽了,我就是要一个类干多件事怎么着,当然可以啊,如果是你自己一个人写代码,随便写,只要你能保证后面修改的时候不出bug,并且改的快,就行。比方说,一个类A,我既做了条件判断,又弹了提示,比如下面代码:
public static boolean checkAge(int age){
if(age < 18) {
ToastTool.showShort("未成年人不得观看");
return false;
}
return true;
}
简直美滋滋,任何时候只要直接调checkAge(age)就行,判断了还自带提示,突然有一天,产品说有些地方不要提示了,如果不满18岁,直接跳转到另一个页面,怎么办,这个函数调了就提示,如果去掉提示,其他需要提示的地方也没有了,说白了就是: 有的地方需要提示,有的地方不需要提示,现在要把不需要提示的地方的提示去掉,只能重写一个不带提示的方法,最简单的就是: 把原来里面的逻辑判断剥离出来,抽成一个方法,专门用来判断,原来带提示的方法调用新方法去判断,然后自己内部弹出提示。
public static boolean checkAge(int age) {
if(!checkAgeInner(age)) {
ToastTool.showShort("未成年人不得观看");
return false;
}
return true;
}
//新方法,只做逻辑判断
public static boolean checkAgeInner(int age) {
if(age < 18) return false;
return true;
}
这虽然只是一个方法的剥离,但是体现了单一职责的好处,在需求细化的时候,单一职责的作用体现的更明显,简言之就是需求的粒度与单一职责的优势成正比,我们要尽量避免大类,避免"多功能类",因为一个类的功能越多,那么使用到它的地方也越多,就会导致任何一个部分都离不开它,都要依赖它,A模块需要,B模块也需要,在将A模块拆出来的时候,不得不把这个多功能类也拷贝过来,而这个多功能类里面又有B模块的逻辑,导致A模块不干净,被污染,所以我们要避免多功能类,或者说要尽量去中心化,多功能类是耦合的罪魁祸首,一定要尽量避免。
单一职责不仅限于类,还限于方法、模块等,适用于天地万物,宇宙洪荒,这是一种思想,一定要掌握。
里氏置换原则
里氏置换原则LSP(Liskov Substitution Principle): 所有使用基类的地方都必须能透明的使用子类。
什么意思呢,就是说凡是用到父类的地方,替换成子类不会改变原有逻辑,我们知道面向对象编程的三个基本原则: 封装,继承和多态。里氏置换原则就是继承的体现,有的人说这不是废话吗,子类继承了父类,用到父类的地方替换成子类肯定没问题啊,不一定!我们来看demo:
public abstract class Car {
abstract void run();
}
public class ToyCar extends Car {
@Override
void run() {
System.out.println("玩具车不会跑");
}
}
public class Bike extends Car {
@Override
void run() {
System.out.println("自行车靠腿蹬");
}
}
我们定义了一个抽象汽车类,然后创建了两个子类,一个玩具汽车,一个自行车,然后看使用场景:
public void toBeijing(Car car) {
car.run();
}
我们直接驾车出发,这里可以替换成Bike,因为可以骑自行车出发,但是能替换成ToyCar吗,明显不能,因为ToyCar不会跑。这就明显破坏了原来的逻辑,或者我们改写Bike,run()什么都不做,也会破坏原有逻辑,那么这里明显不能再用继承关系了,可以改用组合等模式,或者扩大父类,添加接口等:
扩大父类
// 泛指车
public abstract class Car {
}
// 带发动机的车
public abstract class EngineCar {
abstract void run();
}
//玩具车
public class ToyCar extends Car{
}
//自行车
public class Bike extends EngineCar {
@Override
void run() {
System.out.println("自行车靠腿蹬");
}
}
添加接口就简单了,直接定义一个Run接口,里面定义一个run()方法,会跑的车实现Run接口,不会跑的不实现。可以看到,里氏置换原则是对java继承的校验,不适合的继承关系就不满足里氏置换,所以如果我们在纠结是否应该用继承关系的时候,可以套用套里氏置换来看效果。
依赖倒置原则
依赖倒置原则DIP(Dipendence Inversion Principle): 面向接口编程。
依赖倒置的官方定义很晦涩: 高层不应该依赖底层,两者都应该依赖抽象;抽象不应该依赖细节;细节应该依赖抽象。说白了就是: 面向接口编程,再广义一点就是: 面向抽象编程。也就是说,我们使用一个类的时候,尽量依赖接口,定义函数的时候,参数尽量传递接口,返回值也尽量返回接口,所有对外部类的依赖尽量都是接口,我们看个例子:
private void loadImage(String url, ImageView imageView) {
Picasso.get().load(url).resize(50, 50).centerCrop().into(imageView)
}
代码很简单,就是用Picasso加载图片到ImageView,相信很多同学都这么写过,假设我们的项目中到处都是这样的代码,突然有一天,老大发话了,说要把Picasso替换成Glide,我一听美滋滋,这个简单,直接在gradle中删除Picasso的依赖并添加Glide的依赖,然后找报错的地方改成Glide不就行了,改着改着感觉有点不对劲,为啥不直接写个工具类,把加载图片的逻辑封装进去呢,这样后面改的话只要改那个工具类就行了,于是就写了个工具类:
public class PicassoUtils {
//从网络加载
public static void loadByUrl(String url, IamgeView imageView){
Picasso.get().load(url).resize(50, 50).centerCrop().into(imageView)
}
//从文件加载
public static void loadByPath(String path,ImageView imageView) {
}
//....从其他地方加载
}
改完后感觉爽歪歪,直接在这个类里面替换Picasso相关Api为Glide的就行了,爽啊!改完后,老大说,能不能让我可以选择性的使用Glide还是Picasso,比如对华为手机用Picasso,对小米使用Glide?WTF! 怎么办,此时就要用接口了,定义顶层接口,代码中使用图片加载的地方都使用接口,至于怎么实现,就是下层的逻辑了,不管用Picasso还是Glide,顶层逻辑都不需要改变:
interface ImageLoader {
//从网络加载
void loadByUrl(String url, ImageView imageView);
//从文件加载
void loadByPath(String path, ImageView imageView);
//...从其他地方加载
}
//使用Picasso
public class PicassoLoader implements ImageLoader {
...实现相关逻辑
}
//使用Glide
public class GlideLoader implements ImageLoader {
...实现相关逻辑
}
//业务层使用,imageLoader是什么就看需求,随便创建,当然你可以定义一个全局管理类,里面视需求自定义全局PicassoLoader或GlideLoader
imageLoader.loadByUrl(url, imageView);
从这个demo可以看到面向接口的好处: 能够减少耦合,代码容易维护,容易拓展,因为接口是抽象的,所以不用纠结于具体的逻辑,想怎么实现都可以;又因为接口是顶层的,所以下层就多,就更容易扩展,就更灵活。
接口隔离原则
接口隔离原则ISP(Interface Segregation Principle): 接口尽量小,功能尽量单一,说白了就是接口粒度要细。
接口隔离要求接口的功能尽量单一,而单一职责也是要求一个类只干一件事,他们有什么区别?
单一职责针对的是"职责",一个职责可能有多个功能,可能由多个接口完成;而接口隔离针对的是"接口",一个接口应该只负责"一个"功能,而不是"一块"功能,举个例子,我要实现一个音乐播放器,我只定义了一个接口:
interface IMusicPlayer {
//开始
void start();
//停止
void stop();
//暂停
void pause();
//复原
void resume();
//获取歌曲时长
String getSongLength();
}
这个满足了单一职责,但是却不满足接口隔离,假如我们现在有个歌曲展示器SongDisplayer,需要展示歌曲时长,那么我们也应该有个getSongLength()函数,我们直接实现IMusicPlayer接口吗,实现这个接口就必须实现里面的start()等方法,但是这些方法肯定不是我需要的,也不是我应该有的,这就是问题,因为接口不够小,不干净,不纯粹,明显违背了接口隔离原则,我们就可以对接口进行拆分:
//音乐播放器就仅限于对播放的控制
interface IMusicPlayer {
//开始
void start();
//停止
void stop();
//暂停
void pause();
//复原
void resume();
...
}
//歌曲展示器就仅限于对歌曲信息的展示
interface ISongDisplayer {
//获取歌曲时长
String getSongLength();
//获取歌曲名字
String getSongName();
...
}
此时我们创建的播放器就可以同时实现两个接口,只展示歌曲的话就只实现ISongDisplayer即可。
总之一句话: 接口要尽量小,尽量单一,尽量干净,尽量偏科。
最少知识原则
最少知识原则LKP(Least Knowledge Principle)也叫迪米特法则(LOD): 一个对象应该对其他对象有最少的了解,说白了就是:只关联自己需要的。
废话不说,我们来看demo:
public class MainActivity {
//定义一个Button
private Button btnOK;
protected void onCreate(){
setContentView(R.layout.main);
//初始化并设置点击事件
btnOk = findViewById(R.id.btn_ok);
btnOk.setOnClickListenter(v->{
Log.d(TAG,"hello world")
})
}
}
代码完美运行,突然有一天,产品说Button不好看,要替换成别的控件,好啊,我们直接替换成TextView,然后一运行,直接崩了,为啥?因为TextView不是Button,我们还要把:
private Button btnOk;
改成
private TextView btnOk;
如果代码中只有这一个还好,如果很多呢,脾气不好的立刻就炸了;如果再后来又要改成一个ImageView呢?此时我们不仅想到:其实我们并不关心这个玩意儿是什么,只知道它能设置点击事件就行,因为我们只调用了它的setOnClickListener(),而这个方法只要是个View就能设置的,也就是说:我们只关心我们使用到的,这就是最少知识原则,知道的越少越好,只关心自己需要的。于是最终我们改成了:
privagte View btnOk;
这样以来,后面随便你怎么改xml文件,都无所谓了,岂不美哉!此时有人说了,我要是用setText()函数怎么办,那就声明为TextView啊,因为setText()的最直接定义者就是TextView,也就是与它直接相关的,Button虽然也有setText()函数,但是是继承自TextView的,不是直接相关的了,而且Button还有我们不需要的其他属性,我们并不关心。
所以我们应该只关心自己直接关联的,不关心那些不需要的,这样,那些发生在我们不关心的区域内的事,不会引起我们的任何改变,从而大大提升了代码的鲁棒性。
开放闭合原则
开放闭合原则OCP(Open Close Principle): 一个类应该对扩展开放,对修改关闭。
开闭原则是最基础的原则,也是最完美的原则,很难百分百实现,他要求我们的任何修改都是不改变老代码,只增加新代码,这样不会引起老逻辑的改变,使得代码更加安全。
举个例子,我们来写个计算器,只需要支持短整型的加法和减法就行,很简单,我们直接这么写:
public class Calculator {
public static int calculate(int left, int right, String option) {
//加法
if("+".equals(option)) return left + right;
//减法
if("-".equals(option)) return left - right;
new IllegalArgumentException("不支持的运算符");
}
}
这样写怎么样,当然没问题,但是如果后期要支持其他的各种各样的运算,都需要在这里面添加if分支,缺点很多:
- 1 可读性差,那么多if语句,每次都要从上往下找感兴趣的运算符,肯定不行。
- 2 效率低,if太多,比如现在有100个if,我要计算的运算在最后一个if,肯定效率低。
- 3 不容易维护,因为都在一个类一个函数中,同时只能一个人来修改,维护成本太高
我们第一步优化就是拆函数:
public class Calculator {
public static int calculate(int left, int right, String option) {
//加法
if("+".equals(option)) return plus(left, right);
//减法
if("-".equals(option)) return sub(left, right);
new IllegalArgumentException("不支持的运算符");
}
//加法
public static int plus(int left, int right){
return lefft + right;
}
//加法
public static int sub(int left, int right){
return lefft - right;
}
}
这一步优化,仅仅是提出了函数,后面如果有需要改的地方,直接添加函数,然后添加if分支,但是还是费劲,既然提出函数还不行,那么再进一步,提出个类怎么样?
//定义计算器抽象类
public abstract Calculator {
//左操作数
protected String leftOpt;
//右操作数
protected String rightOpt;
//操作符
protected String operator;
//设置左操作数
public void setLeftOpt(String leftOpt) {
this.leftOpt = leftOpt;
}
//设置右操作数
public void setRightOpt(String rightOpt) {
this.rightOpt = rightOpt;
}
//计算,提供一个模版函数,供子类实现
protected abstract int calculate();
//对外公开的获取结果的Api
public String getResult(){
//计算结果
String result = calculate();
//清空操作数
clear();
//返回结果
retrun result;
}
//清空操作数
public void clear(){
leftOpt = null;
rightOpt = null;
}
}
//加法器
public class PlusCalculator extends Calculator {
public static String OPERATOR = "+";
public PlusCalculator() {
super();
this.operator = OPERATOR;
}
//加法器就实现加法
@Override
public String calculate() {
return String.valueOf(Integer.parseInt(leftOpt) + Integer.parseInt(rightOpt));
}
}
//减法器
public class SubCalculator extends Calculator {
public static String OPERATOR = "-";
public SubCalculator() {
super();
this.operator = OPERATOR;
}
//减法器就实现减法
@Override
public String calculate() {
return String.valueOf(Integer.parseInt(leftOpt) - Integer.parseInt(rightOpt));
}
}
代码很简单,根据不同的运算符定义不同的计算器实现类,每个类负责实现自己的计算逻辑,如果将来需要支持其他运算符,直接再添加一个对应的类即可,而且支持多人同时修改,比如A来做乘法器,B来做除法器,完事后merge一下代码即可。我们看下使用:
//加法
Calculator calculator = new PlusCalculator();
calculator.setLeftOpt("10");
calculator.setRightOpt("20");
calculator.cal();
//减法
calculator = new SubCalculator();
calculator.setLeftOpt("10");
calculator.setRightOpt("20");
calculator.cal();
可以看到,这里想使用什么运算就创建对应的计算器,不用再走if语句,提升了效率。而且此代码还可以扩展,通过使用工厂模式,我们可以将对应的计算器缓存起来,避免反复创建:
//定义一个计算器工厂,这里直接依赖的是Calculator这个抽象类,典型的面向接口编程
public abstract class CalFactory {
public abstract Calculator create(String operator);
}
//工厂的实现类
public class ConcreteFactory extends CalFactory {
//这一级缓存用来存放计算器的class文件
private static final HashMap<String, Class<? extends Calculator>> map = new HashMap<>();
//这一级缓存用来存放创建出来的计算器
private static final HashMap<String, Calculator> calculatorHashMap = new HashMap<>();
//静态加载,提前存放计算器对应的类,如果将来有其他计算器,也可以在这里添加
static {
map.put(PlusCalculator.OPERATOR, PlusCalculator.class);
map.put(SubCalculator.OPERATOR, SubCalculator.class);
}
@Override
public Calculator create(String operator) {
Calculator calculator = null;
//从缓存中取
calculator = calculatorHashMap.get(operator);
if (calculator != null) return calculator;
//create
Class<? extends Calculator> aClass = map.get(operator);
try {
calculator = aClass.newInstance();
//放入缓存
calculatorHashMap.put(operator, calculator);
} catch (Exception e) {
e.printStackTrace();
}
return calculator;
}
}
看下使用了工厂模式后的使用:
CalFactory factory = new ConcreteFactory();
String operator = "+";
Calculator calculator = factory.create(operator);
calculator.setLeftOpt("10");
calculator.setRightOpt("20");
String result = calculator.getResult();
使用很简单,直接传入运算符就行,不用在逻辑层直接创建具体的计算器,而且自带缓存,效率更加提升。
本章讲了六大设计原则,这是所有设计模式的基础,根据这六大设计原则领悟到23种设计模式,再由23种设计模式归纳到这六大设计原则,闭环的走上一遍,那就真的是领会贯通,达到第十层境界了。