面向对象原则 - web240/web240.github.io GitHub Wiki
| 首字母 | 指代 | 概念 |
|---|---|---|
| S | 单一功能原则 | 认为对象应该仅具有一种单一功能的概念。 |
| O | 开闭原则 | 认为“软件体应该是对于扩展开放的,但是对于修改封闭的”的概念。 |
| L | 里氏替换原则 | 认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”的概念。参考契约式设计。 |
| I | 接口隔离原则 | 认为“多个特定客户端接口要好于一个宽泛用途的接口”的概念。 |
| D | 依赖反转原则 | 认为一个方法应该遵从“依赖于抽象而不是一个实例”的概念。依赖注入是该原则的一种实现方式。 |
在运用面向对象的思想进行软件设计时,需要遵循的原则一共有7个,他们是:
1. 单一职责原则(Single Responsibility Principle)
每一个类应该专注于做一件事情。
2. 里氏替换原则(Liskov Substitution Principle)
超类存在的地方,子类是可以替换的。
3. 依赖倒置原则(Dependence Inversion Principle)
实现尽量依赖抽象,不依赖具体实现。
4. 接口隔离原则(Interface Segregation Principle)
应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。
又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。
面向扩展开放,面向修改关闭。
7. 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
尽量使用合成/聚合达到复用,尽量少用继承。原则: 一个类中有另一个类的对象。
因为:
可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;提高类的可读性,提高系统的可维护性;变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
所以:
从大局上看Android中的Paint和Canvas等类都遵守单一职责原则,Paint和Canvas各司其职。
因为:
里氏替换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
所以:
使用里氏替换原则时需要注意,子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。
从大局看Java的多态就属于这个原则。
因为:
具体依赖抽象,上层依赖下层。假设B是较A低的模块,但B需要使用到A的功能,这个时候,B不应当直接使用A中的具体类;而应当由B定义一抽象接口,并由A来实现这个抽象接口,B只使用这个抽象接口;这样就达到了依赖倒置的目的,B也解除了对A的依赖,反过来是A依赖于B定义的抽象接口。通过上层模块难以避免依赖下层模块,假如B也直接依赖A的实现,那么就可能造成循环依赖。
所以:
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和可维护性。
从大局看Java的多态就属于这个原则。
因为:
提供尽可能小的单独接口,而不要提供大的总接口。暴露行为让后面的实现类知道的越少越好。譬如类ProgramMonkey通过接口CodeInterface依赖类CodeC,类ProgramMaster通过接口CodeInterface依赖类CodeAndroid,如果接口CodeInterface对于类ProgramMonkey和类CodeC来说不是最小接口,则类CodeC和类CodeAndroid必须去实现他们不需要的方法。将臃肿的接口CodeInterface拆分为独立的几个接口,类ProgramMonkey和类ProgramMaster分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
所以:
建立单一接口,不要建立庞大的接口,尽量细化接口,接口中的方法尽量少。也就是要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的约定,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
从大局来说Java的接口可以实现多继承就是接口隔离原则的基础保障。
因为:
类与类之间的关系越密切,耦合度也就越来越大,只有尽量降低类与类之间的耦合才符合设计模式;对于被依赖的类来说,无论逻辑多复杂都要尽量封装在类的内部;每个对象都会与其他对象有耦合关系,我们称出现成员变量、方法参数、方法返回值中的类为直接的耦合依赖,而出现在局部变量中的类则不是直接耦合依赖,也就是说,不是直接耦合依赖的类最好不要作为局部变量的形式出现在类的内部。
所以:
一个对象对另一个对象知道的越少越好,即一个软件实体应当尽可能少的与其他实体发生相互作用,在一个类里能少用多少其他类就少用多少,尤其是局部变量的依赖类,能省略尽量省略。同时如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一方法的话,可以通过第三者转发这个调用。
从大局来说android App开发中的多Fragment与依赖的Activity间交互通信遵守了这一法则。
因为:
开放封闭原则主要体现在对扩展开放、对修改封闭,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。软件需求总是变化的,世界上没有一个软件的是不变的,因此对软件设计人员来说,必须在不需要对原有系统进行修改的情况下,实现灵活的系统扩展。
所以:
可以通过Template Method模式和Strategy模式进行重构,实现对修改封闭,对扩展开放的设计思路。 封装变化,是实现开放封闭原则的重要手段,对于经常发生变化的状态,一般将其封装为一个抽象,拒绝滥用抽象,只将经常变化的部分进行抽象。
因为:
其实整个设计模式就是在讲如何类与类之间的组合/聚合。在一个新的对象里面通过关联关系(包括组合关系和聚合关系)使用一些已有的对象,使之成为新对象的一部分,新对象通过委派调用已有对象的方法达到复用其已有功能的目的。也就是,要尽量使用类的合成复用,尽量不要使用继承。
如果为了复用,便使用继承的方式将两个不相干的类联系在一起,违反里氏代换原则,哪是生搬硬套,忽略了继承了缺点。继承复用破坏数据封装性,将基类的实现细节全部暴露给了派生类,基类的内部细节常常对派生类是透明的,白箱复用;虽然简单,但不安全,不能在程序的运行过程中随便改变;基类的实现发生了改变,派生类的实现也不得不改变;从基类继承而来的派生类是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。
所以:
组合/聚合复用原则可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
其核心思想为:一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。
专注,是一个人优良的品质;同样的,单一也是一个类的优良设计。交杂不清的职责将使得代码看起来特别别扭牵一发而动全身,有失美感和必然导致丑陋的系统错误风险。
其核心思想是:软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。开放封闭原则主要体现在两个方面1、对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。2、对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
实现开开放封闭原则的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以修改就是封闭的;而通过面向对象的继承和多态机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。
“需求总是变化”没有不变的软件,所以就需要用封闭开放原则来封闭变化满足需求,同时还能保持软件内部的封装体系稳定,不被需求的变化影响。
其核心思想是:子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
Liskov替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了Liskov替换原则,才能保证继承复用是可靠地。实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责。
Liskov替换原则是关于继承机制的设计原则,违反了Liskov替换原则就必然导致违反开放封闭原则。
Liskov替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。
其核心思想是:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
我们知道,依赖一定会存在于类与类、模块与模块之间。当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标。
抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。
依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是一层不变的。依赖于抽象,就是对接口编程,不要对实现编程。
其核心思想是:使用多个小的专门的接口,而不要使用一个大的总接口。
具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。
接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;而某些时候,实现类型并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种灾难。在这种情况下,将胖接口分解为多个特点的定制化方法,使得客户端仅仅依赖于它们的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。
分离的手段主要有以下两种:1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。
一、SRP简介(SRP--Single-Responsibility Principle):就一个类而言,应该只专注于做一件事和仅有一个引起它变化的原因。
所谓职责,我们可以理解他为功能,就是设计的这个类功能应该只有一个,而不是两个或更多。也可以理解为引用变化的原因,当你发现有两个变化会要求我们修改这个类,那么你就要考虑撤分这个类了。因为职责是变化的一个轴线,当需求变化时,该变化会反映类的职责的变化。 “就像一个人身兼数职,而这些事情相互关联不大,,甚至有冲突,那他就无法很好的解决这些职责,应该分到不同的人身上去做才对。”
二、举例说明:违反SRP原则代码:
modem接口明显具有两个职责:连接管理和数据通讯;
Java代码 收藏代码
interface Modem{
public void dial(string pno);
public void hangup();
public void send(char c);
public void recv();
}
如果应用程序变化影响连接函数,那么就需要重构:
Java代码 收藏代码
interface DataChannel{
public void send(char c);
public void recv();
}
interface Connection{
public void dial(string pno);
public void hangup();
}
三、SRP优点: 消除耦合,减小因需求变化引起代码僵化性臭味
四、使用SRP注意点: 1、一个合理的类,应该仅有一个引起它变化的原因,即单一职责; 2、在没有变化征兆的情况下应用SRP或其他原则是不明智的; 3、在需求实际发生变化时就应该应用SRP等原则来重构代码; 4、使用测试驱动开发会迫使我们在设计出现臭味之前分离不合理代码; 5、如果测试不能迫使职责分离,僵化性和脆弱性的臭味会变得很强烈,那就应该用Facade或Proxy模式对代码重构;
一、OCP简介(OCP--Open-Closed Principle):
Software entities(classes,modules,functions,etc.) should be open for extension, but closed for modification。
软件实体应当对扩展开放,对修改关闭,即软件实体应当在不修改(在.Net当中可能通过代理模式来达到这个目的)的前提下扩展。
Open for extension:当新需求出现的时候,可以通过扩展现有模型达到目的。
Close for modification:对已有的二进制代码,如dll,jar等,则不允许做任何修改。
二、OCP举例: 1、例子一 假如我们要写一个工资税类,工资税在不同国家有不同计算规则,如果我们不坚持OCP,直接写一个类封装工资税的算税方法,而每个国家对工资税的具体实现细节是不尽相同的!如果我们允许修改,即把现在系统需要的所有工资税(中国工资税、美国工资税等)都放在一个类里实现,谁也不能保证未来系统不会被卖到日本,一旦出现新的工资税,而在软件中必须要实现这种工资税,这个时候我们能做的只有找出这个类文件,在每个方法里加上日本税的实现细节并重新编译成DLL!虽然在.NET的运行环境中,我们只要将新的DLL覆盖到原有的DLL即可,并不影响现有程序的正常运行,但每次出现新情况都要找出类文件,添加新的实现细节,这个类文件不断扩大,以后维护起来就变的越来越困难,也并不满足我们以前说的单一职责原则(SRP),因为不同国家的工资税变化都会引起对这个类的改变动机!如果我们在设计这个类的时候坚持了OCP的话,把工资税的公共方法抽象出来做成一个接口,封闭修改,在客户端(使用该接口的类对象)只依赖这个接口来实现对自己所需要的工资税,以后如果系统需要增加新的工资税,只要扩展一个具体国家的工资税实现我们先前定义的接口,就可以正常使用,而不必重新修改原有类文件!
2、例子二
下面这个例子就是既不开放也不封闭的,因为Client和Server都是具体类,如果我要Client使用不同的一个Server类那就要修改Client类中所有使用Server类的地方为新的Server类。
Java代码 收藏代码
class Client{
Server server;
void GetMessage(){
server.Message();
}
}
class Server{
void Message();
}
下面为修改后符合OCP原则的实现,我们看到Server类是从ClientInterface继承的,不过ClientInterface却不叫ServerInterface,原因是我们希望对Client来说ClientInterface是固定下来的,变化的只是Server。这实际上就变成了一种策略模式(Gof Strategy)
Java代码 收藏代码
interface ClientInterface{
public void Message();
//Other functions
}
class Server:ClientInterface{
public void Message();
}
class Client {
ClientInterface ci;
public void GetMessage(){
ci.Message();
}
public void Client(ClientInterface paramCi){
ci=paramCi;
}
}
//那么在主函数(或主控端)则
public static void Main(){
ClientInterface ci = new Server();
//在上面如果有新的Server类只要替换Server()就行了.
Client client = new Client(ci);
client.GetMessage();
}
3、例子三
使用Template Method实现OCP:
Java代码 收藏代码
public abstract class Policy{
private int[] i ={ 1, 1234, 1234, 1234, 132 };
public bool Sort(){
SortImp();
}
protected virtual bool SortImp(){
}
}
class Bubbleimp : Policy{
protected override bool SortImp(){
//冒泡排序
}
}
class Bintreeimp : Policy{
protected override bool SortImp(){
//二分法排序
}
}
//主函数中实现
static void Main(string[] args){
//如果要使用冒泡排序,只要把下面的Bintreeimp改为Bubbleimp
Policy sort = new Bintreeimp();
sort.Sort();
}
三、OCP优点: 1、降低程序各部分之间的耦合性,使程序模块互换成为可能; 2、使软件各部分便于单元测试,通过编制与接口一致的模拟类(Mock),可以很容易地实现软件各部分的单元测试; 3、利于实现软件的模块的呼唤,软件升级时可以只部署发生变化的部分,而不会影响其它部分;
四、使用OCP注意点: 1、实现OCP原则的关键是抽象; 2、两种安全的实现开闭原则的设计模式是:Strategy pattern(策略模式),Template Methord(模版方法模式); 3、依据开闭原则,我们尽量不要修改类,只扩展类,但在有些情况下会出现一些比较怪异的状况,这时可以采用几个类进行组合来完成; 4、将可能发生变化的部分封装成一个对象,如: 状态, 消息,,算法,数据结构等等 , 封装变化是实现"开闭原则"的一个重要手段,如经常发生变化的状态值,如温度,气压,颜色,积分,排名等等,可以将这些作为独立的属性,如果参数之间有关系,有必要进行抽象。对于行为,如果是基本不变的,则可以直接作为对象的方法,否则考虑抽象或者封装这些行为; 5、在许多方面,OCP是面向对象设计的核心所在。遵循这个原则可带来面向对象技术所声称的巨大好处(灵活性、可重用性以及可维护性)。然而,对于应用程序的每个部分都肆意地进行抽象并不是一个好主意。应该仅仅对程序中呈现出频繁变化的那部分作出抽象。拒绝不成熟的抽象和抽象本身一样重要;
一、LSP简介(LSP--Liskov Substitution Principle): 定义:如果对于类型S的每一个对象o1,都有一个类型T的对象o2,使对于任意用类型T定义的程序P,将o2替换为o1,P的行为保持不变,则称S为T的一个子类型。 子类型必须能够替换它的基类型。LSP又称里氏替换原则。 对于这个原则,通俗一些的理解就是,父类的方法都要在子类中实现或者重写。
二、举例说明:
对于依赖倒置原则,说的是父类不能依赖子类,它们都要依赖抽象类。这种依赖是我们实现代码扩展和运行期内绑定(多态)的基础。因为一旦类的使用者依赖某个具体的类,那么对该依赖的扩展就无从谈起;而依赖某个抽象类,则只要实现了该抽象类的子类,都可以被类的使用者使用,从而实现了系统的扩展。
但是,光有依赖倒置原则,并不一定就使我们的代码真正具有良好的扩展性和运行期内绑定。请看下面的代码:
Java代码 收藏代码
public class Animal{
private string name;
public Animal(string name){
this.name = name;
}
public void Description(){
Console.WriteLine("This is a(an) " + name);
}
}
//下面是它的子类猫类:
public class Cat : Animal{
public Cat(string name){
}
public void Mew(){
Console.WriteLine("The cat is saying like 'mew'");
}
}
//下面是它的子类狗类:
public class Dog : Animal{
public Dog(string name){
}
public void Bark(){
Console.WriteLine("The dog is saying like 'bark'");
}
}
//最后,我们来看客户端的调用:
public void DecriptionTheAnimal(Animal animal){
if (typeof(animal) is Cat){
Cat cat = (Cat)animal;
Cat.Decription();
Cat.Mew();
}
else if (typeof(animal) is Dog){
Dog dog = (Dog)animal;
Dog.Decription();
Dog.Bark();
}
}
通过上面的代码,我们可以看到虽然客户端的依赖是对抽象的依赖,但依然这个设计的扩展性不好,运行期绑定没有实现。 是什么原因呢?其实就是因为不满足里氏替换原则,子类如Cat有Mew()方法父类根本没有,Dog类有Bark()方法父类也没有,两个子类都不能替换父类。这样导致了系统的扩展性不好和没有实现运行期内绑定。 现在看来,一个系统或子系统要拥有良好的扩展性和实现运行期内绑定,有两个必要条件:第一是依赖倒置原则;第二是里氏替换原则。这两个原则缺一不可。
我们知道,在我们的大多数的模式中,我们都有一个共同的接口,然后子类和扩展类都去实现该接口。
下面是一段原始代码:
Java代码 收藏代码
if(action.Equals(“add”)){
//do add action
}
else if(action.Equals(“view”)){
//do view action
}
else if(action.Equals(“delete”)){
//do delete action
}
else if(action.Equals(“modify”)){
//do modify action
}
我们首先想到的是把这些动作分离出来,就可能写出如下的代码:
Java代码 收藏代码
public class AddAction{
public void add(){
//do add action
}
}
public class ViewAction{
public void view(){
//do view action
}
}
public class deleteAction{
public void delete(){
//do delete action
}
}
public class ModifyAction{
public void modify(){
//do modify action
}
}
我们可以看到,这样代码将各个行为独立出来,满足了单一职责原则,但这远远不够,因为它不满足依赖颠倒原则和里氏替换原则。
下面我们来看看命令模式对该问题的解决方法:
Java代码 收藏代码
public interface Action{
public void doAction();
}
//然后是各个实现:
public class AddAction : Action{
public void doAction(){
//do add action
}
}
public class ViewAction : Action{
public void doAction(){
//do view action
}
}
public class deleteAction : Action{
public void doAction(){
//do delete action
}
}
public class ModifyAction : Action{
public void doAction(){
//do modify action
}
}
//这样,客户端的调用大概如下:
public void execute(Action action){
action.doAction();
}
看,上面的客户端代码再也没有出现过typeof这样的语句,扩展性良好,也有了运行期内绑定的优点。
三、LSP优点: 1、保证系统或子系统有良好的扩展性。只有子类能够完全替换父类,才能保证系统或子系统在运行期内识别子类就可以了,因而使得系统或子系统有了良好的扩展性。 2、实现运行期内绑定,即保证了面向对象多态性的顺利进行。这节省了大量的代码重复或冗余。避免了类似instanceof这样的语句,或者getClass()这样的语句,这些语句是面向对象所忌讳的。 3、有利于实现契约式编程。契约式编程有利于系统的分析和设计,指我们在分析和设计的时候,定义好系统的接口,然后再编码的时候实现这些接口即可。在父类里定义好子类需要实现的功能,而子类只要实现这些功能即可。
四、使用LSP注意点: 1、此原则和OCP的作用有点类似,其实这些面向对象的基本原则就2条:1:面向接口编程,而不是面向实现;2:用组合而不主张用继承 2、LSP是保证OCP的重要原则 3、这些基本的原则在实现方法上也有个共同层次,就是使用中间接口层,以此来达到类对象的低偶合,也就是抽象偶合! 4、派生类的退化函数:派生类的某些函数退化(变得没有用处),Base的使用者不知道不能调用f,会导致替换违规。在派生类中存在退化函数并不总是表示违反了LSP,但是当存在这种情况时,应该引起注意。 5、从派生类抛出异常:如果在派生类的方法中添加了其基类不会抛出的异常。如果基类的使用者不期望这些异常,那么把他们添加到派生类的方法中就可以能会导致不可替换性。
一、DIP简介(DIP--Dependency Inversion Principle): 1、高层模块不应该依赖于低层模块,二者都应该依赖于抽象。 2、抽象不应该依赖于细节,细节应该依赖于抽象。
高层模块包含了一个应该程序中的重要的策略选择和业务模型,正是这些高层模块才使得其所有的应用程序区别于其他,如果高层依赖于低层,那么对低层模块的改动就会直接影响到高层模块,从而迫使它们依次做出改动。
二、举例说明: 反面例子:
缺点:耦合太紧密,Light发生变化将影响ToggleSwitch。
解决办法一: 将Light作成Abstract,然后具体类继承自Light。
优点:ToggleSwitch依赖于抽象类Light,具有更高的稳定性,而BulbLight与TubeLight继承自Light,可以根据"开放-封闭"原则进行扩展。只要Light不发生变化,BulbLight与TubeLight的变化就不会波及ToggleSwitch。 缺点:如果用ToggleSwitch控制一台电视就很困难了。总不能让TV继承自Light吧。
解决方法二:
优点:更为通用、更为稳定。
三、DIP优点: 使用传统过程化程序设计所创建的依赖关系,策略依赖于细节,这是糟糕的,因为策略受到细节改变的影响。依赖倒置原则使细节和策略都依赖于抽象,抽象的稳定性决定了系统的稳定性。
四、启发式规则: 1、任何变量都不应该持有一个指向具体类的指针或者引用 2、任何类都不应该从具体类派生(始于抽象,来自具体) 3、任何方法都不应该覆写它的任何基类中的已经实现了的方法
一、ISP简介(ISP--Interface Segregation Principle): 使用多个专门的接口比使用单一的总接口要好。 一个类对另外一个类的依赖性应当是建立在最小的接口上的。 一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。
“不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。”这个说得很明白了,再通俗点说,不要强迫客户使用它们不用的方法,如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。
二、举例说明: 参考下图的设计,在这个设计里,取款、存款、转帐都使用一个通用界面接口,也就是说,每一个类都被强迫依赖了另两个类的接口方法,那么每个类有可能因为另外两个类的方法(跟自己无关)而被影响。拿取款来说,它根本不关心“存款操作”和“转帐操作”,可是它却要受到这两个方法的变化的影响。
那么我们该如何解决这个问题呢?参考下图的设计,为每个类都单独设计专门的操作接口,使得它们只依赖于它们关系的方法,这样就不会互相影了!
三、实现方法: 1、使用委托分离接口 2、使用多重继承分离接口
以上就是5个基本的面向对象设计原则,它们就像面向对象程序设计中的金科玉律,遵守它们可以使我们的代码更加鲜活,易于复用,易于拓展,灵活优雅。不同的设计模式对应不同的需求,而设计原则则代表永恒的灵魂,需要在实践中时时刻刻地遵守。就如ARTHUR J.RIEL在那边《OOD启示录》中所说的:“你并不必严格遵守这些原则,违背它们也不会被处以宗教刑罚。但你应当把这些原则看做警铃,若违背了其中的一条,那么警铃就会响起。”
面向对象设计原则 http://www.cnblogs.com/feipeng/archive/2007/03/02/661840.html
所谓封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。 简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
所谓继承是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。继承概念的实现方式有二类:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;
所谓多态就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
是指一个类的功能要单一,不能包罗万象。如同一个人一样,分配的工作不能太多,否则一天到晚虽然忙忙碌碌的,但效率却高不起来。
一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。比如:一个网络模块,原来只服务端功能,而现在要加入客户端功能, 那么应当在不用修改服务端功能代码的前提下,就能够增加客户端功能的实现代码,这要求在设计之初,就应当将服务端和客户端分开,公共部分抽象出来。
子类应当可以替换父类并出现在父类能够出现的任何地方。比如:公司搞年度晚会,所有员工可以参加抽奖,那么不管是老员工还是新员工, 也不管是总部员工还是外派员工,都应当可以参加抽奖,否则这公司就不和谐了。
这个时候,B不应当直接使用A中的具体类: 而应当由B定义一抽象接口,并由A来实现这个抽象接口,B只使用这个抽象接口:这样就达到 了依赖倒置的目的,B也解除了对A的依赖,反过来是A依赖于B定义的抽象接口。通过上层模块难以避免依赖下层模块,假如B也直接依赖A的实现,那么就可能造成循环依赖。一个常见的问题就是编译A模块时需要直接包含到B模块的cpp文件,而编译B时同样要直接包含到A的cpp文件。
模块间要通过抽象接口隔离开,而不是通过具体的类强耦合起来
S.O.L.I.D 是面向对象设计(OOD)的头五大基本原则的首字母缩写,由俗称「鲍勃大叔」的 Robert C. Martin 提出。
这些原则,结合在一起能够方便程序员开发易于维护和扩展的软件,也让开发人员轻松避免代码异味,易于重构代码,也是敏捷或自适应软件开发的一部分。
注意:这只是一篇“欢迎来到S.O.L.I.D”的简单介绍文章,它只是揭示了S.O.L.I.D是什么。
S.O.L.I.D 代表什么:
虽然缩略词展开后看似复杂,但其实非常容易掌握。
S – 单一职责原则 O – 开放封闭原则 L – 里氏替换原则 I – 接口隔离原则 D – 依赖倒置原则 让我们来单独看看每个原则,来理解为什么 S.O.L.I.D 能帮助我们成为更优秀的开发人员。
S.R.P(简称)原则指出:
一个类应该有且只有一个去改变它的理由,这意味着一个类应该只有一项工作。
例如,假设我们有一些shape(形状),并且我们想求所有shape的面积的和。这很简单对吗?
class Circle {
public $radius;
public function __construct($radius) {
$this->radius = $radius;
}
}
class Square {
public $length;
public function __construct($length) {
$this->length = $length;
}
}
class Circle {
public $radius;
public function __construct($radius) {
$this->radius = $radius;
}
}
class Square {
public $length;
public function __construct($length) {
$this->length = $length;
}
}
首先,我们创建shape类,让构造函数设置需要的参数。接下来,我们继续通过创建AreaCalculator类,然后编写求取所提供的shape面积之和的逻辑。
class AreaCalculator {
protected $shapes;
public function __construct($shapes = array()) {
$this->shapes = $shapes;
}
public function sum() {
// logic to sum the areas
}
public function output() {
return implode('', array(
"<h1>",
"Sum of the areas of provided shapes: ",
$this->sum(),
"</h1>"
));
}
}
class AreaCalculator {
protected $shapes;
public function __construct($shapes = array()) {
$this->shapes = $shapes;
}
public function sum() {
// logic to sum the areas
}
public function output() {
return implode('', array(
"<h1>",
"Sum of the areas of provided shapes: ",
$this->sum(),
"</h1>"
));
}
}
使用AreaCalculator类,我们简单地实例化类,同时传入一个shape数组,并在页面的底部显示输出。
$shapes = array(
new Circle(2),
new Square(5),
new Square(6)
);
$areas = new AreaCalculator($shapes);
echo $areas->output();
$shapes = array(
new Circle(2),
new Square(5),
new Square(6)
);
$areas = new AreaCalculator($shapes);
echo $areas->output();
输出方法的问题在于,AreaCalculator处理了输出数据的逻辑。因此,如果用户想要以json或其他方式输出数据该怎么办?
所有的逻辑将由AreaCalculator类处理,这是违反单一职责原则(SRP)的;AreaCalculator类应该只对提供的shape进行面积求和,它不应该关心用户是需要json还是HTML。
因此,为了解决这个问题,你可以创建一个SumCalculatorOutputter类,使用这个来处理你所需要的逻辑,即对所提供的shape进行面积求和后如何显示。
SumCalculatorOutputter类按如下方式工作:
$shapes = array(
new Circle(2),
new Square(5),
new Square(6)
);
$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);
echo $output->JSON();
echo $output->HAML();
echo $output->HTML();
echo $output->JADE();
$shapes = array(
new Circle(2),
new Square(5),
new Square(6)
);
$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);
echo $output->JSON();
echo $output->HAML();
echo $output->HTML();
echo $output->JADE();
现在,不管你需要何种逻辑来输出数据给用户,皆由SumCalculatorOutputter类处理。
对象或实体应该对扩展开放,对修改封闭。
这就意味着一个类应该无需修改类本身但却容易扩展。让我们看看AreaCalculator类,尤其是它的sum方法。
public function sum() {
foreach($this->shapes as $shape) {
if(is_a($shape, 'Square')) {
$area[] = pow($shape->length, 2);
} else if(is_a($shape, 'Circle')) {
$area[] = pi() * pow($shape->radius, 2);
}
}
return array_sum($area);
}
public function sum() {
foreach($this->shapes as $shape) {
if(is_a($shape, 'Square')) {
$area[] = pow($shape->length, 2);
} else if(is_a($shape, 'Circle')) {
$area[] = pi() * pow($shape->radius, 2);
}
}
return array_sum($area);
}
如果我们希望sum方法能够对更多的shape进行面积求和,我们会添加更多的If / else块,这违背了开放封闭原则。
能让这个sum方法做的更好的一种方式是,将计算每个shape面积的逻辑从sum方法中移出,将它附加到shape类上。
class Square {
public $length;
public function __construct($length) {
$this->length = $length;
}
public function area() {
return pow($this->length, 2);
}
}
class Square {
public $length;
public function __construct($length) {
$this->length = $length;
}
public function area() {
return pow($this->length, 2);
}
}
对Circle类应该做同样的事情,area方法应该添加。现在,计算任何所提的shape的面积的和的方法应该和如下简单:
public function sum() {
foreach($this->shapes as $shape) {
$area[] = $shape->area;
}
return array_sum($area);
}
public function sum() {
foreach($this->shapes as $shape) {
$area[] = $shape->area;
}
return array_sum($area);
}
现在我们可以创建另一个shape类,并在计算和时将其传递进来,这不会破坏我们的代码。然而,现在另一个问题出现了,我们怎么知道传递到AreaCalculator上的对象确实是一个shape,或者这个shape具有一个叫做area的方法?
对接口编程是S.O.L.I.D不可或缺的一部分,一个快速的例子是我们创建一个接口,让每个shape实现它:
interface ShapeInterface {
public function area();
}
class Circle implements ShapeInterface {
public $radius;
public function __construct($radius) {
$this->radius = $radius;
}
public function area() {
return pi() * pow($this->radius, 2);
}
}
interface ShapeInterface {
public function area();
}
class Circle implements ShapeInterface {
public $radius;
public function __construct($radius) {
$this->radius = $radius;
}
public function area() {
return pi() * pow($this->radius, 2);
}
}
在我们AreaCalculator的求和中,我们可以检查所提供的shape确实是ShapeInterface的实例,否则我们抛出一个异常:
public function sum() {
foreach($this->shapes as $shape) {
if(is_a($shape, 'ShapeInterface')) {
$area[] = $shape->area();
continue;
}
throw new AreaCalculatorInvalidShapeException;
}
return array_sum($area);
}
public function sum() {
foreach($this->shapes as $shape) {
if(is_a($shape, 'ShapeInterface')) {
$area[] = $shape->area();
continue;
}
throw new AreaCalculatorInvalidShapeException;
}
return array_sum($area);
}
在对象 x 为类型 T 时 q(x) 成立,那么当 S 是 T 的子类时,对象 y 为类型 S 时 q(y) 也应成立。(即对父类的调用同样适用于子类)
这一切说明的是,每一个子类或派生类应该可以替换它们基类或父类。
还利用AreaCalculator类,我们有一个VolumeCalculator类,它扩展了AreaCalculator类:
class VolumeCalculator extends AreaCalulator {
public function __construct($shapes = array()) {
parent::__construct($shapes);
}
public function sum() {
// logic to calculate the volumes and then return and array of output
return array($summedData);
}
}
class VolumeCalculator extends AreaCalulator {
public function __construct($shapes = array()) {
parent::__construct($shapes);
}
public function sum() {
// logic to calculate the volumes and then return and array of output
return array($summedData);
}
}
In the SumCalculatorOutputter class:
在SumCalculatorOutputter类中:
class SumCalculatorOutputter {
protected $calculator;
public function __constructor(AreaCalculator $calculator) {
$this->calculator = $calculator;
}
public function JSON() {
$data = array(
'sum' => $this->calculator->sum();
);
return json_encode($data);
}
public function HTML() {
return implode('', array(
'<h1>',
'Sum of the areas of provided shapes: ',
$this->calculator->sum(),
'</h1>'
));
}
}
class SumCalculatorOutputter {
protected $calculator;
public function __constructor(AreaCalculator $calculator) {
$this->calculator = $calculator;
}
public function JSON() {
$data = array(
'sum' => $this->calculator->sum();
);
return json_encode($data);
}
public function HTML() {
return implode('', array(
'<h1>',
'Sum of the areas of provided shapes: ',
$this->calculator->sum(),
'</h1>'
));
}
}
如果我们试图这样来运行一个例子:
$areas = new AreaCalculator($shapes);
$volumes = new AreaCalculator($solidShapes);
$output = new SumCalculatorOutputter($areas);
$output2 = new SumCalculatorOutputter($volumes);
$areas = new AreaCalculator($shapes);
$volumes = new AreaCalculator($solidShapes);
$output = new SumCalculatorOutputter($areas);
$output2 = new SumCalculatorOutputter($volumes);
程序可以运行,但是当我们在$output2对象调用HTML方法,我们得到一个E_NOTICE错误,提示数组到字符串的转换。
为了解决这个问题,不要从VolumeCalculator类的sum方法返回一个数组,你应该:
public function sum() {
// logic to calculate the volumes and then return and array of output
return $summedData;
}
public function sum() {
// logic to calculate the volumes and then return and array of output
return $summedData;
}
求和的结果作为一个浮点数,双精度或整数。
不应强迫客户端实现一个它用不上的接口,或是说客户端不应该被迫依赖它们不使用的方法。
仍然以shape为例,我们知道也有立体shape,如果我们也想计算shape的体积,我们可以添加另一个合约到ShapeInterface:
interface ShapeInterface {
public function area();
public function volume();
}
interface ShapeInterface {
public function area();
public function volume();
}
任何我们创建的shape必须实现volume的方法,但是我们知道正方形是平面形状没有体积,所以这个接口将迫使正方形类实现一个它没有使用的方法。
接口隔离原则(ISP)不允许这样,你可以创建另一个名为SolidShapeInterface的接口,它有一个volume合约,对于立体形状比如立方体等等,可以实现这个接口:
interface ShapeInterface {
public function area();
}
interface SolidShapeInterface {
public function volume();
}
class Cuboid implements ShapeInterface, SolidShapeInterface {
public function area() {
// calculate the surface area of the cuboid
}
public function volume() {
// calculate the volume of the cuboid
}
}
interface ShapeInterface {
public function area();
}
interface SolidShapeInterface {
public function volume();
}
class Cuboid implements ShapeInterface, SolidShapeInterface {
public function area() {
// calculate the surface area of the cuboid
}
public function volume() {
// calculate the volume of the cuboid
}
}
这是一个更好的方法,但小心一个陷阱,当这些接口做类型提示时,不要使用ShapeInterface或SolidShapeInterface。
你可以创建另一个接口,可以是ManageShapeInterface,平面和立体shape都可用,这样你可以很容易地看到它有一个管理shape的单一API。例如:
interface ManageShapeInterface {
public function calculate();
}
class Square implements ShapeInterface, ManageShapeInterface {
public function area() { /*Do stuff here*/ }
public function calculate() {
return $this->area();
}
}
class Cuboid implements ShapeInterface, SolidShapeInterface, ManageShapeInterface {
public function area() { /*Do stuff here*/ }
public function volume() { /*Do stuff here*/ }
public function calculate() {
return $this->area() + $this->volume();
}
}
interface ManageShapeInterface {
public function calculate();
}
class Square implements ShapeInterface, ManageShapeInterface {
public function area() { /*Do stuff here*/ }
public function calculate() {
return $this->area();
}
}
class Cuboid implements ShapeInterface, SolidShapeInterface, ManageShapeInterface {
public function area() { /*Do stuff here*/ }
public function volume() { /*Do stuff here*/ }
public function calculate() {
return $this->area() + $this->volume();
}
}
现在AreaCalculator类中,我们可以轻易用calculate替代area调用,同时可以检查一个对象是ManageShapeInterface而不是ShapeInterface的实例。
最后一条,但肯定不是最无足轻重的一条:
实体必须依靠抽象而不是具体实现。它表示高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象。
这听起来可能有点绕,但它很容易理解。这一原则允许解耦,这似乎是用来解释这一原则最好的例子:
class PasswordReminder {
private $dbConnection;
public function __construct(MySQLConnection $dbConnection) {
$this->dbConnection = $dbConnection;
}
}
class PasswordReminder {
private $dbConnection;
public function __construct(MySQLConnection $dbConnection) {
$this->dbConnection = $dbConnection;
}
}
首先MySQLConnection是低层次模块,而PasswordReminder处于高层次,但根据S.O.L.I.D.中D的定义,即依赖抽象而不是具体实现,上面这段代码违反这一原则,PasswordReminder类被迫依赖于MySQLConnection类。
以后如果你改变数据库引擎,你还必须编辑PasswordReminder类,因此违反了开闭原则。
PasswordReminder类不应该关心你的应用程序使用什么数据库,为了解决这个问题我们又一次“对接口编程”,因为高层次和低层次模块应该依赖于抽象,我们可以创建一个接口:
interface DBConnectionInterface {
public function connect();
}
interface DBConnectionInterface {
public function connect();
}
接口有一个connect方法,MySQLConnection类实现该接口,在PasswordReminder类的构造函数不使用MySQLConnection类,而是使用接口替换,不用管你的应用程序使用的是什么类型的数据库,PasswordReminder类可以很容易地连接到数据库,没有任何问题,且不违反OCP。
class MySQLConnection implements DBConnectionInterface {
public function connect() {
return "Database connection";
}
}
class PasswordReminder {
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
}
}
class MySQLConnection implements DBConnectionInterface {
public function connect() {
return "Database connection";
}
}
class PasswordReminder {
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
}
}
根据上面的代码片段,你现在可以看到,高层次和低层次模块依赖于抽象。
老实说,S.O.L.I.D初看起来可能棘手,但只要通过连续使用并遵守其指导方针,它就会变成你和你的代码的一部分,可以让你的代码很容易地扩展、修改、测试和重构,不出任何问题。
面向对象设计原则是OOPS编程的核心, 但我见过的大多数Java程序员热心于像Singleton (单例) 、 Decorator(装饰器)、Observer(观察者) 等设计模式, 而没有把足够多的注意力放在学习面向对象的分析和设计上面。学习面向对象编程像“抽象”、“封装”、“多态”、“继承” 等基础知识是重要的,但同时为了创建简洁、模块化的设计,了解这些设计原则也同等重要。我经常看到不同经验水平的java程序员,他们有的不知道这些 OOPS 和SOLID设计原则,有的只是不知道一个特定的设计原则会带来怎样的益处,甚至不知道在编码中如何使用这些设计原则。
(设计原则)底线是永远追求高内聚、低耦合的编码或设计。 Apache 和 Sun的开源代码是学习Java和OOPS设计原则的良好范例。它们向我们展示了,设计原则在Java编程中是如何使用的。Java JDK 使用了一些设计原则:BorderFactory类中的工厂模式、Runtime类中的单例模式、java.io 类中的装饰器模式。顺便说一句,如果您真的对Java编码原则感兴趣,请阅读Joshua Bloch 的Effective Java,他编写过Java API。我个人最喜欢的关于面向对象设计模式的是Kathy Sierra的Head First Design Pattern(深入浅出设计模式),以及其它的关于深入浅出面向对象分析和设计。这些书对编写更好的代码有很大帮助,充分利用各种面向对象和SOLID的设计模式。 虽然学习设计模式(原则)最好的方法是现实中的例子和理解违反设计原则带来的不便,本文的宗旨是向那些没有接触过或正处于学习阶段的Java程序员 介绍面向对象设计原则。我个人认为OOPS 和SOLID设计原则需要有文章清楚的介绍它们,在此我一定尽力做到这点,但现在请您准备浏览以下设计模式(原则) .
我们第一个面向对象设计原则是:DRY ,从名称可以看出DRY(don’t repeat yourself)意思是不写重复代码,而是抽象成可复用的代码块。如果您有两处以上相同的代码块,请考虑把它们抽象成一个单独的方法;或者您多次使用了 硬编码的值,请把它们设置成公共常量。这种面向对象设计原则的优点是易于维护。重要的是不要滥用此原则,重复不是针对代码而是针对功能来说。它的意思是, 如果您使用通用代码来验证OrderID和SSN,这并不意味着它们是相同的或者他们今后将保持不变。通过把通用代码用于实现两种不同的功能,或者您把这 两种不同的功能密切地联系在一起;当您的OrderID格式改变时,您的SSN验证代码将会中断。所以要当心这种耦合,而且不要把彼此之间没有任何关系却 类似的代码组合在一起。 封装经常修改的代码 在软件领域永远不变的是“变化”,所以把您认为或怀疑将来要被修改的代码封装起来。这种面向对象设计模式的优点是:易于测试和维护恰当封装的代码。 如果您在用Java编程,那么请遵守以下原则:变量和方法的访问权限默认设置为私有,并且逐步放开它们的访问权限,例如从“private”到 “protected ”、“not public”。Java中的一些设计模式使用了封装,工厂设计模式就是一个例子,它封装了创建对象的代码而且提供了以下灵活性:后续生成新对象不影响现 有的代码。 打开/关闭设计原则
类、方法/函数应当是对扩展(新功能)开放,对修改闭合。这是另外一个优雅的SOLID 设计原则,以防止有人修改通过测试的代码。理想情况下假如您添加了新功能,那么您的代码要经过测试,这就是打开/关闭设计原则的目标。顺便说一 句,SOLID中的字母“O”指的是打开/关闭设计原则。 单一职责原则
单一职责原则是另外一个SOLID设计原则,SOLID中的字母“S”指的就是它。按照SRP,一个类修改的原因应当有且只有一个,或者一个类应当 总是实现单一功能。如果您在Java中的一个类实现了多个功能,那么这些功能之间便产生了耦合关系;如果您修改其中的一个功能,您有可能就打破了这种耦合 关系,那么就要进行另一轮测试以避免产生新的问题。 依赖注入/反转原则
不要问框架的依赖注入功能将会给你带来什么益处,依赖注入功能在spring框架里已经很好的得到了实现,这一设计原则的优雅之处在于:DI框架注 入的任何一个类都易于用模拟对象进行测试,并且更易于维护,因为创建对象的代码在框架里是集中的而且和客户端代码是隔离的。有多种方法可以实现依赖注入, 例如使用字节码工具,其中一些AOP(面向切面编程)框架如切入点表达式或者spring里使用的代理。想对这种SOLID设计原则了解更多,请看IOC 和 DI设计模式中的例子。 SOLID中的字母“D”指的就是这种设计原则。 优先使用组合而非继承
如果可以的话,要优先使用组合而非继承。你们中的一些人可能为此争论,但我发现组合比继承更有灵活性。组合允许在运行时通过设置属性修改一个类的行 为,通过使用多态即以接口的形式实现类之间的组合关系,并且为修改组合关系提供了灵活性。甚至 Effective Java也建议优先使用组合而非继承。 里氏替换原则
根据里氏替换原则,父类出现的地方可以用子类来替换,例如父类的方法或函数被子类对象替换应该没有任何问题。LSP和单一职责原则、接口隔离原则密 切相关。如果一个父类的功能比其子类还要多,那么它可能不支持这一功能,而且也违反了LSP设计原则。为了遵循 LSP SOLID设计原则,派生类或子类(相对父类比较)必须增强功能,而非减少。SOLID中的字母“L”指的就是 LSP设计原则。
接口隔离原则指,如果不需要一个接口的功能,那么就不要实现此接口。这大多在以下情况发生:一个接口包含多种功能,而实现类只需要其中一种功能。接 口设计是一种棘手的工作,因为一旦发布了接口,您就不能修改它否则会影响实现该接口的类。在Java中这种设计原则的另一个好处是:接口有一个特点,任何 类使用它之前都要实现该接口所有的方法,所以使用功能单一的接口意味着实现更少的方法。 编程以接口(而非实现对象)为中心 编程总是以接口(而非实现对象)为中心,这会使代码的结构灵活,而且任何一个新的接口实现对象都能兼容现有代码结构。所以在Java中,变量、方法 返回值、方法参数的数据类型请使用接口。这是许多Java程序员的建议, Effective Java 以及 head first design pattern 等书也这样建议。
不要期望一个类完成所有的功能,可以适当地把一些功能交给代理类实现。代理原则的典范是:Java 中的equals() 和 hashCode() 方法。为了比较两个对象的内容是否相同,我们让用于比较的类本身完成对比工作而非它们的调用方。这种设计原则的好处是:没有重复编码而且很容易修改类的行 为。
以上所有面向对象的设计原则可以帮助您写出灵活、优雅的代码:具有高内聚低耦合的代码结构。理论只是第一步,更重要的是我们要习得一种能力去发现什 么时候使用这些设计原则。去发现我们是否违反了什么设计原则和影响了代码的灵活性,但是世界上没有什么是完美的,我们解决问题时不能总去使用设计模式和设 计原则,它们大多用于有较长维护周期的大型企业项目。
本章没有详细介绍 OOP 六大原则、设计模式、反模式等内容,只是对它们做了一些简单的介绍。并不是因为它们不重要,而是由于它们太重要,因此我们必须阅读更详尽的书籍来涉入这些知识,设计模式可以参考《设计模式之禅》、《设计模式:可复用面向对象软件的基础》以及《Android源码设计模式解析与实战》,反模式的权威书籍则为《反模式:危机中软件、架构和项目的重构》一书。
在此之前,有一点需要大家知道,熟悉这些原则并不是说你写出的程序就一定灵活、清晰,只是为你优秀的代码之路铺上了一层栅栏,在这些原则的指导下,你才能避免陷入一些常见的代码泥沼,从而让你写出优秀的东西。
单一职责原则的英文名称是 Single Responsibility Principle,简称是 SPR,简单地说就是一个类只做一件事,这个设计原则备受争议却又极其重要。只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的。因为单一职责的划分界限并不是如马路上的行车道那么清晰,很多时候都是需要个人经验来界定。当然,最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。
试想一下,如果你遵守了这个原则,那么你的类就会划分的很细,每个类都有比较单一的职责,这不就是高内聚、低耦合么!当然,如何界定类的职责就需要你的个人经验了。
我们定义一个网络请求的类,来体现 SRP 的原则,来执行网络请求的接口,代码如下:
public interface HttpStack {
/**
* 执行 Http 请求,并且返回一个 Response
*/
public Response performRequest(Request<?> request);
}
从上述程序中可以看到,HttpStack 只有一个 performRequest 函数,它的职责就是执行网络请求并且返回一个 Response,它的职责很单一,这样在需要修改执行网络请求的相关代码时,只需要修改实现 HttpStack 接口的类,而不会影响其他类的代码。如果某个类的职责包含有执行网络请求、解析网络请求、进行 gzip 压缩、封装请求参数等,那么在你修改某处代码时就必须谨慎,以免修改的代码影响了其它的功能。当你修改的代码能够基本上不影响其他功能。这就一定程度上保证了代码的可维护性。注意,单一职责原则并不是一个类只能有一个函数,而是说这个类中的函数所做的工作是高度相关的,也就是高内聚。 HttpStack 抽象了执行网络请求的具体过程,接口简单清晰,也便于扩展。
优点:
类的复杂性降低,实现什么职责都有清晰明确的定义。 可读性提高,复杂性降低,那当然可读性提高了。 可维护性提高,可读性提高了,那当然更容易维护了。 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是所有引用基类、接口的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何报错或者异常,使用者可能根本就不需要知道是子类还是父类。但是,反过来就不行了,有子类出现的地方,父类未必就能使用。
还是以 HttpStack 为例, HttpStack 来表示执行网络请求这个抽象概念。在执行网络请求时,只需要定义一个 HttpStack 对象,然后执行 performRequest 即可,至于 HttpStack 的具体实现由更高层的调用者指定。这部分代码在 RequestQueue 类中,示例如下:
/**
* @param coreNums 核心线程数
* @param httpStack http 执行器
*/
protected RequestQueue(int coreNums, HttpStack httpStack) {
mDispatcherNums = coreNums;
mHttpStack = httpStack != null ? httpStack : HttpStackFactory.createHttpStack();
}
HttpStackFactory 类的 createHttpStack 函数负责根据 API 版本创建不同的 HttpStack,实现代码如下:
/**
* 根据 sdk 版本选择 HttpClient 或者 HttpURLConnection
*/
public static HttpStack createHttpStack() {
int runtimeSDKApi = Build.VERSION.SDK_INT;
if (runtimeSDKApi >= GINGERBREAD_SDK_NUM) {
return new HttpUrlConnStack();
}
return new HttpClientStack();
}
上述代码中, RequestQueue 类中依赖的是 HttpStack 接口,而通过 HttpStackFactory 的 createHttpStack 函数返回的是 HttpStack 的实现类 HttpClientStack 或 HttpUrlConnStack。这就是所谓的里氏替换原则,任何父类、父接口出现的地方子类都可以出现,这不就保证了可扩展性吗!
任何实现 HttpStack 接口的类的对象都可以传递给 RequestQueue 实现网络请求的功能,这样执行网络请求的方法就有很多种可能性,而不是只有 HttpClient 和 HttpURLConnection。例如,用户想使用 OkHttp 作为新的网络搜索执行引擎,那么创建一个实现了 HttpStack 接口的 OkHttpStack 类,然后在该类的 performRequest 函数中执行网络请求,最终将 OkHttpStack 对象注入 RequestQueue 即可。
细想一下,很多应用框架不就是这样实现的吗?框架定义一系列相关的逻辑骨架和抽象,使得用户可以将自己的实现注入到框架中,从而实现变化万千的功能。
优点:
代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性。 提高代码的重用性。 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,很多开源框架的扩展接口都是通过继承父类来完成的。 提高产品或项目的开放性。 缺点:
继承是侵入性的。只要继承,就必须拥有父类所有的属性和方法。 降低了代码的灵活性。子类必须继承父类的属性和方法,让子类自由的世界中多了些约束。 增强了耦合性。当父类的常量、变量和方法被修改时,必须要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的后果—大量的代码需要重构。
依赖倒置原则这个名字看起来有点不好理解,“依赖”还有“倒置”,这到底是什么意思?依赖倒置原则的几个关键点如下。
高层模块不应该依赖底层模块,两者都应该依赖其抽象。 抽象不应该依赖细节。 细节应该依赖抽象。 在 Java 语言中,抽象就是指接口或者抽象类,两者都是不能直接被实例化的。细节就是实现类、实现接口或者继承抽象类而产生的类,其特点就是可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。依赖倒置原则是 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接依赖的关系,其依赖关系是通过接口或者抽象类产生的。软件先驱们总是喜欢将一些理论定义得很抽象,弄得不是那么容易理解,其实就是一句话:面向接口编程,或者说是面向抽象编程,这里的抽象是指抽象类或者是接口。面向接口编程是面向对象精髓之一。
采用依赖倒置原则可以减少类之间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
在前面我们的例子中, RequestQueue 实现类依赖于 HttpStack 接口(抽象),而不依赖于 HttpClientStack 与 HttpUrlConnStack 实现类(细节),这就是依赖倒置原则的体现。如果 RequestQueue 直接依赖了 HttpClientStack ,那么 HttpUrlConnStack 就不能传递给 RequestQueue 了。除非 HttpUrlConnStack 继承自 HttpClientStack 。但这么设计显然不符合逻辑,他们两个之间是同等级的“兄弟”关系,而不是父子的关系,因此,正确的设计就是依赖于 HttpStack 抽象,HttpStack 只是负责定义规范,而 HttpClientStack 和 HttpUrlConnStack 分别实现具体的功能。这样一来也同样保证了扩展性。
优点:
可扩展性好 耦合度低
开闭原则是 Java 世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:一个软件实体类,模块和函数应该对扩展开放,对修改关闭。在软件的生命周期内,因为变化、升级和维护等原因,需要对软件原有的代码进行修改时,可能会给旧代码引入错误。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。
在软件开发过程中,永远不变的就是变化。开闭原则是使我们的软件系统拥抱变化的核心原则之一。对扩展开放,对修改关闭这样的高层次概括,即在需要对软件进行升级、变化时应该通过扩展的形式来实现,而非修改原有代码。当然这只是一种比较理想的状态,是通过扩展还是通过修改旧代码需要依据代码自身来定。
在我们封装的网络请求模块中,开闭原则体现的比较好的就是 Request 类族的设计。我们知道,在开发 C/S 应用时,服务器返回的数据多种多样,有字符串类型、xml、Json 等。而解析服务器返回的 Response 的原始数据类型则是通过 Request 类来实现的,这样就使得 Request 类对于服务器返回的数据格式有良好的扩展性,即 Request 的可变性太大。
例如,返回的数据格式是 Json,那么使用 JsonRequest 请求来获取数据,它会将结果转成 JsonObject 对象,我们看看 JsonRequest 的核心实现:
// 返回的数据格式为 Json 的请求,Json 对应的对象类型为 JSONObject
public class JsonRequest extends Request<JSONObject> {
public JsonRequest(HttpMethod method, String url,
RequestListener<JSONObject> listener) {
super(method, url, listener);
}
// 将 Response 的结果转化为 JSONObject
@Override
public JSONObject parseResponse(Response response) {
String jsonString = new String(response.getRawData());
try {
return new JSONObject();
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
}
JsonRequest 通过实现 Request 抽象类的 parseResponse 解析服务器返回的结果,这里将结果转换为 JSONObject,并且封装到 Response 类中。
例如,我们的网络框架中,添加对图片请求的支持,即要实现类似 ImageLoader 的功能。这个时候我的请求返回的是 Bitmap 图片,因此,我需要在该类型的 Request 中得到的结果是 Request,但支持一种新的数据格式不能通过修改源码的形式,这样可能会为旧代码引入错误,但是,你又必须实现功能扩展。这就是开闭原则的定义:对扩展开放,对修改关闭。我们看看应该如何做:
public class ImageRequest extends Request<Bitmap> {
public ImageRequest(HttpMethod method, String url,
RequestListener<Bitmap> listener) {
super(method, url, listener);
}
// 将 Response 的结果转化为 Bitmap
@Override
public Bitmap parseResponse(Response response) {
return BitmapFactory.decodeByteArray(response.rawData, 0, response.rawData.length);
}
}
ImageRequest 类的 parseResponse 函数中将 Response 中的原始数据转换成为 Bitmap 即可,当我们需要添加其他数据格式的时候,只需要继承自 Request 类,并且在 parseResponse 方法中将数据转换为具体的形式即可。这样通过扩展的形式来应对软件的变化或者说用户需求的多样性,既避免了破坏原有系统,又保证了软件系统的可维护性。依赖于抽象,而不依赖于具体,使得对扩展开放,对修改关闭。开闭原则与依赖倒置原则,里氏替换原则一样,实际上都遵循一句话:面向接口编程。
优点:
增加稳定性 可扩展性高
客户端应该依赖于它不需要的接口:一个类对另一个类的依赖应该建立在最小的接口上。根据接口隔离原则,当一个接口太大时,我们需要把它分离成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
可能描述起来不是很好理解,我们还是以示例来加强理解吧。 我们知道,在网络框架中,网络队列中是会对请求进行排序的。内部使用 PriorityBlockingQueue 来维护网络请求队列,PriorityBlockingQueue 需要调用 Request 类的排序方法就可以了,其他的接口他根本不需要,即 PriorityBlockingQueue 只需要 compareTo 这个接口,而这个 compareTo 接口就是我们所说的最小接口,而是 Java 中的 Comparable 接口,但我们这里是指为了学习,至于哪里定义的无关紧要。
在元素排序时,PriorityBlockingQueue 只需要知道元素是个 Comparable 对象即可,不需要知道这个对象是不是 Request 类以及这个类的其他接口。它只需要排序,因此,只要知道它是实现了 Comparable 对象即可,Comparable 就是它的最小接口,也是通过 Comparable 隔离了 PriorityBlockingQueue 类对 Request 类的其他方法的可见性。
优点:
降低耦合性 提升代码的可读性 隐藏实现的细节
迪米特法则也成为最少知识原则(Least Knowledge Principle),虽然名字不同,但是描述的是同一个原则,一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或者调用的类知道得最少,这有点类似于接口隔离原则中的最小接口的概念。类的内部如何实现、如何复杂都与调用者或者依赖者没有关系,调用者或者依赖者只需要知道它需要它需要的方法即可,其他的一概不关心。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
迪米特原则还有一个英文解释是:Only talk to your immedate friends(只与直接的朋友通信)。什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多例如组合、聚合、依赖等。
例如在本例中,网络缓存中的 Response 缓存接口的设计。
/**
* 请求缓存接口
*
* @param <K> key 的类型
* @param <V> value 的类型
*/
public interface Cache<K, V> {
public V get(K key);
public void put(K key, V value);
public void remove(K key);
}
Cache 接口定义了缓存类型需要实现的最小接口,依赖缓存类的对象只需要知道该接口即可。例如,需要将 Http Response 缓存到内存中,并且按照 LRU 的规则进行存储。我们需要 LruCache 类实现这个功能。代码如下:
// 将请求结果缓存到内存中
public class LruMemCache implements Cache<String, Response> {
/**
* Response LRU 缓存
*
* @param key
* @return
*/
private LruCache<String, Response> mResponseCache;
public LruMemCache() {
//计算可使用的最大内存
final int maxMemory=(int) (Runtime.getRuntime().maxMemory() / 1024);
//取八分之一的可用最大内存为缓存
final int CacheSize = int maxMemory / 8;
mResponseCache = new LruCache<String, Response>(int CacheSize) {
@Override
protected int SizeOf(String key, Response response) {
return response.rawData.length / 1024;
}
};
}
@Override
public Response get(String key) {
return mResponseCache.get(key);
}
@Override
public void put(String key, Response value) {
mResponseCache.get(key, value);
}
@Override
public void remove(String key) {
mResponseCache.remove(key);
}
}
在这里,网络请求框架的直接朋友就是 Cache 或者 LruMemCache,间接朋友就是 LruCache 类。它只需要跟 Cache 类交互即可,并不需要知道 LruCache 类的存在,即真正实现了缓存功能的是 LruCache。这就是迪米特原则,尽量少地知道对象的信息,只与直接的朋友交互。
优点:
降低复杂度 降低耦合性 增加稳定性 设计模式
在软件工程中,设计模式是对软件设计中普遍存在、反复出现的各种问题所提出的通用解决方案。这个术语是由 Erich Gamma 等人在1990 年从建筑设计领域引入到软件工程领域,从此设计模式在面向对象设计领域逐渐被重视起来。
设计模式并不直接用来完成代码的编写,而是描述在各种情况下要如何解决软件设计问题。面向对象设计模式通常以类或对象来描述其中的关系和相互作用,他们的相互作用能够使软件系统具有高内聚、低耦合的特性,并且使软件能够应对变化。
模式名称 模式名称用一两个词来描述模式的问题、解决防范和效果。基于一个模式词汇表,同行、同事之间就可以通过它们进行交流,文档中也可以通过模式名来代表一个设计。模式名可以帮助我们思考,便于我们与其他人交流设计思想以及设计结果。
问题 描述了应该在什么情况使用设计模式。它解释了设计问题和问题存在的前因后果,它可能描述了特定的设计问题,例如,某个设计不具备良好的可扩展性等,也可能描述了导致不灵活设计的类或者对象结构。
解决方案 描述了设计的组成成分,它们之间的相互关系以及各自的职责和协作方式。因为模式就像一个模板,可应用于多种不同的场合,所以解决方案并不描述一个具体的设计或者实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的类或者对象组合来解决这个问题。
效果 描述了模式应用的效果及使用模式应权衡的问题。尽管我们描述设计决策时,并不总提到模式效果,但它们对于评价设计选择和理解使用模式的代价及好处具有重要意义。软件效果大多关注对时间和空间的衡量,它们也表述了语言和实现问题。因为复用是面向对象的设计要素之一。所以模式效果包括对它系统的灵活性、扩充性或可移植性的影响,显式地列出这些效果对理解和评价这些模式很有帮助。
设计模式为反复出现的局部软件设计问题指出了通用的解决方案,在很大程度上促进了面向对象软件工程的发展。它将这些常见的设计问题一一总结,将大师们的经验、教训、设计经验分享给了所有人,使得即便是刚刚入行的工程师,也能够设计出可扩展、灵活的软件系统,大大提升了软件质量。关于设计模式领域的书籍大家可以参考《设计模式之禅》、《Android 源码设计模式解析与实战》。
避免掉进过度设计的怪圈
当你掌握一些设计模式或者方法之后,比较容易出现的问题就是过度设计。有的人甚至在一个应用中一定要将 23 种常见的设计模式运用上,这就本末倒置了。设计模式的四大要素中就明确指出,模式的运用应该根据软件系统所面临的问题来决定是否需要使用现有的设计。也就是说,再出现问题或者你预计会出现那样的问题时,才推荐使用特定的设计模式,而不是将各种设计模式套进你的软件中。
不管在设计、实现、测试之剑有多少时间都应该避免过度设计,它会打破你的反馈回路,使你的设计得不到反馈,从而慢慢陷入危险中。所以你只需要保持简单的设计,这样就有时间来测试该设计是否真的可行,然后作出最后的决策。
当设计一款软件时,从整体高度上设定一种架构模式,确定应用的整体架构,然后再分析一些重要米快的设计思路,并且保证他们的简单性、清晰性,如果有时间可以使用 Java 代码模拟一个简单的原型,确保设计是可行的,最后就可以付诸行动了。切实不要过度的追求设计,适当就好,当我们发现或者预计到将要出现问题时,在判断是否需要运用设计模式。
反模式
反模式是一种文字记录形式,描述了对某个问题必然产生的消极后果的常见解决方案。由于管理人员或者开发人员不知道更好的解决方案,缺乏决定特定问题的经验或知识,或者说不适合的条件下套用了某个设计模式,这些都会造成反模式。与设计模式类似,反模式描述了一个一般的形式,主要原因、典型症状。后果,以及最后如何通过重构解决问题。
反模式是把一般情况映射到一类特定解决方案的有效方法。反模式的一般形式为它所针对的哪类问题提供了一个易于辨识的模板。此外,它还清楚地说明了与该问题相关联的症状以及导致这一问题的内在原因:把特定设计模式应用于不正确的环境。
反模式为识别软件行业反复出现的问题提供了实际经验,并为大多数常见的问题提供了详细的解决方案。反模式对业界常见的问题进行总结,并且告诉你如何识别这些问题以及如何解决。它有效的说明了可以在不同的层次上采取的措施,以便改善应用开发过程,软件系统和对软件项目的有效管理。
总的来说,设计模式总结了在特定问题下正确的解决方案,而反模式则是告诉你在特定问题上的错误解决方案和他们的原因、解决方案,通过最终的解决方案,它能够将腐化的软件系统拉回正轨。
总结
灵活的软件设计需要知识和经验与思考,好的设计通常是经历了时间的洗礼慢慢演化而来,工程师的成长也是一样。因此,掌握必要的面向对象、设计模式、反模式等知识,并且这工作中不断实践、思考,将使你的软件设计之路走得更加从容、顺畅。
写在后面: 面向对象的六大原则在开发过程中极为重要,他们给灵活、可扩展的软件系统提供了更细粒度的指导原则。如果能很好地将这些原则运用到项目中,再在一些合适的场景运用一些经过验证过设计模式,那么开发出来的软件在一定程度上能够得到质量保证。其实六大原则最终可以简化为几个关键字:抽象、单一职责、最小化。那么在实际开发中如何权衡,实践这些原则,也是需要大家在工作过程中不断地思考、摸索、实践。