软件设计最大的难题就是应对需求的变化,往往我们对这些变化不知所措。我们会遇到系统修改难或者扩展难、代码过分复杂而且重复代码多、公共代码不能复用、系统不够稳定,改完经常出错等一系列问题。如何实现系统的可扩展、可复用、灵活性好、维护性好等就需要好的设计模式了。今天首先介绍设计模式需要遵循的六大原则。
第一:单一职责原则(SPR)
先来看一个场景,一个类中包含两个职责T1和T2,当由于职责T1的需求需要修改类时,很有可能会影响正在执行的职责T2。因此得出单一职责的概念,即一个类应该有且仅有一个原因导致该类的变更,换句话说就时一个类应该只负责一项职责,这个类的变更智能是由这一项职责引起的。
单一职责告诉我们,一个类不能太“累”!当一个类(包括模块和方法)承担的责任越多时,它被复用的可能性越小;而且一个类承担的职责过多就相当于把这些职责都耦合在一起了,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力,因此这种耦合是非常不利的。我们需要做的就是把职责分离,把不同的职责封装到不同的类中。
举个例子来说:
/*
* 这个接口很明显的有两个职责,即连接管理和数据传送
*/
public interface Modem {
public void dial(String pno);
public void hangup();
public void send(char c);
public void recv();
}
当连接管理或者数据传送发生变化时都会引起这个类发生变化,因此上述接口就违背了单一职责原则,应该把接口拆分如下:
/*
* 负责连接管理的接口
*/
public interface Connenction {
public void dial(String pno);
public void hangup();
}
/*
* 负责数据传送的接口
*/
public interface DataChannel {
public void send(char c);
public void recv();
}
现在来总结一下,SRP的优点是消除耦合,减小因需求的变化引起代码僵化的问题。SRP强调的就是根据职责来衡量接口或者类,但是往往我们对于职责的定义时不明确的,因此我们要因项目而异。一个合理的类中尽量做到仅由一个原因引起它的变化;但是对于接口一定要做到单一职责;在需求发生实际变化时应该使用SRP等原则来重构代码。
第二:里氏替换原则(LSP)
里氏替换原则的概念是所有引用基类的地方必须能透明的使用其子类的对象,即只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,因此在程序中尽量使用基类类型对对象进行定义,而在运行时再确定其子类类型,用子类对象替代父类类型;但是有子类出现的地方,父类未必能适应。
在使用LSP时,要注意以下:
(1)子类必须完全实现父类的方法,在程序中使用父类来进行定义对象;如果一个方法只存在子类中,则父类对象时无法使用该方法的。如果子类不能完整的实现父类的方法,则要考虑断开继承关系,采用依赖聚集组合等关系代替继承。
(2)在用LSP时,尽量把父类设计成接口或者抽象类,让子类继承父类或者实现父类的接口,并实现在父类中声明的方法,运行时子类实例代替父类实例,要进行系统扩展时,不需要修改以前子类的代码,只要再增加一个新的子类来实现即可。
(3)子类在覆盖或者实现父类的方法时输入参数可以被放大。换句话说如果你想让子类的方法运行,则必须重写父类的方法,也可以重载这个方法,但是前提是子类的参数范围要大于父类的参数范围。
(4)重写或实现父类的方法时输出结果类型要小于等于父类的输出结果。
Father.java
public class Father {
public void show(int i){
System.out.println(i);
}
public Object show1(){
return "父类";
}
public void show2(HashMap map){
System.out.println("父类");
}
}
Son.java
public class Son extends Father{
public void show(int i){
System.out.println("子类重写了父类的show");
System.out.println(i);
}
public String show1(){
return "11";
}
public void show2(Map map){
System.out.println("子类");
}
}
main1.java
/*
* 里氏替换原则LSP
* 关于重写(override)和重载(overload)
* 重写即子类重新写父类的方法,方法的名字和列表都是完全一致的。
* 重载一般是发生在一个类中的,但是子类在继承父类的时候也会发生重载,但是为了避免子类在没有复写父类的情况下就能被执行,
* 即子类重载的这个方法不能被执行,所以在发生重载时务必要保证子类的参数的范围大于父类的参数范围
* 子类重写父类方法,返回值可以不一样,但是要保证父类的返回值得类型是子类的返回值类型的父类,重载也一样
* 必须保证父类的返回值类型是子类的返回值类型父类。
*/
public class main1 {
public static void main(String[] args){
HashMap map=new HashMap<>();
Father f=new Father();
//f.show(1);
System.out.println(f.show1());
//f.show2(map);
Son s=new Son();
//s.show(1);
System.out.println(s.show1());
//s.show2(map);
}
}
介绍一个里氏替换的例子:
客户(customer)可以分为VIP客户(VIPCustomer)和普通客户(CommonCustomer)两类,系统需要提供一个发送邮件的功能,初始设计如下:
我们可以看到两个send发送的过程都是一样的,说明这个代码是重复的,而且如果再增加新的类型的客户,则要再增加一个一样的send方法。为了让系统有更好的扩展性,使用里氏替换原则进行重构。修改方案是新增加一个抽象客户类Customer,让VIP客户和普通客户作为其子类,设计如下:
里氏替换原则能够保证系统具有良好的扩展性,同时实现基于多态的抽象机制。子类必须满足基类和客户端对其行为的约定。
第三:依赖倒置原则(DIP)
依赖倒置原则指的是(1)模块之间的依赖是通过抽象发生的,实现类之间不能直接的依赖关系,实现类的依赖关系是通过接口或者抽象类产生的;(2)接口或者抽象类不依赖于实现类(3)实现类要依赖接口或者抽象类。或者更加精简的定义是依赖倒置原则是面向接口编程。
依赖倒转原则要求我们在程序代码中传递参数时或者在关联关系中,尽量引用层次高的抽象类,即用接口和抽象类进行变量类型的声明、参数类型声明、方法返回类型声明以及数据类型的转换等,而不使用具体的类来做这些事情。在引入抽象以后,系统具有很好的灵活性,在程序中尽量使用抽象层进行编程,当系统行为发生变化时,只需要对抽象层进行扩展即可,不需要修改原有系统的代码。
依赖倒置的本质就是通过抽象使各个类或模块的实现彼此独立,实现模块之间的松耦合。在使用依赖倒置原则时要注意(1)每个类尽量都有接口或抽象类,因为有了抽象才能依赖倒置。(2)变量的表面类型尽量是接口或者是抽象类。注意的是工具类一般是不需要接口或者抽象类的。(3)任何类都尽量的不能从具体类派生(4)尽量不要重写父类的方法,如果基类是一个抽象类,而且这个方法已经实现了,子类尽量的不要重写。
在实现依赖倒置原则时,我们需要对抽象层编程,而将具体类的对象通过依赖注入的方式注入到其他对象中。依赖注入指的是一个对象与其他对象发生依赖关系时,通过抽象来注入依赖对象。依赖注入有三种方式,包括构造函数注入、设置(setter)注入和接口注入,常用的就是前两种。
(1)构造注入,即通过构造函数来传入具体类的对象。如下:
public class UserRegister {
private UserDao userDao =null;//由容器通过构造函数注入的实例对象
publicUserRegister(UserDao userDao){
this.userDao =userDao;
}
}
(2)Setter设置注入,即在抽象中设置Setter方法声明依赖关系,如下:
public interface InjectUserDao {
public void setUserDao(UserDao userDao);
}
public class UserRegister implementsInjectUserDao{
private UserDao userDao = null;//该对象实例由容器注入
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
(3)接口注入,即在接口的方法中声明依赖对象,如上面的InjectUserDao就属于接口注入。
第四:接口隔离原则(ISP)
接口隔离原则指的是使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。接口要尽量的细化,同时接口中的方法尽量少。一般而言,接口中仅包含为某一类用户定制的方法即可。
第五:迪米特法则(LoD)
迪米特法则指的是一个软件实体应当尽可能少的与其他实体发生相互作用。迪米特法则可以降低系统的耦合度,使类与类之间保持松耦的耦合关系。即只和朋友交流。对于一个对象,它的朋友包括当前对象本身、当前对象的成员对象、如果当前对象的成员对象是一个集合那么集合中的元素也是朋友、当前对象所创建的对象,出现在方法体内的类不属于朋友类,类与类之间的关系是建立在类间的,而不是方法间。
第六:开闭原则
一个软件实体应当对扩展开闭,对修改关闭。即实体软件应尽量在不修改原有代码的情况下进行扩展。抽象化是开闭原则的关键。开闭原则是最基础的一个原则,对于其他五个原则来说,开闭原则属于抽象类,其他五个原则属于实现类。
如何使用开闭原则:(1)通过接口或者抽象类约束扩展,在抽象中的方法应该都是public的;参数类型引用对象应尽量的使用接口或者抽象类而不是具体的类;一旦抽象层确定后就尽量的保持稳定,不要修改(2)不应该有两个不同的变化出现在同一个接口或者抽象类中。
类必须做到低内聚高耦合。