审视对象的问题所在






4.94/5 (138投票s)
基于CodeProject用户对“对象有什么问题?”这个问题的回答,探讨面向对象设计/面向对象编程(OOD/OOP)中存在的问题。
目录
引言
当我问“对象有什么问题?”时,收到了许多回复,这些回复都非常出色,涵盖了从核心实现问题到哲学问题的软件设计全谱。最令我惊讶的是,在最初的17个回复中,几乎每个都指出了不同的问题。在我看来,这是一份非常棒的知识集合,非常适合整理成一个单一的存储库。所以,这篇文章就是关于这个的。感谢所有回答我问题的人。我希望您觉得它既有趣又有益。本文中任何错误或纯粹愚蠢之处都完全由我负责,与您无关。如果您不同意某一点,请告诉我。说真的,我自己也不同意其中的几点,但如果您不表明立场,又怎能学习呢?
人的问题
几位CodeProject用户指出,面向对象编程(OOP)之所以失败,是因为人们缺乏足够的人际交往能力。
培训
培训是关键因素。学习曲线可以用下图表示。这是一个相当通用的学习曲线表示——例如,它没有深入到专业领域,如SQL、客户端/服务器、Web应用程序等。我花了一些精力来整理我认为是一个合理的学习编程的方法。例如,如果我要编写一个编程课程,它可能会是这样的。
上图只是情况的一半。这是另一半(打印出来并拼在一起!)。
关键在于,一旦你掌握了基础知识,就可以开始关注设计问题、代码重构和其他能增强你作为程序员/设计者能力的技术。这条学习曲线非常困难,因为学习OOP与学习MFC等框架、调试器等工具以及STL等语言增强(找不到更好的词了)紧密相关。事实上,学习OOP需要先学习大量非OOP概念!
为了完整起见,我将尝试用一句话描述每个阶段,并扩展“大师三角”。
编程基础 | 这是纯粹的架构和冯·诺依曼机的机制 |
编程工具 | 编译器、调试器、源代码管理、缺陷跟踪器、性能分析器等。 |
语言基础 | 编程基础应用于特定语言的语法 |
过程式语言 | 编写良好的过程式代码 |
API和SDK | 学习平台的基础 |
封装 | 从封装形式和功能的角度审查过程式代码 |
修饰符 | 隐藏内部实现并公开公共接口的艺术 |
多态 | 利用强类型语言 |
继承-特化 | 如何特化类的功能 |
继承-抽象 | 抽象思维是否可以教授? |
继承-单重与多重 | 单重与多重继承问题 |
接口 | 接口是命令程序员实现功能,而不是继承。 |
对象应用 | 抽象思考——它是高尔夫球对象还是具有高尔夫球属性值的球? |
对象应用 | “是一种”与“拥有”问题 |
模板/泛型/元数据 | 又一层抽象 |
STL | 一团乱麻——非常有用的,但很容易被滥用和误用 |
框架 | MFC, .NET(没有其他星球上有智慧生命了) |
重构,第一级 | 方法内重构——修复表达式、循环、条件等。 |
重构,第二级 | 对象内重构——修复内部对象实现问题 |
设计模式,第一级 | 创建模式——这些最容易理解。 |
重构,第三级 | 对象间重构——修复继承体系。 |
设计模式,第二级 | 结构模式——解耦对象依赖。 |
重构,第四级 | 设计模式重构——希望你永远不用做这个。 |
设计模式,第三级 | 行为模式——学习识别代码中的行为,并将通用行为与特定实现解耦。 |
临时方法
我过去发布的关于eXtreme Programming等方法的帖子收到了各种各样的回复,大部分都是负面的。我发现令人不安(尽管并不意外)的是,似乎每一代人都必须重新学习过去的错误。我并不特别确信我们在提高质量和客户关系,更不用说减少bug、按时交付和准确估算项目方面正在朝着前进的方向发展。看起来更像是我们正在原地踏步。TQM、ISO 9001、CMM、敏捷方法以及各种其他方法似乎都从不同的角度解决了同一个问题。大约每五年,就会出现一种关于“如何成功编写程序”的新潮流。
但真正令人不安的是,根据我与其他程序员的交流,似乎很少有人真正了解这些方法中的任何一种。看来,那里存在大量的临时软件开发。我认为是时候停止原地踏步,而是向前迈进了。我认为重构和设计模式方面的书籍是朝着正确方向迈出的一步,因为它们为程序员提供了实际可用的具体(或大部分具体)工具,这些工具确实能让他们成为更优秀的程序员。但这些概念也很难掌握。这就是为什么我对自动化和开发强制执行良好编程实践的框架如此热衷。说起来容易做起来难,而且也有权衡——当框架迫使程序员以非常特定的方式与其他代码交互时,程序员会失去一定的创造力和控制力。更糟糕的是,谁能说我正在做的是真正的向前解决方案呢?它可能只是原地踏步,甚至,天哪,是退步!
正规教育
正规教育发生了什么?现在人们获得学位是为了找工作和获得更好的报酬,而不是真正学习东西。学习似乎已经退居人生的第二位。但这也有一点道理。我在美国(尤其是在加利福尼亚)的高等教育机构的经历非常糟糕。过时的机器,使用过时的操作系统和工具的过时编程课程,以及教授们只看“是否遵循指示”而不是高效高性能的解决方案来评分。最大的抱怨——大学根本不了解实际编程是什么样的。可悲的是,在计算机科学领域,正规教育似乎是一种必要的邪恶。过去,大学是创新和研究的领导者。现在,它们似乎更像是社交障碍者的避风港,而“教育”则使人技术上不称职。
具体的技术问题
技术困惑
许多人指出,在使用OOP方面存在很多困惑。面向对象设计(OOD)只提供指导方针。OOD有很多模糊的地方,只能通过痛苦的经验来学习。
不仅仅是一个容器?
在计算机体系结构发展过程中,数据和指令在物理上(在地址空间中)分离,从而实现了创建多线程、多任务系统,并保护它们不相互干扰。对象用于统一数据和指令的分歧。是的,它们仍然在物理上分离,并局限于应用程序及其数据,但关键在于对象将数据和操作绑定到一个逻辑结构中。那么,对象仅仅是这样吗?是的,就对象本身而言,就是这样。当对象与其他对象相关时,事情会变得更有趣,也更复杂。
现在,上面的图表有点可笑,但我认为它也能说明硬件和软件体系结构的演变。最后一列,“Aspect Computing”,是我完全发明的东西,但我觉得随着分布式服务的开发和组件/组件通信的标准化,大量的“编程”将仅仅是粘合不同的组件、处理事件和协调它们之间的数据流。
继承还是封装,这是个问题
我学习设计对象结构时首先问自己的问题是,对象A是对象B的“一种”,还是它“拥有”一个对象B。不幸的是,这只是冰山一角。许多关系没有明确定义,有时很容易被视为两者皆有。例如,在GUI设计中,如果我想要一个自定义控件,那么自定义控件应该是通用控件的“一种”,还是它应该“拥有”一个通用控件?两者都有利弊。
如果对象是派生的
那么用户(另一个程序员)很可能(除非基类在派生类中声明为private
)拥有对基类方法的完全访问权限,并且自定义控件需要确保它处理了基类的所有功能。
列表 1:继承
class BaseClass
{
public:
virtual ~BaseClass() {}
void Foo(void) {}
};
class DerivedClass : public BaseClass
{
public:
void Bar(void) {}
};
// usage:
DerivedClass* dc=new DerivedClass();
dc->Foo();
dc->Bar();
如果控件被包含
那么对通用控件的访问可以得到严格控制(无双关语)。
列表 2:封装
class InnerClass
{
public:
// note the missing virtual destructor
void Foo(void) {}
};
class OuterClass
{
private:
// private restricts access by derived classes
InnerClass ic;
public:
void Bar(void) {}
};
更糟糕的是,根据基类是如何实现的,程序员可能没有选择。非虚拟析构函数、未声明为virtual
的方法以及private
成员通常使得通过派生来特化对象变得不可能。
另一个关注点是在增长一个僵化的继承结构与增长一个高度依赖的关联结构之间的权衡。在这两种情况下,对象之间都有很强的耦合。然而,在使用继承结构时,基类很容易通过改变实现来破坏应用程序。
封装稍微缓解了这个问题,因为封装允许你实现围绕其他对象的包装器,而继承则不能。
包装器是保护你免受其他对象更改的重要方法。根据基类的实现(例如,.NET的Form
类),可能需要实现一个派生类来访问protected
方法并封装特化类。
继承还会将你锁定在一个随着时间推移可能变得不适用的结构中。可能希望特化两个或更多对象的函数(多重继承)或通过从其他东西派生来改变对象的行为。特别是对于不支持多重继承(如C#)或不支持两类实现的方法重复(如C++)的语言,封装可能是更好的选择。
正如你所见,继承与封装的决定不仅仅是一个简单的关系决定。它涉及访问、支持、设计、可扩展性和灵活性的问题。
基因库——单继承 vs. 多继承
考虑三种不同类型的球
- 高尔夫球
- 篮球
- 网球
它们被以下类很好地表示,所有类都派生自Ball
列表 3:一个看起来合理的类结构
class Ball
{
public:
Ball(void) {}
virtual ~Ball() {}
virtual void WhoAmI(void) {printf("Ball\r\n");}
virtual void Bounce(void) {printf("Ball\r\n");}
double diameter;
}
class GolfBall : Ball {}
class BasketBall : Ball {}
class TennisBall : Ball {}
每个专门版本都实现了它特有的附加属性。例如
列表 4:GolfBall 派生包含了特化
class GolfBall : Ball
{
public:
GolfBall(void) : public Ball() {}
virtual ~GolfBall() {}
virtual void WhoAmI(void) {printf("GolfBall\r\n");}
virtual void Bounce(void) {printf("GolfBall\r\n");}
DimplePattern p;
Compression c;
}
(来自 http://news.bbc.co.uk/sportacademy/bsp/hi/golf/equipment/other_gear/html/ball.stm 和 http://www.golftoday.co.uk/clubhouse/library/qandaballs.html)
列表 5:BasketBall 派生包含了特化
class BasketBall : public Ball
{
public:
BasketBall(void) : Ball() {}
virtual ~BasketBall() {}
virtual void WhoAmI(void) {printf("BasketBall\r\n");}
virtual void Bounce(void) {printf("BasketBall\r\n");}
Category cat;
ChannelSize csize;
Color color;
}
(来自 https://www.sportime.com/products/smartbasketballs.jsp)
列表 6:TennisBall 派生包含了特化
class TennisBall : public Ball
{
public:
TennisBall(void) : Ball() {}
virtual ~TennisBall() {}
virtual void WhoAmI(void) {printf("TennisBall\r\n");}
virtual void Bounce(void) {printf("TennisBall\r\n");}
Speed speed;
Felt feltType;
Bounce bounce;
}
(来自 http://tennis.about.com/library/blfaq22.htm)
现在假设我们想要一个新的球,一个GobaskisBall
球,它结合了高尔夫球的压缩性、篮球的颜色和网球的绒面材质。从新手角度来看,按照我们正在创建一个具有其他三个球特征的专用球的想法,Gobaskis
球可能会这样构建
列表 7:派生一个具有其他三个球特征的新球
class GobaskisBall : public GolfBall, public BasketBall, public TennisBall
{
public:
GobaskiBall(void) : GolfBall(), BasketBall(), TennisBall() {}
virtual ~GobaskiBall() {}
virtual void WhoAmI(void) {printf("GobaskiBall\r\n");}
virtual void Bounce(void) {printf("GobaskiBall\r\n");}
}
这是继承的错误应用,因为它会创建“死亡之钻”(diamond of death)
(我稍后会添加一些解释。现在,就说我确实写了一些东西,但我犯了一个非常非常愚蠢的错误,最好在你们其他人注意到之前把它删除!)
是对象还是属性?
创建对象模型时,一个非常令人困惑的事情是确定什么是对象,什么是属性。现实世界对事物的建模方式与它们在OOP语言中最佳建模方式之间存在争议。以上面的例子为例,还要考虑继承模型效率低下,并且当添加其他属性时,很可能会导致大量依赖代码被重新编译。
在上面的例子中,为了避免多重继承问题,程序员很可能会将所有感兴趣的属性放在一个派生自Ball
的新类中。
列表 9:将属性放入派生自Ball的新类中
class GobaskisBall : public Ball
{
Compression c;
Color color;
Felt feltType;
}
但精明的程序员会意识到,这些属性非常通用。它们不仅可以以各种组合方式组合,而且球的“需求”可能会随着应用程序的开发而变化。这导致了认识到拥有专门的ball
类可能是不正确的做法。相反,需要的是一个单一的球类,它包含一个ball
属性的集合。然后,一个“球工厂”方法可以构建所需的任何特定ball
实例,无论是描述ball
的属性还是这些属性的值。
列表 10:抽象球的概念并使用集合代替
class Ball
{
Collection ballProperties;
}
我们现在已经消除了特化,并创建了一个叫做“集合”的东西。(这里还有更多内容,如下面“抽象致死”部分所述)。
正如我上面通过示例希望说明的那样,继承的概念并非总是有效,使用抽象和集合会在设计时做出如何实现对象模型的决策——对象本身是一个一等公民,还是对象本身的概念最好通过属性来表达?这不是一个容易的答案,也没有正确的答案。
接口不同于继承
基类可以包含字段和默认实现。接口两者都不包含,要求派生类提供所有实现。例如,使用C#,IBall
的所有方法和属性都必须实现。
列表 11:接口需要实现
interface IBall
{
void Bounce();
double Diameter {get; set;}
}
public class TennisBall : IBall
{
private double diameter;
public double Diameter
{
get
{
return diameter;
}
set
{
diameter=value;
}
}
public void Bounce() {}
}
正如本例所示,将Ball
制作成接口类会带来问题。例如,无法实现默认的Bounce
函数行为。更糟糕的是,每个类都必须实现自己的Diameter
属性和访问方法。微软的解决方案是自动化IDE,这样你就可以按Tab键,它就会生成所有接口存根。太棒了。这对于语言约束和/或糟糕的对象模型毫无帮助。(是的,我勉强承认它确实会迫使你养成一些良好的习惯,例如用get/set属性访问方法包装字段。)此时,我认为我们拥有了最糟糕的两方面——一个不支持多重继承的语言,以及一个需要大量代码重复的接口类结构。那么替代方案是什么?
首先,不应将接口类视为多重继承的解决方案。相反,它们应该被视为实现要求。这对于经验丰富的程序员来说可能很明显,但对于初级程序员来说并非如此。事实上,我清楚地记得我曾经共事的Java开发者说接口是多重继承的替代品。我认为事实并非如此。“class Foo implements the interface Biz”这句话清楚地表明了接口的用途——实现功能。因此,接口应该非常小——它们应该只指定实现一个非常“垂直”需求所需的功能(好吧,这是Marc的建议)。
然而,如何指定默认行为以及如何使用指定一组参数的便利性,这些参数是所有相似(如派生)类都可以在其自身特化中包含的问题仍然存在。显然,这可以通过使用类而不是接口来实现,但这可能不是理想的方法。在单一继承语言中,必须有所取舍。反之,也许问题应该是:“拥有默认行为和一组默认字段这个想法真的那么好吗?”答案可能是否定的,“实际上不是个好主意”。但是,当有人刚开始学习OOP时,很难理解“为什么不是”。请继续阅读……
对象是否充分分解?
有一天,一个年轻人从他学习音乐的大学回家。像往常一样,他总是经过一个墓地。那天,他听到墓地里传来奇怪的音乐。这音乐听起来有点熟悉,但他就是想不起来。第二天又发生了。第三天,他带了他的教授一起来帮他辨认音乐。教授惊呼:“为什么,那是在解构贝多芬!”
首先,上述示例中的IBall
接口过于复杂——它没有得到充分的分解。真正应该存在的是
列表 12:分解IBall
interface IBounceAction
{
void Bounce();
}
和
interface IBallMetrics
{
double Diameter {get; set;};
}
这会将球的形状与弹跳等动作分开。当需要一个例如足球时(它不是球形),这种设计更改可以提高应用程序的可扩展性。
其次,也许更难争论的是,“直径”这个概念应该被抽象。为什么?因为这个值的含义存在不可预见的依赖关系。例如,它可以从半径计算得出。球的周长或体积可以从直径或半径计算得出。或者给定体积,可以计算直径。通过使“直径”的概念更灵活,应用程序也变得更灵活。因此,直径不应仅仅是一个整数类型,而应该是一个对象本身。
列表 13:“直径”成为一等公民
public class RoundBallMetrics
{
private double d;
public double Diameter {get {return d;} set {d=value;}}
public double Radius {get {return d/2;} set {d=value*2;}}
public double Circumference {get {return d*Math.PI;} set {d=value/Math.PI;}}
public double Volume
{
get {return (4.0/3.0)*Math.PI*Math.Pow(d/2, 3);}
set {d=2*Math.Pow((3.0*value)/(4.0*Math.PI), 1.0/3.0);}
}
public double SurfaceArea
{
get {return 4.0*Math.PI*(d/2)*(d/2);}
set {d=2*Math.Sqrt(value/(4.0*Math.PI));}
}
}
(注意,我没有测试这些方程的准确性!)
为稍事打岔,我们现在有了一个不错的类,可以使用接口(是的,接口!)来让任何实现球的度量标准的类都必须实现这些函数。但我们必须小心,因为有些球不是圆的,比如足球,它有一个长轴和一个短轴,这意味着直径、半径和周长根据轴的不同而不同。通过使用接口,我们可以指定某些函数需要实现,同时将实现留给应用程序的需求。仅解决圆形球的问题,接口可能如下所示。
列表 14:一些有用的球度量标准接口
interface I3DMetrics
{
double Volume {get; set;}
double SurfaceArea {get; set;}
}
interface I2DMetrics
{
double Radius {get; set;}
double Diameter {get; set;}
double Circumference {get; set;}
}
// ooh, look. Sort-of multiple inheritance!
interface IBallMetrics : I2DMetrics, I3DMetrics
{
// adds nothing
}
public class RoundBallMetrics : IBallMetrics
{...}
此外,这个类现在还可以实现流式传输、与Forms设计器集成以及其他有用的功能(其中大部分会使类与其他对象纠缠——权衡,总是关于权衡)。
回到重点。我们现在有了一个处理圆形球直径的良好类。IBall
接口现在只需要实现一个getter函数,该函数返回一个一等公民的球度量标准。这允许不同的球形和尺寸。
列表 15:可能的球实现
interface IBall
{
IBallMetrics {get;}
}
public class RoundBall : IBall, IBounceAction
{
private RoundBallMetrics rbm;
public IBallMetrics {get {return rbm;}}
public void Bounce() {}
}
既然我们已经创建了一个处理其度量的抽象球,那么关于接口的默认数据和默认实现的问题就解决了——根本不存在!永远不存在!如果你想要默认属性或实现,那么你必须在基类中实现该行为,并从该类进行特化,这会在单继承和多继承语言中导致各种问题。所以经验法则是不实现默认属性和方法。
总结本节,我只能说,我希望我已经充分展示了在管理属性、对象、接口、继承、分解和集合方面所涉及的技术挑战。每个应用程序在这方面都是一个独特的挑战。
关系咨询
不同家族之间的关系与你家族内的关系同样重要。可悲但真实,与其他家庭扯上关系通常意味着你会陷入他们的政治斗争,并经常被要求“选边站”,而自己(他们以为!)是一个公正的观察者。
“独生子女”综合症
在对象模型中,我们有父母和孩子,但没有兄弟姐妹。因为对象模型是层级结构,所以它们是对关系的残缺模型。这会让程序员陷入各种麻烦,当必须创建家族之间的关系时。考虑以下层级及其彼此之间的关系:
对象模型在表示层级结构方面做得很好,但在C#或C++语言的语法中,它们并没有解决对象之间关系的问题。为此,你必须研究诸如设计模式之类的东西。一个与你的兄弟姐妹交谈的有用的设计模式是电话。给他们打电话(他们可能也不会接听),然后留个信息。
婚外情会导致纠缠、离婚和赡养费
消息传递是避免纠缠的绝佳方式,但它也需要一个基础设施,其中必须处理同步、工作流和超时(并且它也很适合工作线程)。例如,当登录过程向数据库发送消息查找用户时,它必须等待响应,或者在一定时间内没有收到响应时超时。其他设计模式也能在同一线程中解耦或解纠缠对象,从而无需处理同步及相关问题。
纠缠会导致设计死亡。当我看到一个纠缠的应用程序时,我通常会告诉客户,重写比重构更容易。纠缠的对象关系必须尽早重构,才能挽救项目。对象必须彼此“离婚”,这通常会给双方带来很大的费用——律师费、法庭诉讼等。最好的办法是最初就不要纠缠。例如,使用消息传递系统可以保持一种熟人关系,而不是一种亲密的爱情关系。是的,我知道,这很无聊。
哲学问题
除了人的问题和技术问题之外,OOP还有很多哲学问题,主要与现实世界的工作方式有关。
抽象与信息丢失
抽象是泛化,当发生泛化时,信息不仅被移动,实际上被丢失了。抽象的危险之一是,通常包含在结构中的信息会在结构被抽象时丢失。取而代之的是,所有信息都在数据中。虽然这使得程序更灵活(因为它能处理不同类型的数据),但也使得对象更晦涩——它们应该如何真正使用?
表示性抽象
C#(是的,Java也是)中最终的表示性抽象是object
。在C++中,它是void
指针。一个对象可以是任何东西,因为说
object o;
完全没有信息量,所以需要“反射”的概念,以便o
可以知道它是什么。
列表 16:获取对象类型信息
object o=new RoundBallDiameter();
Type t=o.GetType();
Console.WriteLine(t.ToString());
答案:interfaceExample.RoundBallDiameter
模型抽象
层级结构会产生抽象,但在这里信息丢失并不像关键,因为它不是概念被抽象,而是概念的容器。被抽象的容器仍然保留了关于对象可以做什么的信息。例如,在这个层级结构中
Window
类相当abstract
,但它仍然可以包含用于操作的方法,例如
- 位置
- 文本
- 选择事件
- 背景颜色
- 边框样式
- 等等。
因此,即使容器本身是抽象的,上下文信息也得以保留。
概念抽象
概念抽象是指你正在实现的那个概念被抽象了。而不是在层级结构中表示抽象,而是使用集合在一个容器中表示抽象,以管理概念的属性和方法。这种对象可以代表任何东西,包括其他对象。从平凡的,例如Matrix
类
列表 17:Matrix类
public class Matrix
{
private ArrayList columnList;
private int cols;
private int rows;
public ColumnList this[int col]
{
get
{
return (ColumnList)columnList[col];
}
}
...
}
public class ColumnList
{
private ArrayList rows;
public object this[int row]
{
get
{
return ((Cell)rows[row]).Val;
}
set
{
((Cell)rows[row]).Val=value;
}
}
...
}
它可以用来管理任何二维数据(类似于RecordSet
),到荒谬的(但有时仍然是必要的)
列表 18:抽象对象概念
public class AbstractObject
{
public string name;
public Collection methods;
public Collection properties;
public Collection events;
}
概念抽象会丢失关于概念本身的所有信息。虽然强大,但通常效率不高。但有时是不可避免的。当我从事一个自动化卫星设计项目时,我被迫实现了一个高度抽象的模型——类别-属性-单位-类型-值(CAUTV)模型,以充分捕捉所需的概念。
在这个模型中(实际上,这只是一个更复杂模型的片段,涉及组件、自动单位转换、组件等),一件设备属于一个类别,例如行波管放大器(TWTA)。TWTA具有属性,如质量、功率、热损耗、放大率和噪声。每个属性都以特定的默认单位计量,例如千克、瓦特、毫瓦等。每个单位可能有一个或多个类型——最小值、最大值、标称值、平均值等。然后,设备的一个特定实例由一个值组成,该值结合了单位、类型和属性。由于工程师无法量化这些信息(事实上,它根据设备制造商而异),因此必须构建一个非常抽象的模型来支持这些需求。
正如应该显而易见的那样,这种抽象导致了上下文信息的完全丢失。像这样的模型可以很容易地用来描述卫星设备,也可以用来描述奇幻角色扮演角色。这也使得传达模型的应用变得更加困难。这种抽象需要“通过示例解释”才能理解模型所属的上下文。不那么抽象的模型通过“通过定义解释”来传达它们的意图。
抽象致死
过度抽象是完全可能的。我的经验法则是考虑一个具体的概念,然后考虑前两个抽象级别。通常只有一个抽象级别就足够了,并且有用。超越第一个抽象级别可能会导致关于抽象对象适用的混乱。最糟糕的是,我有时会发现自己抽象了一个抽象,结果却发现除了类名之外什么都没改变!
没有水晶球
编程就像预测未来。我认为有两个预测领域——功能领域和实现领域。为一个应用程序制定功能集是对未来将有用的事物的预测。我曾经看到一个统计数据,90%的人只使用文字处理器10%的功能,但根据不同的人来看,这10%是不同的。因此,预测客户的需求,尤其是在世界其他地方飞速变化的情况下,是困难的。但是水晶球还有另一个用途。它必须告诉程序员代码未来将如何被使用。所以实际上,程序员必须在两个领域做出明智的猜测——对客户来说什么重要,以及对他的同僚来说什么重要。第二个领域我将详细说明。
未知的领域
编写代码,无论意图是否使其可重用,都需要预测未来。在预测代码将如何被使用时,有许多考虑因素,包括设计问题、实现问题和通信问题。编写代码时可能需要考虑的一些事项:
- 输入是否经过验证?
- 输出是否经过验证?
- 我能依赖我正在交互的组件吗?
- 我是否考虑了代码可能使用的所有方式?
- 设计和实现是否足够灵活,能够处理需求变更?
- 它的文档是否足够,以便我或其他人能够弄清楚我做了什么?
- 代码能否在下一次技术飞跃(硬件和软件)中得以生存?
其中许多答案需要做出权衡。有时,会做出错误的权衡,但只有在事后看来。谁能想到COBOL程序员的代码在本世纪初仍然在使用,当时节省一年记录中的两个字节加起来节省了大量的磁盘空间?到2038年,当所有硬件时钟重置为1970年时,有多少旧PC(即使按今天的标准)仍然可以运行?当我于1987年在DOS中编写远程视频监控软件时,谁能想到日本警察部门会成为我们最大的客户,并且他们想用日语编程?Unicode和16位字符甚至不存在,Windows 3.1也不是高性能图形应用程序的环境!而且,当处理器和图形性能足够好可以将代码迁移到Windows时,有多少代码是可重用的?
消费者
预测技术变化和应用程序的生命周期是一个问题,但另一个问题是预测同僚的需求。程序员是生产者——他们创造东西。其他程序员是消费者——他们使用其他程序员编写的东西。作为一名顾问,我就像吃自己尾巴的蛇——我消耗了我生产的许多东西。预测我将来需要什么,并编写足够好的代码,以便我将来自己编写的东西有用,这已经很难了!想象一下还要为别人这样做。但这就是我们不断尝试做的事情。在某种程度上,这背后的心理是,我们希望在项目完成后,自己的一部分能够继续存在(一种小型的死亡)。这也是一种挑战——我能写出别人觉得有用并能在未来使用的东西吗?这也是对自尊心的满足——我做的事情被别人使用了。
所有这些因素都掩盖了一个事实:消费者(下一个程序员)想要自己的成就,自己的挑战,自己的虚荣心得到满足。那么会发生什么?旧代码被批评、谴责和抱怨(戴尔·卡耐基的三C),随后是热情的“我能做得更好”的声明。也许是真的,也许不是。但事实仍然是,程序员,作为一种不善社交的群体,在实现重用方面实际上是激烈的竞争者。
现实很残酷
除了重用的心理学之外,重用失败还有很多好原因。如果你想知道我为什么谈论重用,那是因为这是对象技术应该帮助解决的“副作用”之一——对象是可重用的,可以降低开发成本和调试成本。
差异太大
代码实际上有两个层面——应用程序特定和应用程序通用。重用只会在应用程序通用领域发生。当应用程序代码高度纠缠和/或抽象性差时,重用量将非常少,因为对象与应用程序特定域的耦合太紧密。当然,这不是对象的错,但对象本身并不能防止这种情况。然而,对象可以用来解耦代码,例如实现设计模式、创建抽象以及使用代理和外观包装MFC和.NET等框架实现。我发现能产生高重用性的方法是为各种对象组件化,并使用一个框架,在该框架中编写简单的脚本来协调所有组件之间的数据流。这就是文章开头图表中的“Aspect Computing”概念。
概念无法转化为实现
概念很难转化为实现,因为概念没有解决实现所需的那种低级细节。因此,无法预测应用程序需要多少时间来开发。当然,可以产生高度详细的设计,但我一遍又一遍地发现,实现常常会暴露设计中的问题。设计在纸面上可能看起来很棒,但实现会显示出问题。这不仅仅是设计,甚至延伸到概念本身。我曾多次发现概念中的缺陷,因为在实现过程中,我能更好地理解真正要解决的问题。有人可能会争辩说这是沟通不足、培训不足、文档不足、在解释概念上投入的时间不够等等的结果。这可能是真的,但我要反驳的是,当实现开始时,无论概念定义多么完善,设计对概念的支持多么好,都会出现问题。
数据纠缠
就像对象之间会发生纠缠一样,对象与驱动对象的数据之间也会发生纠缠。例如,在AAL中,XML模式(XSD)与组件功能之间存在深刻的纠缠。还存在其他依赖关系——正如CodeProject用户Brit指出的那样,一个对话框依赖于对话框布局、string
表、位图和资源ID。所有这些都导致对象无法移植。同样,这不是对象的错。然而,它深刻地影响了与对象相关的问题。
项目约束
这应该是个显而易见的问题,所以我只是为了完整性而包含它。有时重用、解耦、抽象、设计模式、重构等等,都会屈服于项目的约束,通常是时间和金钱。项目上的拉扯由功能、资源(时间、金钱、人员)和质量这三个三角形表示。
Atlas 设计
根据大多数世界概念,有个人在天上支撑着大地。对希腊人来说,那个人是阿特拉斯。阿特拉斯可以被视为应用程序设计赖以生存的框架。许多应用程序没有一个包容性的框架,或者它们完全依赖于现有的框架,如MFC、COM、CORBA、ActiveX和/或dot-NET,以及无数其他框架。根据我的经验,很少有人考虑“Atlas Design”这个概念——一个构建应用程序的元设计。可能缺乏专业知识、远见或资金。在一间公司,远见和资金都有,但缺乏专业知识,结果却是一个真正令人窒息的框架,程序员们想方设法绕过它,因为它太难用了,而且很烦人。在我其他的文章中,我写过关于应用程序自动化层(Application Automation Layer)的文章。AAL是一个“Atlas Design”——一个所有其他框架都可以在其上运行的框架。本质上,它将那些框架创建为组件,并且常常将那些框架的子集创建为组件。
结论
好吧,对象本身并没有什么真正的问题。它们解决了特定的问题,而且做得相当好。但也许我们对对象的期望过高,期望它们能提供比它们能力范围更多的解决方案。或者,也许软件开发这个领域还太新,以至于没有人真正知道如何做好。很明显,问题不在于对象本身,而在于应用它们的人以及它们被要求执行的任务。如果我想赢得Indy500,我不能用大众甲壳虫做到。是时候看看谁在支撑着世界了。