设计原则就是在设计模式背后更为深层的、更具有普遍性的、共同的思想原则,是提高软件系统的可维护性和可复用性的指导原则。
可维护性
可复用性
单一职责原则
"开-闭"原则
里氏代换原则
依赖倒转原则
接口隔离原则
组合/聚合复用原则
迪米特法则
可维护性
一个好的系统设计:可扩展性(Extensibility)、灵活性(Flexibility)、可插入性(Pluggability)
可复用性
传统的:代码的剪贴复用、算法的复用、数据结构的复用
面相对象设计的:抽象化、继承、封装和多态
设计模式本身并不能保证一个系统的可复用性和可维护性,但是通过学习这些设计模式的思想可以有效提高设计师的设计风格、设计水平,并促进与同行之间的沟通,从而帮助设计师提高系统设计的复用性和可维护性。
0、单一职责原则(Single Responsibility Principle)
又称单一功能原则。它规定一个类应该只有一个发生变化的原因。
特点:
(1)降低类的复杂性,对类或接口的职责有清晰明确定义。
(2)提高可读性
(3)提高可维护性
(4)降低变更引起的风险,接口改变只影响相应的实现类,不影响其他类
重点:
接口一定要做到单一职责;
类的单一职责比较难以实现, 尽量做到只有一个原因引起变化;
一个方法尽可能做一件事, 能分解就分解, 分解到原子级别;
1、开闭原则(Open Close Principle)
在设计一个模块的时候,应当使这个模块再不被修改的前提下被扩展。
一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。没错!抽象化就是解决问题的关键!
对可变性的封装原则(Principle of Encapsulation of Variation)
找到一个系统的可变因素,将之封装起来。是"开-闭"原则从另外一个角度的讲述。
(1)一种可变性不应当散落再代码的很多角落里,而应当被封装到一个对象里面。同一种可变性的不同表象意味着同一个继承登记结构中的具体子类。继承应当被看做事封装变化的方法,二不应当被认为是从一般的对象生成特殊的对象的方法。
(2)一种可变性不应当与另一种可变性混合在一起。
一些设计模式的类图继承机构不会超过两层,不然就意味着将两种不同的可变性混合在了一起。
一个重构做法的讨论
“将条件转移语句改写成为多态性”是一条广为流传的代码重构做法。意思是:将一个将使用多次条件转移的商业逻辑封装到不同的具体子类中去,从而使用多态性代替条件转移语句。
何时使用这种重构做法:应当从"开-闭"原则出发来判断。如果一个条件转移语句确实封装了某种商务逻辑的可变性,那么此种可变性封装起来就符合"开-闭"原则设计思想了。否则就会对多态性造成滥用(“多态性污染”)。
2、里氏代换原则(Liskov Substitution Principle)
任何基类可以出现的地方,子类一定可以出现。
所有 引用基类的地方 必须能 透明地使用其子类的对象;
通俗点讲:只要 父类出现的地方子类就可以出现, 替换为子类也不会产生任何错误, 使用者不需要知道父类还是子类;
核心
继承复用。里氏代换原则是对"开-闭'原则的补充。
重点
返回值-->父类方法返回值类型 F, 子类方法返回值类型 S, 里氏替换原则是 S 范围必须小于 F;
重载-->父类 子类 方法参数类型或者数量不同, 如果要符合里氏替换要求的话, 子类参数必须 >= 父类参数, 即不能让子类自己定义的方法被调用;
举栗时间:
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 四边形 <br>
* 从抽象类继承,而不是从具体类继承。
* 抽象化:抽象类、java的接口
*/
public interface Quadrangle {
public long getWidth();
public long getHeight();
}
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 矩形 <br>
*/
public class Rectangle implements Quadrangle {
private long width;
private long height;
public void setWidth(long width) {
this.width = width;
}
public void setHeight(long height) {
this.height = height;
}
@Override
public long getWidth() {
return this.width;
}
@Override
public long getHeight() {
return this.height;
}
}
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 正方形 <br>
*/
public class Square implements Quadrangle {
private long side;
public long getSide() {
return side;
}
public void setSide(long side) {
this.side = side;
}
@Override
public long getWidth() {
return getSide();
}
@Override
public long getHeight() {
return getSide();
}
}
3、依赖倒转原则(Dependence Inversion Principle)
这个是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于实现。是实现"开-闭"原则的手段。
为何而“倒转”
为什么要叫做倒转呢?
传统的过程性系统的设计办法倾向于使高层次的模块依赖于低层次的模块。
而倒转:使低层次的模块依赖于高层次的模块。
(低层次的模块)抽象层次:商务逻辑和宏观的、对整个系统来说重要的战略性决定,是必然性的体现;
(高层次的模块)具体层次:一些次要的与实现有关的算法和逻辑,以及战术性的决定,带有相当大的偶然性选择。
倒转也就是要用具体层次依赖于抽象层次。(细节依赖于抽象)
复用与可维护性的“倒转”
这样子的话,我们要复用跟维护的就是抽象层次。
依赖倒转原则
三种耦合关系
在面向对象的系统里,两个类之间可以发生三种不同的耦合关系:
零耦合(Nil Coupling)关系:如果两个雷没有耦合关系,就称之为零耦合。
具体耦合(Concrete Coupling)关系:具体性耦合发生在两个具体的(可实例化的)类之间,经由一个类对另一个具体类的直接引用造成。
抽象耦合(Abstract Coupling)关系:抽象耦合关系发生在一个具体类和一个抽象类(或者Java接口)之间,使两个必须发生关系的类之间存有最大的灵活性。
抽象不应当依赖于细节;细节应当依赖于抽象。
要针对接口变成,不要针对实现变成。
变量的静态类型和真实类型
引用对象的抽象类型
对象的创建
怎么做到依赖倒转原则
以抽象方式耦合是依赖倒转原则的关键。
注意点
尽量不要覆盖方法, 如果方法在抽象类中已经实现, 子类不要覆盖;
栗子时间:
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 聚合关系 <br>
*/
public class Account {
/*聚合关系*/
private AccountType mAccountType;
/*聚合关系*/
private AccountStatus mAccountStatus;
public Account(AccountType accountType) {
}
public void deposit(float amt) {
}
}
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 高级商业逻辑 <br>
*/
abstract public class AccountStatus {
public abstract void sendCorrespondence();
}
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 高级商业逻辑 <br>
*/
abstract public class AccountType {
public abstract void deposit(float amt);
}
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 支票账号 <br>
*/
public class Checking extends AccountType{
@Override
public void deposit(float amt) {
}
}
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 储蓄账号 <br>
*/
public class Savings extends AccountType{
@Override
public void deposit(float amt) {
}
}
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 表示账号处于“开”状态 <br>
*/
public class Open extends AccountStatus {
@Override
public void sendCorrespondence() {
}
}
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 账号处于“超支”状态 <br>
*/
public class Overdrawn extends AccountStatus{
@Override
public void sendCorrespondence() {
}
}
/**
* <b>Create Date:</b> 2017/10/9<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 新型的账号 <br>
*/
public class MoneyMarket extends AccountType{
@Override
public void deposit(float amt) {
}
}
4、接口隔离原则(Interface Segregation Principle)
使用多个隔离的接口,比使用单个接口要好。提供尽可能小的单独的接口,而不要提供大的总接口。降低耦合。
角色的合理划分
每一个角色都应当由一个特定的接口代表
定制服务
同一个角色提供宽、窄不同的接口,以对象不同的客户端。
接口污染(Interrface Contamination)
过于臃肿的接口是对接口的污染
由于每一个接口都代表一个角色,实现一个接口的对象,在它的整个生命周期中都扮演这个角色,因此将角色区分清楚就是系统设计的一个重要工作。因此,一个符合逻辑的推断,不应当将几个不同的角色都交给同一个接口,而应当交给不同的接口。
需要准确而恰当地划分角色以及角色所对应的接口,是面向对象的设计的一个重要的组成部分。
5、合成/聚合复用原则(Composite/Aggregate Reuse Principle)
又叫做合成复用原则(Composite Reuse Principle)
合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新的对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。
尽量使用合成/聚合的方式,而不是使用继承关系达到复用的目的。与里氏代换原则是相辅相成的。
合成和聚合的区别
合成和聚合均是关联的特殊种类。
聚合:用来表示“拥有”关系或者整体与部分的关系。体现的是A对象可以包含B对象但B对象不是A对象的一部分。
合成:用来表示一种强得多的“拥有”关系。体现了严格的部分和整体关系,部分和整体的生命周期一样。
在一个合成关系里,部分和整体的生命周期是一样的。一个合成的新的对象完全拥有对其组成部分的 支配权,包括它们的创建和湮灭等。使用程序语言的术语来讲,组合而成的新的对象对组成部分的内存分配、内存释放有绝对的责任。
复用的基本种类
合成/聚合复用
由于合成/聚合可以将已有的对象纳入到新对象中,使之成为新的对象的一部分,因此新的对象可以调用已有对象的功能。
这样做得好处:
(1)新对象存取成分对象的唯一方法是通过成分对象的接口
(2)这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
(3)这种复用支持包装。
(4)这种复用所需的依赖较少。
(5)每一个新的类可以将焦点集中在一个任务上。
(6)这种复用可以在运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。
一般而言,如果一个角色得到了更多的责任,那么可以使用合成/聚合关系将新的责任委派到合适的对象。
当然这种复用也有缺点。其中最主要的缺点就是通过使用这种复用建造的系统会有较多的对象需要管理。
通过集成达到复用的目的
合成/聚合作为复用的手段可以应用到几乎任何环境中去。
而继承只能应用到很悠闲的一些环境中去。换言之,尽管继承是一种非常重要的复用手段,但是设计师应当先考虑使用合成/聚合,而不是继承。
继承的种类:
实现继承:面向对象语言特有的复用工具,而且是最容易被滥用的复用工具。
继承复用通过扩展一个已有对象的实现来得到新的功能,基类明显地捕获共同的属性和方法。
继承复用的优点:
(1)新的实现较为容易,因为超类的大部分功能可以通过集成关系自动进入子类。
(2)修改或扩展集成而来的实现较为容易。
继承复用的缺点:
(1)继承复用破坏包装,因为继承将超类的实现细节暴露给子类。犹豫超类的内部细节常常是对子类透明的,因此这种复用是透明的复用,又称“白箱复用”。
(2)如果超类的实现发生改变,那么子类的实现也不得不发生改变。因此,当一个基类发生改变时,这种改变会像水中投入石子引起的水波一样,将彼岸花一圈又一圈地传导到一级又一级的子类,使设计师不得不相应地改变这些子类,以适应超类的变化。
(3)从超类继承而来的实现是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。
由于以上的这些缺点,尽量使用合成/聚合而不是继承阿里大道对实现的复用,是非常重要的设计原则。
从代码重构的角度理解
区分“Has-A”与“Is-A”
“Is-A”是严格的分类学意义上的定义,意思是一个类是另一个类的“一种”。
而“Has-A”则不同,它表示某一角色具有某一项责任。
“Is-A”代表一个类是另一个类的一种;
“Has-A”代表一个类是另一个类的一个角色,而不是另一个类的一个特殊种类。这是Coad条件的第一条。
与里氏代换原则联合使用
里氏代换原则是继承复用的基石。如果在任何使用B类型的地方可以使用S类型,那么S类型才能成为B类型的子类,而B类型才能成为S类型的基类型。
换言之,只有当每一个S在任何情况下都是一种B的时候,才可以将S设计成为B的子类。如果两个类的关系是“Has-A”关系而不是“Is-A”关系,这两个类一定违反里氏代换原则。
只有两个类满足里氏代换原则,才有可能是“Is-A”关系。
栗子时间:
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 软件 <br>
*/
public abstract class PhoneSoft {
public abstract void Run();
}
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 手机品牌 <br>
*/
public abstract class PhoneBrand {
protected PhoneSoft mPhoneSoft;
public void setPhoneBrand(PhoneSoft phoneSoft) {
mPhoneSoft = phoneSoft;
}
public abstract void Run();
}
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 游戏 <br>
*/
public class PhoneGame extends PhoneSoft {
@Override
public void Run() {
Log.i("Tongson", "运行手机游戏");
}
}
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 通讯录 <br>
*/
public class PhoneAddressList extends PhoneSoft {
@Override
public void Run() {
Log.i("Tongson", "运行手机通讯录");
}
}
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 品牌A <br>
*/
public class PhoneBrandA extends PhoneBrand{
@Override
public void Run() {
mPhoneSoft.Run();
}
}
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> 品牌B <br>
*/
public class PhoneBrandB extends PhoneBrand {
@Override
public void Run() {
mPhoneSoft.Run();
}
}
/**
* <b>Create Date:</b> 2017/10/12<br>
* <b>Author:</b> mmc_Kongming_Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> <br>
*/
public class Client {
public static void main(String[] args) {
PhoneBrand phoneBrand;
phoneBrand=new PhoneBrandA();
phoneBrand.setPhoneBrand(new PhoneGame());
phoneBrand.Run();
phoneBrand.setPhoneBrand(new PhoneAddressList());
phoneBrand.Run();
phoneBrand=new PhoneBrandB();
phoneBrand.setPhoneBrand(new PhoneGame());
phoneBrand.Run();
phoneBrand.setPhoneBrand(new PhoneAddressList());
phoneBrand.Run();
}
}
6、迪米特法则(Demeter Principle)(Law of Demeter)
也叫做最少知识原则(Least Knowledge Principle),就是说,一个对象应当对其他对象有尽可能少的了解。
一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
迪米特法则的各种表述
(1)只与你直接的朋友们通信
(2)不要跟“陌生人”说话
(3)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
狭义的迪米特法则
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。
如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
狭义的迪米特法则缺点
会在系统里早出大量的小方法,散落在系统的哥哥角落。这些方法仅仅是传递间接的调用,因此与系统的商务逻辑无关。当设计师试图从一张类图看出总体的架构时,这些小的方法会造成迷惑和困扰。
遵循类之间的迪米特法则会使一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关系。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。
与依赖倒转原则互补使用
广义的迪米特法则
其实,迪米特法则所谈论的,就是对对像之间的信息流量,流向以及信息的影响的控制。
在软件系统中,一个模块设计得好不好的最主要、最重要的标志,就是该模块在多大的程度上将自己的内部数据和其他与实现有关的细节隐藏起来。一个设计得好的模块可以将它所有的实现细节隐藏起来,切蒂地将提供给外界的API和自己的实现分隔开来。这样一来,模块与模块之间就可以仅仅通过彼此的API相互通信,而不理会模块内部的工作细节。这一概念就是“信息的隐藏”或者叫做“封装”,也就是大家熟悉的软件设计的基本教义之一。
信息的隐藏非常重要的原因在于,它可以使各个子系统之间脱耦,从而允许它们独立被开发、优化、使用、阅读以及修改。这种脱耦化可以有效地加快系统的开发过程,因为可以独立地同时开发各个模块。它可以使维护过程变得容易,因为所有模块都容易读懂,特别是不比担心对其他模块的影响。
虽然信息的隐藏本省并不能带来更好的性能,但是它可以使性能的有效调整变得容易。一旦确认某模块是性能障碍,设计人员可以针对这个模块本身进行优化,而不必担心影响到其他的模块。
信息的隐藏可以促进软件的复用。由于每一个模块都不依赖于其他模块而存在,因此每一个模块都可以独立地在其他的地方使用。一个系统的的规模越大,信息的隐藏就越是重要,而信息隐藏的威力也就越明显。
迪米特法则的主要用意是控制信息的过载。在将迪米特法则运用到系统设计中时,要注意下面的几点:
(1)在类的划分上,应当创建有弱耦合的类。类之间的耦合越弱,就越有利于复用。一个处在弱耦合中的类一旦被修改,不会对有关的类造成波及。
(2)在类的结构设计上,每一个类都应当尽量降低成员的访问权限。换言之,一个类包装好各自的private状态。这样一来,想要了解其中的一个类的意义时,不需要了解很多别的类的细节。一个类不应当public自己的属性,而应当提供取值和赋值方法让外界间接访问自己的属性。
(3)在类的设计上,只要有可能,一个类应当设计成不变类。
(4)在对其他类的因伤上,一个对象对其对象的引用应当降到最低。
广义迪米特法则在类的设计上的体现
优先考虑将一个类设置成不变类
尽量降低一个类的访问权限
谨慎使用Serializable
尽量降低成员的访问权限
取代C Struct
广义迪米特法则在代码层次上的实现
限制局域变量的有效范围
Java语言允许一个变量在任何地方声明,即任何可以有语句的地方都可以声明变量。这样做的意义之深远,是很多人想不到的。
在需要一个变量的时候才声明它,可以有效地局限局域变量的有效范围。一个变量如果仅仅在块的内部使用的话,就应当将这个变量在程序块的内部使用它的地方声明,而不是放到块的外部或者块的开头声明。
这样做有两个好处:
(1)程序员可以很容易读懂程序。
(2)如果一个变量是在需要它的程序块的外部声明的,那么当这个块还没有被执行时,这个变量就已经被分配了内存;而在这个程序块已经执行完毕后,这个变量所占据的内存空间还没有被释放,这显然是不好的。所以,如果局域变量都是在马上就要使用的时候才声明,也可以避免这种情况。
栗子时间:
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> <br>
*/
public interface ISystem {
void close();
}
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> <br>
*/
public class System implements ISystem {
private void saveCurrentTask() {
//do something
}
private void closeService() {
//do something
}
private void closeScreen() {
//do something
}
private void closePower() {
//do something
}
@Override
public void close() {
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> <br>
*/
public interface IContainer {
void sendCloseCommand();
}
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> <br>
*/
public class Container implements IContainer {
private System mSystem;
@Override
public void sendCloseCommand() {
mSystem.close();
}
}
/**
* <b>Create Date:</b> 2017/10/18<br>
* <b>Author:</b> Tongson <br>
* <b>Email:</b> 289286298@qq.com<br>
* <b>Description:</b> <br>
*/
public class Person {
private IContainer c;
//......
public void clickCloseButton() {
c.sendCloseCommand();
}
}