面向对象理论很早就被提出了,但它真正地渗透到软件开发的各个领域,并且在软件开发实践中大规模应用,却要等到上世纪九十年代。到目前为止,面向对象技术已是软件开发的主流,全面取代了结构化编程技术曾经具有的地位。
面向对象技术与结构化编程技术有着不同的风格,但同时也有着密切的联系。从具体编程角度来看,面向对象技术与结构化编程技术很难截然分开,两者的根本差别在于思维方式。
要了解面向对象技术,得从结构化编程技术入手。
1.1、结构化编程引例
结构化编程在上世纪七十年代兴起,由于它具有很多的优点,出现之后没几年就占据了软件开发的主流,大家熟悉的C语言就是一种典型的结构化编程语言。
结构化编程的基本方法是“功能分解法”,具体来说,就是将要解决的实际问题进行分解,把一个大问题分成若干个子问题,每个子问题又可以被分解为更小的问题,直到得到的子问题可以用一个函数来实现为止。
我们先从一个简单的编程任务开始,逐步体会结构化编程与面向对象编程的不同风格。请看以下的这道编程题目:
请编程计算出1999年5月10日到2006年3月8日期间一共有多少天?
这是一个简单的算术问题,尽管如此,为了做个铺垫,我们还是把计算方法再叙述一下,以帮助读者理清思路。
有以下注意事项:
①、一年有365天,但闰年有366天;
②、一年有12个月,大月31天,小月30天;
③、2月最特殊,普通年份28天,闰年29天。
综上所述,计算步骤如下:
(1)计算从1999到2006期间一共有多少个整年:
2000、2001、2002、2003、2004、2005,共有6个整年,其中2000和2004年是闰年,因此,共有6*365+2=2192天。
(2)计算从1999年5月10日到年底(即12月31日)共有多少天:
5月10日到12月31日中共有4个整的大月(7、8、10、12月),三个整的小月(6、9、11月),共计4*31+3*30=214天。
5月10日到月底(31日)还有31-10=21天。
所以,1999年5月10日到年底共有214+21=235天。
(3)计算从2006年1月1日到2006年3月8日期间一共有多少天:
1月有31天,2006年不是闰年,2月有28天,所以,总共有31+28+8=67天。
综上所述,1999年5月10日到2006年3月8日期间一共有2192+235+67=2494天。
事实上,上述计算过程其实就是一个计算机算法,由于步骤很明确,可以很容易地将这一过程转为程序。
编程之前,先将实际问题抽象为以下模型:
输入:两个日期 => 程序 => 输出:两个日期间的天数
我们要完成的工作就是开发出这样一个程序:程序接收两个日期值,经过计算之后,输出这两个日期之间的天数。
(1)结构化分析过程
为了清晰地表达出程序需要处理的信息,先定义一个结构体类型:
结构体类型MyDate其实是定义了一种数据结构。我们正是在这个数据结构之上构建出整个程序的。
对模型进行结构化分析的第一步,是将“程序”完成的功能转化为由一个函数CalculateDaysOfTwoDate()实现:
//计算两个日期中的整天数
static int CalculateDaysOfTwoDate(MyDate beginDate, MyDate endDate)
{
//……
}
余下的开发工作体现为给CalculateDaysOfTwoDate()编写代码实现日期计算的功能。
在结构化编程中,有这样一个重要的公式:
程序=数据结构+算法
数据结构代表了要处理的信息,而算法则表明要对这些信息进行哪些处理工作。
只要确定了数据结构和算法,一个程序就成型了。因此,将程序中要处理的数据抽象为某种数据结构是结构化编程的基础。
在本例中,算法非常简单,可以直接将人计算过程中的每一个步骤转为一个函数,由此得到以下两个函数:
//计算两年之间的整年天数,不足一年的去掉
static int CalculateDaysOfTwoYear(int beginYear, int endYear)
{
//……
}
//根据两个日期,计算出这两个日期之间的天数,不理会中间的整年
static int CalculateDaysOfTwoMonth(MyDate beginDate, MyDate endDate)
{
//……
}
第一个函数根据两个年份之间的整年数计算出天数,第二个函数根据月和日计算出两个日期之间的天数(不理会中间的整年数)。
在深入的考虑这两个函数的具体实现算法时,会发现它们都需要判断一年是否是闰年,于是设计另一个函数IsLeapYear()完成此功能:
//根据年数判断其是否为闰年
static bool IsLeapYear(int year)
{
//……
}
这样,函数CalculateDaysOfTwoYear()和CalculateDaysOfTwoMonth()在需要的时候即可调用IsLeapYear()函数来判断是否某年为闰年。
至此设计工作完成,得到以下结果:
CalculateDaysOfTwoDate
CalculateDaysOfTwoMonth CalculateDaysOfTwoYear
IsLeapYear
以上展示了结构化分析得到的设计方案,箭头表示函数调用关系。
在整个结构化分析过程中,我们采用的是先设计出最顶层的CalculateDaysOfTwoDate()函数的接口,再设计第二层的两个函数CalculateDaysOfTwoMonth()和CalculateDaysOfTwoYear(),最后抽象出第三层的函数IsLeapYear()。
有了设计图,即可动手写代码。现在有四个函数需要开发,如何确定开发顺序?
很明显,必须先开发IsLeapYear()函数,因为此函数被其它函数调用,但它不调用其他的函数。
接着可以开发CalculateDaysOfTwoMonth()和CalculateDaysOfTwoYear()两个函数,因为CalculateDaysOfTwoYear()函数比较简单,所以先开发它。
最后开发CalculateDaysOfTwoDate()函数。
上述开发次序的确定可以用两句话来表达:
(1)盖楼先打地基:先开发最底层的函数,因为不完成开发这些函数,调用它们的上层函数就无法运行。
(2)柿子捡软的捏:在同一层次的函数中,先开发简单的,再开发复杂的。
函数开发完成之后,以下代码调用最顶层函数CalculateDaysOfTwoDate()完成计算两日期之间天数的工作:
MyDate d1, d2; //起始日期和结束日期
//1999年5月10日
d1.Year = 1999;
d1.Month = 5;
d1.Day = 10;
//2006年3月8日
d2.Year = 2006;
d2.Month = 3;
d2.Day = 8;
//计算结果
int days = CalculateDaysOfTwoDate(d1, d2);
现在可以对结构化编程方法做个小结。
① 结构化软件系统的基本编程单位是函数。
② 整个系统按功能划分为若干个模块,每个模块都由逻辑上或功能上相关的若干个函数构成,各模块在功能上相对独立。
③ 公用的函数存放在公用模块中,各模块间可以相互调用,拥有调用关系的模块形成一个树形结构,这种调用关系应尽可能做到是单向的。
结构化软件系统的架构:
主控模块
↓
子模块 ↓ 子模块
↓
公用模块
结构化编程的开发过程可以分为以下三个阶段:
(1)分析阶段:在编程之前,需要仔细分析要解决的问题,确定好数据结构与算法。
(2)设计阶段:结构化编程的基本单元是函数,每个函数都完成整个程序的一个功能,整个设计过程就是函数接口的设计过程,这是一个“自顶向下,逐步求精”的过程,将一个大函数不断分解为多个小函数,直至可以很容易用某种程序设计语言实现时为止。
(3)编码阶段:在开发时,根据在设计阶段得到的函数调用图,先开发最底层的函数,再开发上层函数。这是一个“自底向上,逐层盖楼”的方法。
结构化编程中“自顶向下,逐步求精”的“功能分解法”,是一种重要的软件开发方法,其本质是一种“分而治之”的思维方式,在面向对象的程序中也有广泛的应用。掌握这两种分析方法,对软件工程师而言是非常重要的。
(2)面向对象分析过程
有了结构化分析的基础,可以很容易的将原先结构化的程序转为面向对象的程序。
创建一个CalculateDate类,作为上面结构化分析得到的四个函数的“新家”,如图:
由于外界只需要调用CalculateDaysOfTwoDate()一个函数,所以将此函数设置为公有(public),而其它三个函数则成为类的私有(private)成员,外界不可访问。
以下为调用此类完成计算两个日期间天数的代码示例:
MyDate d1, d2;
//MyDate d1, d2; //起始日期和结束日期
//1999年5月10日
d1.Year = 1999;
d1.Month = 5;
d1.Day = 10;
//2006年3月8日
d2.Year = 2006;
d2.Month = 3;
d2.Day = 8;
string str = "{0}年{1}月{2}日到{3}年{4}月{5}日共有天数:";
str = String.Format(str, d1.Year, d1.Month, d1.Day, d2.Year, d2.Month, d2.Day);
CalculateDate obj = new CalculateDate();//创建类CalculateDate对象obj
//调用对象obj的CalculateDaysOfTwoDate方法计算
int days = obj.CalculateDaysOfTwoDate(d1, d2);
Console.WriteLine(str + days);
对比前面结构化的程序,不难发现面向对象的程序具有以下几个特点:
(1)所有的函数都放入到一个类中,成为某个类的成员,类是编程的基本单元。
(2)外界不能直接调用类的成员函数,必须先创建一个对象,再通过对象来调用这些函数。
(3)只有声明为public的函数可以被外界调用,其余声明为private的函数是私有的,外界无法访问。
从这个实例可以看出,面向对象程序与结构化程序相比没有明显的优越性,而且显得更麻烦,但如果是大规模的软件系统,则面向对象程序就有着结构化程序不可比拟的优势,简单地说:
对于大规模的系统,采用面向对象技术开发可以达到较高的开发效率与较低的维护成本,系统的可扩展性也更好。
其实.NET Framework本身就提供了两个类DateTime和TimeSpan可完成同样的功能:
对比一下,显然使用.NET Framework提供的现成类比我们手工编写代码完成同样的工作开发效率要高得多。.NET Framework中所提供的现成代码都是以面向对象的形式封装的。实践证明,当需要大规模的复用代码以提高软件生产率时,面向对象比结构化技术更有效。