这里有篇文章关于各个原则结合代码进行较为清晰的阐述:http://www.uml.org.cn/sjms/201211023.asp#4
个人觉得其实六大原则中,单一职责原则、开放-封闭原则其实是模块化编程时代的思想顺延,这两个原则显然是经过经验积累得到的代码可维护性的保证。而关于面向对象编程的三个重要思想则是面向接口编程(中间抽象层)、合成/聚合原则(减少不必要的强耦合)以及迪米特原则(增加中间代理对象以减少模块间的耦合)。下面进行单独介绍。
核心原则之一:依赖倒转原则
问题由来:客户端client直接依赖类A,假如client要改为依赖类B,则必须通过修改client的代码来达成。类A和类B是低层模块,负责基本的原子操作; client一般负责具体的业务逻辑,本就负担繁重,如果底层不能保持统一,任何一次底层模块变动都要惊扰client,这显然是不合适的。
解决方案:添加中间接口层I,将client修改为依赖接口I,类A和类B各自实现接口I,这样client通过接口I间接与类A或者类B发生联系,则会大大降低直接修改client的几率。
核心原则之二:迪米特原则
“高内聚,低耦合”可以说是现在模式设计中的无上心法,而类与类之间彼此要了解的越多,则两者间的耦合关系越强,这样其中一方发生变化,必然会对另一方造成不必要的冲击影响。所以类与类之间的了解在满足需求的前提下,所需知识应该尽可能地少。
迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
给出一段Java的示例代码进行分析
//总公司员工
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;
}
}
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;
}
}
class CompanyManager{
public 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){
List<SubEmployee> list1 = sub.getAllEmployee();
//作为总经理,其实不应该知道分布员工的信息的,甚至连它们被叫做SubEmployee这个名称都不应该知道
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());
}
}
所以改进的逻辑应该如下
即应该讲分布员工的信息枚举功能交给分公司经理来完成,在分公司经理的类中添加一个枚举函数。
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());
}
}
}
/*改写了权限之后,这样总经理的函数操作将会简单并且轻松很多*/
class CompanyManager{
public List<Employee> getAllEmployee() {...}
public void printAllEmployee(SubCompanyManager sub) {
sub.printEmployee();
//to-do ...
}
}
当然这里要注意的迪米特原则又可以叫做中间人代理,但是这里的代理是顺手之举,如这里的分公司经理枚举分公司的员工名单,这是顺手做的,而不能为了代理专门设置一个专门的代理类,这是代理模式,但是不应该过分使用,否则会导致存在大量的中介传递类,这种频繁创建对象用于传递信息,无疑也会导致信息传递链条过长。
核心原则之三:合成/聚合原则
在面向对象设计中,合成/聚合关系的耦合强度是明显低于继承关系的,根据”高内聚,低耦合“心法,显然不能盲目使用继承关系,这会导致UML继承树深度和规模急速扩展,故而应该尽可能选择低耦合的关系,如依赖、关联、组合、聚合、委托等,其中合成/聚合原则(CARP:Composition and Aggregation Reuse Principle)。
单一采用继承关系设计的UML图如下
采用CARP原则之后,得到的UML图如下
采用CARP原则之后,得到的UML图如下
两者对比可以明显发现CARP原则的好处。
除了上述六大原则,还有一些其他领域的原则也可以拿来借鉴,如最小规模接口原则。
最小接口原则定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口分割最小化原则。