原贴:http://www.manew.com/thread-22531-1-1.html
单一职责原则
例如:
class Animal
{
public void breathe(string animal)
{
Debug.Log(animal+"呼吸空气");
}
}
public class Client
{
Animal animal = new Animal();
void Start()
{
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
}
}
运行结果: 牛呼吸空气 ,羊呼吸空气,猪呼吸空气
后添加新需求-比如鱼就是呼吸水的。
修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码如下:
class Terrestrial
{
public void breathe(String animal){
Debug.Log(animal + "呼吸空气");
}
}
class Aquatic
{
public void breathe(String animal){
Debug.Log(animal + "呼吸水");
}
}
public class Client
{
public static void main(String[] args)
{
Terrestrial terrestrial = new Terrestrial();
Debug.Log(terrestrial.breathe("牛"));
Debug.Log(terrestrial.breathe("羊"));
Debug.Log(terrestrial.breathe("猪"));
Aquatic aquatic = new Aquatic();
Debug.Log( aquatic.breathe("鱼"));
}
}
运行结果: 牛呼吸空气,羊呼吸空气,猪呼吸空气,鱼呼吸水
我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。
而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多(原来方法基础上加判断),代码如下:
class Animal
{
public void breathe(String animal)
{
if("鱼".equals(animal))
{
Debug.Log((animal+"呼吸水"));
}
else
{
Debug.Log((animal+"呼吸空气"));
}
}
}
public class Client
{
public static void main(String[] args)
{
Animal animal = new Animal();
Debug.Log(animal.breathe("牛"));
Debug.Log(animal.breathe("羊"));
Debug.Log(animal.breathe("猪"));
Debug.Log(animal.breathe("鱼"));
}
}
可以看到,这种修改方式要简单的多。
但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,
则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,
也许某一天你会发现程序运行的结果变为“牛呼吸水”了。
这种修改方式(直接在原来基础上添加判断)直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。
还有一种修改方式:
class Animal
{
public void breathe(String animal)
{
Debug.Log(animal+"呼吸空气");
}
public void breathe2(String animal)
{
Debug.Log(animal+"呼吸水");
}
}
public class Client
{
public static void main(String[] args)
{
Animal animal = new Animal();
Debug.Log(animal.breathe("牛"));
Debug.Log(animal.breathe("羊"));
Debug.Log(animal.breathe("猪"));
Debug.Log(animal.breathe2("鱼"));
}
}
可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,
但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,
那么在实际编程中,采用哪一种呢?
其实这真的比较难说,需要根据实际情况来确定。
我的原则是:只有逻辑足够简单,才可以在代码级别上遵守单一职责原则;只有类中方法数量足够少,才可以在方法级别上遵守单一职责原则。
遵循单一职责原的优点有:
- 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
- 提高类的可读性,提高系统的可维护性;
- 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
里氏替换原则
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格
例如:
class A
{
public int func1(int a, int b)
{
return a - b;
}
}
public class Client
{
void Start()
{
A a = new A();
Debug.Log(("100-50="+a.func1(100, 50));
Debug.Log(("100-80="+a.func1(100, 80)));
}
}
运行结果:50,20
现在需要增加一个新的功能,完成两次相加,继承自A类的B类来负责。
class B:A{
public int func1(int a, int b)
{
return a+b;
}
public int func2(int a, int b)
{
return func1(a,b)+100;
}
}
public class Client
{
void Start()
{
B b = new B();
Debug.Log("100-50="+b.func1(100, 50));
Debug.Log("100-80="+b.func1(100, 80));
Debug.Log("100+20+100="+b.func2(100, 20));
}
}
运行结果:100-50=150,100-80=180,100+20+100=220
B类重写A类方法func1后,导致A类的相减方法出错。
如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。
依赖倒置原则
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。
抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。依赖倒置原则的核心思想是面向接口编程
例如:
母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下:
class Book{
public String getContent()
{
return "很久很久以前有一个阿拉伯的故事……";
}
}
class Mother{
public void narrate(Book book)
{
Debug.Log("妈妈开始讲故事");
Debug.Log(book.getContent());
}
}
public class Client
{
void Start()
{
Mother mother = new Mother();
Debug.Log(mother.narrate(new Book()));
}
}
运行结果:
妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……
运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:
class Newspaper
{
public String getContent()
{
return "林书豪38+7领导尼克斯击败湖人……";
}
}
这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。
假如以后需求换成杂志呢?换成网页呢?
还要不断地修改Mother,这显然不是好的设计。
原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。
我们引入一个抽象的接口IReader。
读物,只要是带字的都属于读物:
interface IReader
{
public String getContent();
}
Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,
他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:
class Newspaper : IReader
{
public String getContent()
{
return "林书豪17+9助尼克斯击败老鹰……";
}
}
class Book : IReader
{
public String getContent()
{
return "很久很久以前有一个阿拉伯的故事……";
}
}
class Mother
{
public void narrate(IReader reader)
{
Debug.Log("妈妈开始讲故事");
Debug.Log(reader.getContent());
}
}
public class Client
{
public static void main(String[] args)
{
Mother mother = new Mother();
Debug.Log(mother.narrate(new Book()));
Debug.Log(mother.narrate(new Newspaper()));
}
}
采用依赖倒置原则给多人并行开发带来了极大的便利,
比如上例中,原本Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。
修改后的程序则可以同时开工,互不影响,因为Mother与Book类一点关系也没有。
参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。
现在很流行的TDD开发模式就是依赖倒置原则最成功的应用。
在实际编程中,我们一般需要做到如下3点:
- 低层模块尽量都要有抽象类或接口,或者两者都有。
- 变量的声明类型尽量是抽象类或接口。使用继承时遵循里氏替换原则。
- 依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。
接口隔离原则
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
举例来说明接口隔离原则
接口隔离原则的含义是:
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。
也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。
接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性
采用接口隔离原则对接口进行约束时,要注意以下几点:
- 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不争的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
- 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
例如:
interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
class A : I{
public void method1()
{
Debug.Log("类A实现接口I的方法1");
}
public void method2()
{
Debug.Log("类A实现接口I的方法2");
}
public void method3()
{
Debug.Log("类A实现接口I的方法3");
}
//对于类A来说,method4和method5不是必需的,但是由于接口A中有这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
public void method4() {}
public void method5() {}
}
class B : I{
//对于类B来说,method1和method2不是必需的,但是由于接口中有 //这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作 //用的方法进行实现。
public void method1()
{
}
public void method2()
{
}
public void method3()
{
Debug.Log("类B实现接口I的方法3");
}
public void method4()
{
Debug.Log("类B实现接口I的方法4”);
}
public void method5()
{
Debug.Log("类B实现接口I的方法5”);
}
}
依照接口隔离原则:
interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
分为:
interface K {
public void method1();
public void method2();
}
interface P {
public void method3();
public void method4();
public void method5();
}
Class A,Class B分别实现接口K和P
接口隔离原则和单一职责原则的区别:
- 单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。
- 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;
- 接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建。
迪米特法则
通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。
每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。
耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友。
有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工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;
}
}
class SubCompanyManager
{
public List<SubEmployee> getAllEmployee()
{
List<SubEmployee> list = new List<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 List<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();
foreach (SubEmployee e in list1)
{
Debug.Log(e.getId());
}
List<Employee> list2 = this.getAllEmployee();
foreach (Employee e in list2)
{
Debug.Log(e.getId());
}
}
}
public class Client
{
void Start()
{
CompanyManager e = new CompanyManager();
Debug.Log(e.printAllEmployee(new SubCompanyManager()));
}
}
而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,
而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了。
修改为:
class SubCompanyManager
{
public List<SubEmployee> getAllEmployee()
{
List<SubEmployee> list = new List<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();
foreach (SubEmployee e in list)
{
Debug.Log(e.getId());
}
}
}
class CompanyManager
{
public List<Employee> getAllEmployee()
{
List<Employee> list = new List<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();
foreach (Employee e in list2)
{
Debug.Log(e.getId());
}
}
}
过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。
所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
开闭原则(对扩展开放,对修改关闭)
可在原来框架下扩展实现新功能,而不对原来原有的类和方法进行修改。
用抽象构建框架,用实现扩展细节。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。
而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。