看设计模式的相关书籍也有一段时间了,一开始其实是抱着作为java三大框架的基础知识储备来学习的,不过到后来,才发现,在设计模式的一些准则装饰下,java的面向对象威力才真正地体现出来,后面的将会陆续地总结设计模式学习过程中的一些心得体会,这篇作是个人理解设计模式中的一些核心思想的简单总结,但是也是我认为在设计模式中最核心的部分思想了。
一、软件工程的设计尝试
软件工程的一些要求。在软件工程中,最基本的要求便是可重用行以及扩展性,前者要求系统的设计在代码层面可以有良好的组织结构以便公共代码可以一次编写导出调用;后者则要求软件系统在升级维护时可以在不影响前面已经编码好的模块中进行,可扩展性要求我们增加代码达到扩展效果而不是修改代码(毕竟,修改会增加bug出现的风险)。
而软件系统的以上要求,使得我们设计的软件模块要具有最终要的两个性质:高内聚而低耦合,前者说明模块功能尽量依靠自己内部完成,模块本身就可以最大化地实现自己本来的功能;低耦合和高内聚其实意思差不多,只是换了一个算法,它要求模块之间的联系尽可能地降到最低。
二、对象的封装
说到面向对象,可能没有系统思考或者学习过设计模式地人第一印象就是:面向对象=封装类。的确,以前我也是这样肤浅地认为面向对象,不就是将一对相关联的变量和方法,包装 成一个adt,也就是java中的一个类进行复用嘛,在需要的时候就new一个对象调用相应的方法不就行了,封装貌似已经可以解决很多问题了。
的确,封装为我们提供了一种代码重用以及面向对象化的抽象方式来组织我们的程序结构,它可以使得我们拥有高效的程序组织手段。然而,很多时候,仅仅依靠封装的性质是不能达到软件设计所要求的高内聚低耦合的,具体例子后面再进行举例。
三、面向抽象编程
1、利用父类引用进行操控对象。
前面说了,对象的封装不能解决软件工程的扩展性问题,下面例子便没有符合软件工程的要求了:
//有一个动物类
class Animal{
//它有各种属性
String height = null;
String foot = null;
//....其他属性
//还有一些方法
public void run(){
System.out.println("animal run method");
}
public void eat(){
System.out.println("animal eat method");
}
}
//下面有一个cat继承了animal类
class Cat extends Animal{
//覆盖掉父类方法
public void run(){
System.out.println("cat run method");
}
}
//则是一个测试类
class Test{
public void test(){
Cat cat = new Cat();
cat.run();//显然,这样可以调用cat的run方法
}
}
观察代码,在test方法中,它有以下可优化的地方:
A、cat.run并没有过多地体现出父类的作用,它仅仅是调用自己的对象引用来执行方法,当我要在test方法中执行其他动物的run方法时,还得重新写一个为某个动物而特别设定的test方法,没有达到重用代码的目的;
B、另外,如果在某个时候,该test方法中的动物不在是cat了(例如我加突然不想养猫了而换成狗),这时还需要改变硬编码(当然可以利用A的方法新建特定的test方法)
C、这段代码没有体现面向对象一个很重要的性质:多态。既然子类都可以进行run的重写操作,就应该在代码中表现出不同子类的同样方法会表现出不同的行为
(好吧,其实这三点归根到底就是一点:代码不可扩展,没有体现多态)
下面进行简单的改写(仅仅重新写test类):
//这是一个改写后的测试类
class Test{
Animal animal = null;
//新增一个构造函数
public Test(Animal animal){
this.animal = animal;
}
public void test(){
//通过父类引用操作子类对象
animal.run();
}
}
改写后的代码具有高度的内聚性,因为Test类它本身并不依赖其他的任何动物,如果你要对Test方法执行cat对应的方法,直接在构造函数中传入cat对象即可,需要dog就传入dog就可,java的多态性质会自动帮你根据特定的类完成特定的run方法!显然,这个test类变达到了软件工程的基本要求:高内聚(只是依靠内部代码进行相应的操作),低耦合(没有依赖特定的对象,只是依赖抽象层次的animal)
所以可以有一下总结:通过操作父类的引用来控制操作子类对象可以达到多态的效果。
2、通过接口操作对象。看下面的例子:
//定义一个接口,它抽象了一种行为:飞
interface FlyAction{
public void fly();
}
//bird类
class Bird extends Animal implements FlyAction{
//实现接口
public void fly() {
System.out.println("I am bird ,this is my fly methd");
}
}
//duck类
class Duck extends Animal implements FlyAction{
public void fly() {
System.out.println("I am duck ,this is my fly methd");
}
}
//类似,我们新建一个测试类
class Test{
FlyAction animalFlyAction = null;
public Test(FlyAction animalFlyAction){
this.animalFlyAction = animalFlyAction;
}
public void test(){
animalFlyAction.fly();
}
}
代码较简单,我从代码中主要传递一下的一些思想:
A、为什么将fly定义为接口,而不是直接在animal中新增一个fly方法?
因为不是所有的animal都会飞,animal中的代码必须保证所有子类都共有。由此,我们大概可以体会到实现接口与类继承的一些区别了:前者是特定行为的扩展,后者是公共行为的提取与抽象。
B、接口操作对象的优点:显然,有了上面的父类引用操作子类对象的讨论后,我们可以领悟到接口操作对象其实是类似的:通过java的多态机制实现特定实现类的调用同样的方法,而方法的实现却不相同,从而达到了高内聚低耦合的效果(也是多态的效果)。
四、总结
所以,通过上面例子,我们大概可以对面向接口编程或者说面向父类引用编程(其实更恰当地说是面向抽象编程,不过这个概念后面博客再具体讲解)有个大体的认识了:其实就是通过父类引用来操作子类对象或者通过接口引用操作对应的实现类进行代码的解耦和多态的实现。
所以,我们可以得出设计原则一:
尽量利用父类引用和接口进行对象的具体操作。
OK,设计模式的第一篇就到这里,后面的博客再整理一些更深入的思想——面向抽象编程以及封装变化的手段。