架构中的设计原则
架构中的设计原则
在使用面向对象的思想进行系统设计时,前人共总结出了7条原则:单一职责原则、开闭原则、里氏替换原则、依赖注入原则、接口分离原则、迪米特原则和优先使用组合而不是继承原则。
1. 单一原则
核心思想:系统中的每一个对象都应该只有一个单独的职责,而所有的对象所关注的就是自身职责的完成。
Single Responsibility Principle
每个类应该只有一个职责,对外只能提供一种功能,而引起类变化的原因应该只有一个。在设计模式中,所有的设计模式都遵循这一原则。
通常,一个类的“职责”越多,导致其变化的因素也就越多。因为每个职责都可能是一个变化的轴线。一般,我们在设计一个类的时候,会把与该类有关的操作都组合到这个类中,这样设计的后果就有可能将多个职责“耦合”到了一块,当这个类的某个职责发生变化时,很难避免其他的部分不受影响。
解决这种问题的方法就是“分藕”,将不同的职责分别进行封装,不要将其组合在一个类中。比如使用多个接口定义业务操作,每个接口所定义的业务都是单一的。
5点注意:
- 一个合理的类,应该仅有一个引起它变化的原因,即单一原则;
- 在没有变化征兆的情况下使用
SRP
或其他原则是不明智的; - 在需求实际发生变化时就应该应用
SRP
等原则来重构代码; - 使用测试驱动开发会迫使我们在设计出劣质代码之前就分理出不合理代码;
- 如果测试不能迫使职责分离,僵化性和脆弱性的腐朽味会变得很浓烈,那就应该使用
Facade
(外观)或Proxy
(代理)模式对代码重构;
2. 里氏替换原则
核心思想:在任何父类出现的地方都可以用它的子类来替代
Liskov Subsitution Principle
同一个集成体系中的对象应该有共同的行为特征。里氏替换原则关注的是怎样良好地使用继承,也就是说不要乱用继承,它是继承复用的基石。只要父类出现的地方,子类就能出现,而且替换为子类不会产生任何错误或异常。反过来可能就出现问题了。
4层含义:
- 子类必须完全实现父类的方法;
- 子类可以用拥有自己的特性;
- 覆盖或者实现父类的方法时输入参数可以被放大;(结合重载考虑父类方法参数是HashMap而子类方法参数是Map)
- 覆盖或者实现父类的方法时输出结果可以被缩小;
父类能出现的地方子类就可以出现,而且替换为子类不会产生任务错误或者异常,使用者也无需知道是父类还是子类,但是反过来就不行了。
3. 依赖注入原则
核心思想:要依赖抽象,不要依赖于具体的实现,
Dependence Inversion Principle
(也可以翻译为依赖反转原则)
在应用程序中,所有的类如果使用或依赖于其他的类,则都应该依赖于这些类的抽象类,而不是这些类的具体实现类。抽象层次应该不依赖于具体的实现细节,这样才能保证系统的可复用性和可维护性。就要要求开发人员面向接口编程而非针对实现编程。
3点说明:
- 高层模块不应该依赖低层模块,两者都应该依赖于抽象(抽象类或接口);
- 抽象(抽象类或接口)不应该依赖于细节(具体实现类);
- 细节(具体实现类)应该依赖抽象;
本质是通过抽象(抽象类或接口)使各个类或模块的实例彼此独立,互不影响,实现模块间的松耦合。这个原则也是6个原则中最难以实现的,如果没有实现这个原则,那么意味着开闭原则(对扩展开发,对修改关闭)也无法实现。
3种实现方式:
- 通过构造函数传递依赖对象; 构造函数中需要传递的参数是抽象类或接口的方式实现;
- 通过
setter
方法传递依赖对象;我们设置的set方法中,参数为抽象类或接口,来实现传递依赖对象; - 接口声明实现依赖对象
public interface IFood{
public void eat();
}
public class Noodle implements IFood{
@Override
public void eat(){
System.out.println("吃面条~~");
}
}
public class Rice implements IFood{
@Override
public void eat(){
System.out.println("吃米饭~~");
}
}
...
public interface Man{
public void cook(IFood food);
}
public class Cooker implements Man{
@Override
public void cook(IFood food){
food.eat();
}
}
public class App{
public static void main(String [] args){
Cooker cooker = new Cooker();
IFood noodel = new Noodle();
//cooker.cook(noodel);
IFood rice = new Rice();
//cooker.cook(rice);
...
}
}
这样各个类或模块的实现彼此独立,互补影响,实现了模块间的松耦合。
4. 接口分离原则
核心思想:不应该强迫客户程序依赖他们不需要使用的方法,
Interface Segregation Principle
,一个不需要提供太多的行为,一个接口应该只提供一种对外的功能,不应该把所有的操作都封装在一个接口中。
接口分离原则要求的是在一个模块中应该只依赖它需要的接口,以保证接口小纯洁,而且要保证接口应该尽量小。
接口分离原则与单一职责原则有点类似,不过不同在于:单一职责原则要求的是类和接口职责单一,注重是职责,业务逻辑的划分。而接口分离原则要求的是接口的方法尽量少,针对单一模块尽量有用。
3点规范:
- 接口尽量小:为了保证一个接口只服务于一个子模块或者业务逻辑;
- 接口高内聚:接口高内聚是对内高度依赖,对外尽可能隔离。即一个接口内部声明的方法相互之间都与某一个子模块相关,且是这个模块必需的;
- 接口设计是有限度的,65535;
5. 迪米特原则
核心思想:一个对象应当对其他对象尽可能少地了解,降低各个对象之间的耦合,提高系统的可维护性。在模块间,应该只通过接口来通信,而不理会模块的内部工作原理,促进软件的复用。
7点注意事项:
- 在类的划分上,应该创建有弱耦合的类;
- 在类的结构设计上,每一个都应当尽量降低成员的访问权限;
- 在类的设计上,只要有可能,一个类应当设计成不可变类;
- 在对其他类的引用上,一个对象对其他对象的引用应当降到最低;
- 尽量降低类的访问权限;
- 谨慎使用序列化功能;
- 不要暴露类成员,而应该提供相应的访问器(属性);
6. 开闭原则
核心思想:一个对象对扩展开放,对修改关闭
Open for Extension,Closed for Modification
。
意思对类的改动通过增加代码进行的,而不是改动现有的代码。开发人员一旦写出了可行的代码,就不应该去改变它,而是要保证它能一直运行下去。这就要借助抽象和多态,把可能变化的内容抽象出来,从而抽象出来的部分是相对稳定的,而具体的实现层是可以改变和扩展的。
开闭原则是前5种原则的一个抽象总结,前5种是开闭原则的一些具体体现。所以如果使用开闭原则,其实有点虚,因为它没有一个固定的模式,但是最终保证的是提高程序的复用性、可维护性等要求。
写在最后:
不过这些设计原则并不是绝对的,而是根据项目的实际需求来定夺。
下一篇: 模式的设计原则