设计原则则是设计模式所遵循的规则,设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。
单一职责原则(SRP:Single responsibility principle)又称单一功能原则,它规定一个类应该只有一个发生变化的原因。所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。
看上图的IUserInfo接口,用户的属性和用户的行为没有分开。应该将用户的信息抽取成一个BO(Business Object,业务对象),把行为抽取成一个Biz(Business Logic,业务逻辑),按照这个思路对类图进行修改:
重新封装成两个接口,IUserBO负责用户的属性,它职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。
IUserInfo userInfo = new UserInfo();
//我要赋值了, 我就认为它是一个纯粹的BO
IUserBO userBO = (IUserBO)userInfo;
userBO.setPassword("abc");
//我要执行动作了, 我就认为是一个业务逻辑类
IUserBiz userBiz = (IUserBiz)userInfo;
userBiz.deleteUser();
以上我们把一个接口拆分成两个接口的动作,就是依赖了单一职责原则,单一职责原则的定义是:应该有且仅有一个原因引起类的变更。
在面向对象编程领域中,开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查,单元测试以及诸如此类的用以确保产品使用质量的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。
以银行业务为例:
public class BankProcess
{
public void Deposite(){} //存款
public void Withdraw(){} //取款
public void Transfer(){} //转账
}
public class BankStaff
{
private BankProcess bankpro = new BankProcess();
public void BankHandle(Client client)
{
switch (client .Type)
{
case "deposite": //存款
bankpro.Deposite();
break;
case "withdraw": //取款
bankpro.Withdraw();
break;
case "transfer": //转账
bankpro.Transfer();
break;
}
}
}
这种设计显然是存在问题的,目前设计中就只有存款,取款和转账三个功能,将来如果业务增加了,比如增加申购基金功能,理财功能等,就必须要修改BankProcess业务类。我们分析上述设计就能发现不能把业务封装在一个类里面,违反单一职责原则,而有新的需求发生,必须修改现有代码则违反了开放封闭原则。
如何才能实现耦合度和灵活性兼得呢?
那就是抽象,将业务功能抽象为接口,当业务员依赖于固定的抽象时,对修改就是封闭的,而通过继承和多态继承,从抽象体中扩展出新的实现,就是对扩展的开放。
以下是符合OCP的设计:
//首先声明一个业务处理接口
public interface IBankProcess
{
void Process();
}
public class DeposiProcess implements IBankProcess
{
public void Process() //办理存款业务
{
Console.WriteLine("Process Deposit");
}
}
public class WithDrawProcess implements IBankProcess
{
public void Process() //办理取款业务
{
Console.WriteLine("Process WithDraw");
}
}
public class TransferProcess implements IBankProcess
{
public void Process() //办理转账业务
{
Console .WriteLine ("Process Transfer");
}
}
public class BankStaff
{
private IBankProcess bankpro = null ;
public void BankHandle(Client client)
{
switch (client.Type)
{
case "Deposite": //存款
userProc =new WithDrawUser();
break;
case "WithDraw": //取款
userProc =new WithDrawUser();
break;
case "Transfer": //转账
userProc =new WithDrawUser();
break;
}
userProc.Process();
}
}
这样当业务变更时,只需要修改对应的业务实现类就可以,其他不相干的业务就不必修改。当业务增加,只需要增加业务的实现就可以了。
里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。如果对每一个类型为S的对象o1,都有类型为T的对象o2, 使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。 但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
public class A {
public void fun(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
}
public class B extends A{
@Override
public void fun(int a,int b){
System.out.println(a+"-"+b+"="+(a-b));
}
}
public class demo {
public static void main(String[] args){
System.out.println("父类的运行结果");
A a=new A();
a.fun(1,2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.println("子类替代父类后的运行结果");
B b=new B();
b.fun(1,2);
}
}
运行结果:
父类的运行结果
1+2=3
子类替代父类后的运行结果
1-2=-1
想要的结果是“1+2=3”。可以看到,方法重写后结果就不是想要的结果了,也就是这个程序中子类B不能替代父类A。这违反了里氏替换原则原则,从而给程序造成了错误。
有时候父类有多个子类,但在这些子类中有一个特例。要想满足里氏替换原则,又想满足这个子类的功能时,有的伙伴可能会修改父类的方法。但是,修改了父类的方法又会对其他的子类造成影响,产生更多的错误。这是怎么办呢?我们可以为这个特例创建一个新的父类,这个新的父类拥有原父类的部分功能,又有不同的功能。这样既满足了里氏替换原则,又满足了这个特例的需求。
public class A {
public void fun(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
}
public class B extends A{
public void newFun(){
System.out.println("这是子类的新方法...");
}
}
public class demo {
public static void main(String[] args){
System.out.print("父类的运行结果:");
A a=new A();
a.fun(1,2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.print("子类替代父类后的运行结果:");
B b=new B();
b.fun(1,2);
//子类B的新方法
b.newFun();
}
}
运行结果:
父类的运行结果:1+2=3
子类替代父类后的运行结果:1+2=3
这是子类的新方法...
迪米特法则(Law of Demeter)又叫作最少知识原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。英文简写为: LoD。迪米特法则可以简单说成:talk only to your immediate friends。 对于OOD来说,又被解释为下面几种方式:一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
举例:有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。
/*
*总公司员工
*/
class Employee{
private String id;
public void setId(String id){
this.id=id;
}
public String getId(){
return id;
}
}
/*
*分公司员工
*/
class SubEmployee{
private String id;
public void setId(String id){
this.id=id;
}
public String getId(){
return id;
}
}
//为分公司人员按照顺序分配一个ID
class SubCompanyManager{
public List<SubEmployee> getAllEmployee(){
List<SubEmployee> list =new ArrayList<SubEmployee>();
for(int i=0;i<100;i++){
SubEmployee emp = new SubEmployee();
emp.setId("分公司"+i);
list.add(emp);
}
return list;
}
}
//为总公司人员按顺序分配一个ID
class CompanyManager{
public List<Employee> getAllEmployee(){
List<Employee> list = new ArrayList<Employee>();
for(int i=0;i<30;i++){
Employee emp = new Employee();
emp.setId("总公司"+i);
list.add(list);
}
return list;
}
public void printAllEmployee(SubCompanyManager sub){
List<SubEmployee> list1 = sub.getAllEmployee();
for(SubEmployee e:list1){
System.out,println(e.getId());
}
List<Employee> list2 = this.getAllEmployee();
for(Employee e:list2){
System.out.println(e.getId());
}
}
}
/**
*这是一个测试类
*/
public class Client(){
public static void main(String[] args){
CompanyManager e = new CompanyManager();
e.printAllEmployee(new SubCompanyManager());
}
}
现在这个设计的主要问题在于CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而在SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与其他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合,按照迪米特法则,应该避免出现这样直接朋友关系的耦合。修改后的代码如下:
class SubCompanyManager{
public List<SubEmployee> getAllEmployee(){
List<SubEmployee> list = new ArrayList<SubEmployee>();
for(int i=0;i<100;i++){
SubEmployee emp = new SubEmployee();
//为分公司人员按照顺序分配一个ID
emp.setId("分公司"+i);
list.add(emp);
}
return list;
}
public void printEmployee(){
List<SubEmployee> list = this.getAllEmployee();
for(SubEmployee e:list){
System.out.println(e.getId());
}
}
}
//为总公司人员按顺序分配一个ID
class CompanyManager{
public void printEmployee(){
List<Employee> getAllEmployee(){
List<Employee> list = new ArrayList<Employee>();
for(int i=0;i<30;i++){
Employee emp = new Employee();
//为总公司人员按照顺序分配一个ID
emp.setId("总公司"+i);
list.add(emp);
}
return list;
}
public void printAllEmployee(SubCompanyManager sub){
sub.printEmployee();
List<Employee> list2 = this.getAllEmployee();
for(Employee e:list2){
System.out.println(e.getId());
}
}
}
修改后,为分公司增加了打印员工Id的方法,总公司直接调来打印,从而避免了与分公司的员工发生耦合。
迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都要有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如在上面的例子中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的,过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统的复杂度变大。所以在采用迪米特原则的时间,要反复权衡,既做到结构清晰,又要高内聚低耦合。
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。不要在一个接口里面放很多的方法,这样会显得这个类很臃肿不堪。接口应该尽量细化,一个接口对应一个功能模块,同时接口里面的方法应该尽可能的少,使接口更加轻便灵活。或许看到接口隔离原则这样的定义很多人会觉得和单一职责原则很像,但是这两个原则还是有着很鲜明的区别。接口隔离原则和单一职责原则的审视角度是不同的,单一职责原则要求类和接口职责单一,注重的是职责,是业务逻辑上的划分,而接口隔离原则要求方法要尽可能的少,是在接口设计上的考虑。
举例:现在有一个接口,作用是编写一个网站,其中有两个方法。
public interface IWriteWebsite
{
//实现UI
void WriteWebsiteUI();
//实现逻辑代码
void WriteWebsiteLogic();
}
经验丰富的程序员类,可以单独完成开发网站的任务:
//经验丰富的程序员
class ExperiencedProgrammer : IWriteWebsite
{
public void WriteWebsiteUI()
{
Console.WriteLine("实现界面");
}
public void WriteWebsiteLogic()
{
Console.WriteLine("实现后台逻辑");
}
}
但是我们会发现,有一些经验不是那么丰富的程序员只能完成一个方向的工作,那么我们如果实现原有的编写网站的接口,那部分他不会的技能就不能实现,那么这样一来就违反了单一职责原则以及里氏替换原则,所以我们可以将编写网站的接口拆分成IWriteWebsiteUI及IWriteWebsiteLogic两个接口,这样的话,经验丰富的程序员实现这两个接口,而对应方向的程序员实现各自的接口就可以了。
编写网站逻辑接口:
public interface IWriteWebsiteLogic
{
void WriteLogic();
}
```
编写网站界面接口:
```java
public interface IWriteWebsiteUI
{
void WriteUI();
}
编写逻辑的程序员类:
public class LogicProgrammer implements IWriteWebsiteLogic
{
public void WriteLogic()
{
Console.WriteLine("实现后台逻辑");
}
}
编写界面的程序员类:
class UIProgrammer implements IWriteWebsiteUI
{
public void WriteUI()
{
Console.WriteLine("实现界面");
}
}
经验丰富的程序员类:
class ExperiencedProgrammer implements IWriteWebsiteLogic, IWriteWebsiteUI
{
public void WriteLogic()
{
Console.WriteLine("实现后台逻辑");
}
public void WriteUI()
{
Console.WriteLine("实现界面");
}
}
当然上面只是一个简单的示例,其中有很多的东西都没有加入,比如抽象的程序员类,而这个类应该具有可演示的工作结果等等。这里是为了演示一下相关的臃肿接口拆开之后带来的好处。当细粒度减小之后,复用性就提高了;类也不需要实现不合适的接口而造成承担不需要承担的行为,也不存在违反LSP或者SRP。
依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
举例:开车
从上面的类图中可以看出,司机类和奔驰车类都属于细节,并没有实现或继承抽象,它们是对象级别的耦合。通过类图可以看出司机有一个drive()方法,用来开车,奔驰车有一个run()方法,用来表示车辆运行,并且奔驰车类依赖于司机类,用户模块表示高层模块,负责调用司机类和奔驰车类。
public class Driver {
//司机的主要职责就是驾驶汽车
public void drive(Benz benz){
benz.run();
}
}
public class Benz {
//汽车肯定会跑
public void run(){
System.out.println("奔驰汽车开始运行...");
}
}
//高层模块
public class Client {
public static void main(String[] args) {
Driver xiaoLi = new Driver();
Benz benz = new Benz();
//小李开奔驰车
xiaoLi.drive(benz);
}
}
这样的设计乍一看好像也没有问题,小李只管开着他的奔驰车就好。但是假如有一天他不想开奔驰了,想换一辆宝马车玩玩怎么办呢?我们当然可以新建一个宝马车类,也给它弄一个run()方法,但问题是,这辆车有是有了,但是小李却不能开啊。因为司机类里面并没有宝马车的依赖。
public class BMW {
//宝马车当然也可以开动了
public void run(){
System.out.println("宝马汽车开始运行...");
}
}
上面的设计没有使用依赖倒置原则,我们已经郁闷的发现,模块与模块之间耦合度太高,生产力太低,只要需求一变就需要大面积重构,说明这样的设计是不合理。现在我们引入依赖倒置原则,重新设计的类图如下:
//将司机模块抽象为一个接口
public interface IDriver {
//是司机就应该会驾驶汽车
public void drive(ICar car);
}
public class Driver implements IDriver{
//司机的主要职责就是驾驶汽车
public void drive(ICar car){
car.run();
}
}
//将汽车模块抽象为一个接口:可以是奔驰汽车,也可以是宝马汽车
public interface ICar {
//是汽车就应该能跑
public void run();
}
public class Benz implements ICar{
//汽车肯定会跑
public void run(){
System.out.println("奔驰汽车开始运行...");
}
}
public class BMW implements ICar{
//宝马车当然也可以开动了
public void run(){
System.out.println("宝马汽车开始运行...");
}
}
//高层模块
public class Client {
public static void main(String[] args) {
IDriver xiaoLi = new Driver();
ICar benz = new Benz();
//小李开奔驰车
xiaoLi.drive(benz);
}
}
在新增低层模块时,只修改了高层模块(业务场景类),对其他低层模块(Driver类)不需要做任何修改,可以把"变更"的风险降低到最低。在Java中,只要定义变量就必然有类型,并且可以有两种类型:表面类型和实际类型,表面类型是在定义时赋予的类型,实际类型是对象的类型。就如上面的例子中,小李的表面类型是IDriver,实际类型是Driver。
抽象是对实现的约束,是对依赖者的一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的就是保证所有的细节不脱离契约的范畴,确保约束双方按照规定好的契约(抽象)共同发展,只要抽象这条线还在,细节就脱离不了这个圈圈。
后面介绍的各种设计模式,都是基于此6大设计原则。