单一职责原则
Single Responsibility Principle,简称SRP,就一个类而言,应该仅有一个引起它变化的原因。
同价位的相机和手机哪个拍照好?
我觉得说同价位都太谦虚了,低端的千元卡片机完全可以吊打比自身贵至少三五倍价钱的手机,如果是万元单反,我觉得市场上已经没有什么手机的拍照效果可以与之相抗了。
整合当然是一种很好的思想,是时代发展的主方向,但是我们在进行程序设计的时候,更应该要在类的职责分离上多思考,做到单一职责,这样的代码才容易维护与复用。
单一职责的好处
1.类的复杂性降低。
2.可读性提高。
3.可维护性提高。
4.变更引起的风险降低。
很难实践
然而,这个设计原则饱受争议,很难在项目中得到体现。
原因在于,职责边界很难划分,每个人的看法不同,并且随着项目功能的不断迭代,开发人员必须要做出相应的妥协,想要保持类的单一职责几乎不可能,否则就会冗余出非常多的类文件。
而我们设计类的时候应该做的是:接口一定要遵守单一职责原则,实现类尽量遵守单一职责原则。
里氏替换原则
Liskov Substitution Principle,里氏替换原则,简称LSP,任何基类可以出现的地方,子类一定可以出现。
LSP原则是继承复用的基石。
武器设计
我们都看过电影里动作演员打架,他们的武器有各种各样的刀、剑、暗器等等。。。
根据里氏替换原则,类图就可以这样设计:
程序示例
武器的抽象类
public abstract class AbstractWeapon { //攻击 public abstract void attack(); }
刀
public class Knife extends AbstractWeapon { @Override public void attack() { System.out.println("用刀发起普通攻击"); } }
剑
public class Sword extends AbstractWeapon { @Override public void attack() { System.out.println("用剑发起普通攻击"); } }
人物类
public class Person { private AbstractWeapon weapon;//武器 public Person(AbstractWeapon weapon) { this.weapon = weapon;//初始化给一把武器 } //发起攻击 public void attack(){ weapon.attack(); } }
调用
public static void main(String[] args) { Person a = new Person(new Knife()); a.attack(); Person b = new Person(new Sword()); b.attack(); }
输出:
刀发起普通攻击
剑发起普通攻击
示例中代码的核心在于使用父类做为参数,那么子类不管什么武器都可以传入,里氏替换原则的目的就是增强程序的健壮性,对业务的横向扩展有着很好的支持。
依赖倒置原则
Dependence Inversion Principle,依赖倒置原则,简称DIP,
a.高层次的模块不应该依赖低层次的模块,他们都应该依赖于抽象。
b.抽象不应该依赖于具体,具体应该依赖于抽象。
这话说简单点就是要针对接口编程,不要针对实现编程。
比方说电脑的鼠标、键盘都是针对接口设计的,如果针对实现来设计的话,那么鼠标就要对应到具体的哪个品牌的主板,买个鼠标得去研究是否适用于当前电脑主板的型号。
优化武器程序
既然要针对接口编程,那么我们就稍微改一下上面的程序,给Person加个抽象(我这里使用的是抽象类,也可以使用接口,都符合原则)
人物抽象
public abstract class AbstractPerson { protected String name;//每个人都有名字 protected AbstractWeapon weapon;//武器 //创建人物 public AbstractPerson(String name,AbstractWeapon weapon) { this.name = name; this.weapon = weapon; } public abstract void attack();//攻击}
人物
public class Person extends AbstractPerson{ public Person(String name, AbstractWeapon weapon) { super(name, weapon); } //发起攻击 public void attack(){ System.out.print(this.name + "上场,"); this.weapon.attack(); } }
调用
public static void main(String[] args) { AbstractPerson a = new Person("张三",new Knife()); a.attack(); AbstractPerson b = new Person("李四",new Sword()); b.attack(); }
输出:
张三上场,用刀发起普通攻击
李四上场,用剑发起普通攻击
细细看来,在main方法里依赖的就是AbstractPerson这个抽象了,在Person类里依赖的也是AbstractWeapon抽象,完全符合依赖导致原则。
1.每个类尽量都有接口或抽象类。
2.变量的表面类型尽量是接口或抽象类。
接口隔离原则
Interface Segregation Principle,接口隔离原则,简称ISP,客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
简单点说,就是要细化接口,接口的方法尽量少。如果一个接口为多个模块提供访问,那么这个接口就应该进行拆分。
接口隔离原则实际上也很难得到体现,接口的粒度越小,就越是隔离,系统越灵活,但是,系统结构越是复杂,开发难度增加,可维护性降低,所以,遵守接口隔离原则需要一个度。
迪米特法则
Law of Demeter,迪米特法则,又叫做最少知识原则(Least Knowledge Principle),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。
迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
迪米特法则的核心观念就是类与类间要解耦,
1.优先考虑将一个类设置成不变类
2.尽量降低一个类的访问权限。
3.谨慎使用Serializable(防止改变引起反序列化失败)。
4.尽量降低成员的访问权限。
开闭原则
先来看看开闭原则的定义:一个软件实体(类、模块、函数等等)应该对扩展开放,对修改关闭。
这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为,做到真正的拥抱变化。
武器展览
那我们就举例说明一下什么是开闭原则,就以武器作为原型故事吧
武器通用接口
public interface IWeapon { String getName();//每个武器都有名称 Integer getAtk();//每个武器都有攻击力 Float getCrit();//暴击率}
武器类
public class Weapon implements IWeapon { private String name;//武器名称 private Integer atk;//攻击力 private Float crit;//暴击率 public Weapon(String name, Integer atk, Float crit) { this.name = name; this.atk = atk; this.crit = crit; } @Override public String getName() { return name; } @Override public Integer getAtk() { return atk; } @Override public Float getCrit() { return crit; } }
main方法
public static void main(String[] args) { ArrayList<IWeapon> weapons = new ArrayList<>(); weapons.add(new Weapon("倚天剑",1200,0.6F)); weapons.add(new Weapon("打狗棒",850,0.7F)); weapons.add(new Weapon("血饮狂刀",900,0.56F)); weapons.add(new Weapon("绣花针",900,0.7F)); for(IWeapon weapon : weapons){ System.out.println(weapon.getName() + ",攻击力是" + weapon.getAtk() + ",暴击"+weapon.getCrit()); } }
输出:
倚天剑,攻击力是1200,暴击0.6
打狗棒,攻击力是850,暴击0.7
血饮狂刀,攻击力是900,暴击0.56
绣花针,攻击力是900,暴击0.7
东方不败的绣花针
现在,我们来加一个需求,东方不败的武器绣花针,实际暴击应该在原基础上增加0.1,因为飞刀暗器之类的兵器总是让人防不胜防。
面对这个需求,我们来研究一下解决办法
既然绣花针是特殊的武器,那么我们就在Weapon类增加一个参数,是否加暴,如果为true,那么就增加0.1的暴击,怎么传入呢?那就重载一个构造函数即可。
修改后的Weapon类
public class Weapon implements IWeapon { private String name;//武器名称 private Integer atk;//攻击力 private Float crit;//暴击率 private boolean isAddCrit = false; public Weapon(String name, Integer atk, Float crit) { this.name = name; this.atk = atk; this.crit = crit; } public Weapon(String name, Integer atk, Float crit,boolean isAddCrit) { this.name = name; this.atk = atk; this.crit = crit; this.isAddCrit = isAddCrit; } @Override public String getName() { return name; } @Override public Integer getAtk() { return atk; } @Override public Float getCrit() { return isAddCrit ? crit + 0.1F : crit; } }
修改后的main方法
public static void main(String[] args) { ArrayList<IWeapon> weapons = new ArrayList<>(); weapons.add(new Weapon("倚天剑",1200,0.6F)); weapons.add(new Weapon("打狗棒",850,0.7F)); weapons.add(new Weapon("血饮狂刀",900,0.56F)); weapons.add(new Weapon("绣花针",900,0.7F,true)); for(IWeapon weapon : weapons){ System.out.println(weapon.getName() + ",攻击力是" + weapon.getAtk() + ",暴击"+weapon.getCrit()); } }
输出:
倚天剑,攻击力是1200,暴击0.6
打狗棒,攻击力是850,暴击0.7
血饮狂刀,攻击力是900,暴击0.56
绣花针,攻击力是900,暴击0.8
看吧,绣花针的暴击已经达到了0.8,不再是0.7了。
实践
这个解决办法有没有问题呢? 当然有的。
没有的话那我不成傻逼了吗?
有的人可能会把接口增加一个addCrit方法,但你不管怎么折腾,你都是要在原来的结构里进行修改,实现类无论如何都要改,才能完成新需求。
但是这里呢,我们使用继承来做反而更简单,又不破坏原来的程序结构。
新的类图
看到了吗? 我们用一个新类,暗器类,继承武器类,重写getCrit()即可,既拥抱了变化,又没有修改原来的逻辑。
暗器类
public class HiddenWeapon extends Weapon { public HiddenWeapon(String name, Integer atk, Float crit) { super(name, atk, crit); } @Override public Float getCrit() { return super.getCrit() + 0.1F; } }
main方法
public static void main(String[] args) { ArrayList<IWeapon> weapons = new ArrayList<>(); weapons.add(new Weapon("倚天剑",1200,0.6F)); weapons.add(new Weapon("打狗棒",850,0.7F)); weapons.add(new Weapon("血饮狂刀",900,0.56F)); weapons.add(new HiddenWeapon("绣花针",900,0.7F)); for(IWeapon weapon : weapons){ System.out.println(weapon.getName() + ",攻击力是" + weapon.getAtk() + ",暴击"+weapon.getCrit()); } }
输出:
倚天剑,攻击力是1200,暴击0.6
打狗棒,攻击力是850,暴击0.7
血饮狂刀,攻击力是900,暴击0.56
绣花针,攻击力是900,暴击0.8
凡是已经上线了的代码,都是有意义的,经过重重测试才得以上线,如果在原来的代码里修改东西,很容易影响到其他逻辑而不自知,你既在写功能,又在写Bug。
因此我们在设计程序的时候,应当尽量思考一下即将出现的变化,这样在未来进行扩展的时候可以做到游刃有余,在增加功能的时候,应当遵守开闭原则,扩展,而非修改。
作者:不该相遇在秋天