关闭

谈谈面向对象 之 抽象

标签: 面向对象软件开发
2639人阅读 评论(0) 收藏 举报

一、引言

上一篇博客谈了面向对象的基本概念,从这篇开始,聊聊面向对象的几个特征:抽象、封装、继承、多态。我们先拿抽象这一特征开刀。

不止一位同事将抽象这一特征排除在外,认为面向对象有三个特征。这让我感到意外,因为,抽象是面向对象最为重要的特征。对象本身的状态与行为,以及对象之间的关系,都是抽象的结果。没有抽象,就没有对象,也就谈不上面向对象了。因此,抽象是面向对象的第一特征。

那么,都抽象什么呢?

二、抽象的内容

广泛流传的说法是,相比于面向过程,面向对象的优势在于将问题空间和解空间统一起来。在面向过程的时代,问题空间(现实世界)中是对象,解空间(设计、代码)中是过程,两者不一致,需要来回切换,能把人的精神搞分裂。而到了面向对象的时代,问题空间和解空间要处理的都是对象,这样开发系统就顺畅多了。

这样的说辞带有严重的学究气,但说明了一个问题,认为系统中的对象是从现实世界中的对象转换而来的,这种转换是提取其相关属性,摒弃无关属性。换句话说,也就是抽象。想要高端大气上档次,不妨称之为现实客体的静态映像。但这只是抽象的内容之一,

2.1 现实客体的静态映像

这种抽象,对于要处理的客观对象,在系统中也构造相应的对象。提取其中需要关注的属性,而摒弃那些无关的属性。

这是最为简单的抽象,因为其在现实世界中可见,因而容易理解,抽象程度也就比较低。很多菜鸟谈到抽象这一特征时,往往指的是这一点。

例如,现实世界中有用户,我们就抽象出 User 这一对象类,我们关注的属性,如用户名,登陆账号,密码,联系方式等等,都提取过来。而无关的属性,如长相,穿衣习惯,政治面貌,等等,则置之不理。

以能源管理系统为例,装置,介质,管网,节点,等等,既是现实世界中的对象,也是系统中的对象。

深入到我们在做的这个系统的内部看看,有很多对象的抽象只到了这一层面。看看这些对象的类定义,我们会看到他们有共同的特征:类的内容主要是抽象过来的那些属性,方法很少。这种对象称为POJO(Java)或POCO(.NET)。

他们都是初步抽象的产物,还不成熟,等待长大。

2.2 行为封装

属性是对象的状态。对象不仅仅有状态,还有行为。对象幼小时,能做的事很少,也就是行为很少。其成长的重要标志,是职责增加,行为增加。也就是方法的增加。

因此,抽象的第二个层面,是对象行为的抽象。

仅抽象对象的状态,将行为独立于对象,仍然是 程序 = 数据结构 + 算法 的思维,是面向过程的思维。

将和对象紧密耦合的过程,赋予对象,成为其方法,即将处理逻辑封装到对象内,这就是行为抽象。

例如,对于用户,我们需要检查其是否通过认证,是否通过鉴权,这都可以成为其方法。对于装置,获取投入、产出、损失、能源消耗等,也是其方法。

一个真正能用于解决实际业务问题的系统中,对象方法个数往往会超过其属性的个数。

2.3 行为提炼

将行为中相同或相似的部分提炼出来,定义为公共的行为,是设计和开发的重要表现。这种行为的提炼,往往被我们的开发者所忽视。

我们做了一个报表工具,要累加指定期间内各个时间段的量,时间段有不同的类型,如日、旬、月、季、年等。例如,指定期间 1月1日到4月30日,如果时间段类型是日,对于这四个月的120天,取每天的量累加起来,如果时间段类型为月,则对于这四个月,取每月的量累加起来。

我们的程序员最初是这样做的:

decimal sum = 0;

if (periodType == PeriodType.日)
{
    for (DateTime dt = StartTime; dt <= EndTime; dt = dt.AddDays(1))
    {
        sum += GetData(dt, Period.日);
    }
}
else if (periodType == PeriodType.月)
{
    for (DateTime dt = StartTime; dt <= EndTime; dt = dt.AddMonths(1))
    {
        sum += GetData(dt, Period.月);
    }
}
else ...

在实际代码中,这个 if else 的序列相当长,求值累加的逻辑也不像这里只有一行,因而显得重复而冗长。

这里的问题表现为缺少提炼:可以将这个过程抽象为,针对给定的时间序列,累加序列上各个时间点的值:

decimal sum = 0;

foreach (DateTime dt in GetTimeSeries(BeginTime, EndTime, periodType))
{
    sum += GetData(dt, periodType);
}

(如果用resharper,它会提醒你有更简便的表示。)


至于计算时间序列,就是一个很容易处理的问题了。

对行为的提炼,不仅仅限于对象的方法,也适用于其他过程化开发。

在以前做的一个项目中,需要计算各个装置的单位能耗:单位能耗 = 能耗量 / 加工量,这个是在Oracle中用存储过程实现的。问题在于,各个装置的能耗量和加工量的计算方法大致相同,但不完全一样。我们一个兄弟,就为每一套装置,分别写了一套能耗量和加工量的计算逻辑。那是在燕山石化,一百多套装置啊,他那个Package写了一万多行。后来需求不停地变啊,一旦有一点变动,他就要调到半夜。就这样,生生把自己逼离职了。后来我们做了重构,对计算方法做了分类整理,采用通用的计算方法,将不同之处通过加配置来项处理,将一万多行的PL/SQL,变成了一千多行。

2.4 非现实对象的出现

让问题空间和解空间中都是对象,就认为天下太平了,真是 very simple, sometimes naive,这严重低估了系统的复杂性。

问题空间和解空间中确实都是对象,但从需求分析阶段(当然得是面向对象分析)开始,两个空间中的对象就不一样了,随着项目的进展,这种区别会越来越大:解空间中会出现现实世界中不曾有过的对象,这样的对象越来越多。

例如,用户管理中,角色这样的对象,是分析过程中抽象出来的。

更常见的例子是设计模式中出现的那些类,诸如策略、状态、XxxImp之类。更不用说各种抽象类和接口了。

这种无中生有,是进一步抽象的结果。这也更进一步促进了行为的抽象:将合适的行为抽象到新的类中。

2.5 类结构提炼

一些类的集合,形成一种较为固定的结构,完成特定的功能。例如设计模式,例如MVC这样的Paradigm,由多个类组成,但比架构小。这种抽象,提炼出了类的状态、类的行为,更重要的是类与类之间的关系。

在我们的项目中也可以找出类似的例子。我们用NPOI将一个二进制格式的Excel文档,转换为Excel 2003 XML格式的文档。我们把这个转换抽象为树复制,Excel的内容可以看作一棵树:根节点是Workbook,下面是若干Sheet,下面是若干Row,等等。XML天然是一棵树。有了这个抽象,就可以利用树的特性,后面的实现就是如何转换各类节点的细节问题了。

3、抽象是一种思维习惯

用喊口号的方式结束吧:面向对象不仅仅是一种技术,而是一种方法论。与此类似,抽象不仅仅是上述几类动作,而是一种思维习惯。它不是面向对象的专属,而可以用于任何开发场合。





0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:24971次
    • 积分:426
    • 等级:
    • 排名:千里之外
    • 原创:17篇
    • 转载:5篇
    • 译文:1篇
    • 评论:6条
    最新评论