您的位置:知识库 » 软件设计

老话重弹——再谈接口与抽象类

作者: 飞林沙  来源: 博客园  发布时间: 2010-11-02 14:21  阅读: 1195 次  推荐: 1   原文链接   [收藏]  

  1. 从依赖倒置说起

  首先,我们来看下《敏捷软件开发》中对依赖倒置的说明:

  a. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。

  b. 抽象不应该依赖于细节,细节应该依赖于抽象。

  我们先抛开第二点来看第一点,什么叫高层模块,什么叫低层模块。在我理解来看:高层模块也就是战略性模块,业务性模块。而低层模块就是战术性模块,细节类模块。

  先来看这样一段代码:

class Person
{
private Mouth mouth;

public Person(Mouth mouth)
{
this.mouth = mouth;
}

/// <summary>
/// 吃饭
/// </summary>
public void Eat()
{
if (mouth == null)
{
throw new NullReferenceException();
}

mouth.OpenMouth();
FillMouthWithFood();
mouth.CloseMouth();
}

private void FillMouthWithFood(){ }
}

class Mouth
{
/// <summary>
/// 张嘴
/// </summary>
public void OpenMouth() { }

/// <summary>
/// 闭嘴
/// </summary>
public void CloseMouth() { }
}

  也许有人会说,这是再正常不过的代码了。但是我们要考虑到,张嘴和闭嘴的动作是具体的行为,具体的行为就可能发生变化。而在这里,高层代码Person类还依赖于低层代码,这时当低层代码发生变化时,高层代码也就会随之发生变化。而如果随着层数的增多,震荡地剧烈程度也就会随之增加。

  接下来我们来看这样一个图:

image   在传统的过程式设计中,复用都偏向于低层次模块的复用,例如说算法的复用,数据结构的复用,再高级一些可能是某些通用函数的复用,是细节的重用,而忽略了战术层次上的重用。这就是高层依赖于底层的缺点。(请注意,我并没有说,这样不好。)

  那么,我们要怎么样来解决这种情况。

  2. 抽象,我们谈谈抽象

  这个词太时尚了,当然,这个词本身也很抽象,那么什么叫做抽象。

  我们来看看《现代汉语词典》对这个词的解释:抽象是从许多事物中,舍弃个别的,非本质的属性,抽出共同的特征而形成的。

那么说,抽象类和接口都属于抽象。所以很多书上说:接口优于抽象类,我觉得这句话实在是一句非常扯淡的话!接口是抽象,难道抽象类就不是抽象了?明明是两种完全不同的东西,谈何比较?!

  面试的时候,大家也许都被面试过一个问题就是接口和抽象类的区别,网上最常见的一种答案就是:接口代表一种Can-Do语义,抽象类是一个Is-a的语义。这句话不无道理,我以前也都是这样来回答笔试和面试题。可是现在我觉得这句话并不能对真正的设计有任何的帮助。

  这么说吧,任何方法都是可以被表示成Can-Do的语义,那是不是说,所有的方法都要被放到接口中,而所有的抽象类都是贫血类,或者说所有的抽象类都要实现接口么?

  3. 接口和抽象类的区别

回顾我们在第一点种说的依赖反转原则,高层不应该依赖于低层,那么也就是说在设计时,不应该遵循从低层到高层的设计。而应该先设计整体的业务(也就是高层模块,战略性内容),然后再根据高层模块去设计低层模块(也就是具体实现)。而在高层与底层之前也不应该直接产生依赖,而应该在他们两层之间搭建一层抽象,这层抽象在我看来可以说叫做“接口”。

那么这里我就提出我对接口和抽象类的认识:接口是从高层需求而来,抽象类是从底层总结而来。

我来解释一下我这句话,在设计一个高层模块的时候,这个高层模块不知道低层模块的细节,甚至不知道实现方式。它只知道我需要一个这个行为,然后需要一个这个行为。比如这个代码:

class Game
{
public void Sport()
{
Run();
Walk();
Swim();
}
}
这是一个高层代码,比赛中有一项运动是先跑步,在竞走,最后是游泳。也许比赛分为男子组和女子组,不同的人走路,跑步方式还不一样。但是我并不关心这个行为的所属者是谁,我只要求知道,我需要这个行为。这个就是接口的来源。
class Game
{
private IRunable runner;
private IWalkable walker;
private ISwimable swimmer;

public Game(IRunable runner,IWalkable walker, ISwimable swimmer)
{
this.runner = runner;
this.walker = walker;
this.swimmer = swimmer;
}
public void Sport()
{
runner.Run();
walker.Walk();
swimmer.Swim();
}
}

interface IRunable
{
void Run();
}

interface IWalkable
{
void Walk();
}

interface ISwimable
{
void Swim();
}
而抽象类则是从底层的实现中提炼出来的。所以我们不妨也可以换种说法。“接口是一个面向对象分析与设计的必然结果,而抽象类则是重构的结果”。或者我们也可以这样说:“先有接口,后有抽象类”。

本来想结束这一节,但是为了希望大家进一步理解我的观点,我再补一句,绝对不应该从两个类中抽取出接口。

  4.自底向上vs.自顶向下

  究竟是自底向上还是自顶向下,这两种究竟哪一种更好,相信每个人都有自己的观点。那么我们来想一下这两种方法的优劣。

  自底向上方法关键在于组装,先设计低层,低层不知道高层的业务,就像搭积木,每个积木都不是为了最终的建筑物而设计,而是遵循它自有的形状。关键在于玩家自己的组装。

  自顶向下方法的关键在于最初业务的分析,根据需求文档中的业务,提取出业务模型,然后根据业务模型分析出每个业务的行为,然后去分别实现每个行为。

  自底向上方法的最大优点在于底层模块的重用性,就像积木一样,可以搭成高楼,也可以改成狗屋。但是劣势在于底层的变动会引起高层的级联震荡,此外,积木设计者相当于我们的架构师,玩家相当于我们“套页面”的程序员,架构师的好坏会完全直接影响到最终的项目成败。

  自顶向下方法最大的优点就在于上面提的,依赖倒置会保证业务的可重用性,此外,不会让架构师去凭空想象一个业务逻辑,而是根据需求文档中的业务逻辑去整个他的业务模型。当然,劣势就在于底层代码的重用性会极低。

  那么,我们究竟该如何去做呢。这里我只提出自己的观点。

  在软件工程中有一个“V”字型模型,具体是做什么的我有些记不清楚了。在这里,我觉得也可以用V字型模型来做。

  在接到需求文档的时候,采用自顶向下的分析方法来整理出相关业务,然后每个业务的大致实现逻辑,根据这些实现逻辑来抽取去接口,OK。高层模块已经设计完了。

  接下来,我们来采用自底向下的方法来分析。其实就是我们常说的“领域模型”,相信我,单纯地用自顶向下的分析方法是很难设计出通用的领域模型的。这个领域模型,就是我们最可重用的模块。而领域模型的设计,也正是我们“设计模式”的最大应用之处。

  看到上面的两段,就是先有接口后有抽象类的典型。

  说到最后,越说越乱了,算了,停下来吧。。。。。。

1
0
标签:接口 抽象类

软件设计热门文章

    软件设计最新文章

      最新新闻

        热门新闻