类及类层次关系设计

本文详细讨论了面向对象设计中的类、继承、模板和接口。建议使用抽象基类而非普通基类,接口用于规定行为需求,模板用于提高类的通用性。还介绍了内部类、枚举、结构体的使用场景以及错误处理中的异常和返回值策略。

继承、模板和接口

继承

一般情况下,大家使用继承一般有以下几种情况:
(1)数据结构方面:比如定义一个 LinearList 作为基类,再定义一个 SeqList 和 LinkedList 作为子类。
(2)应用层面:Manager 继承自 Employee;MiniVan 继承自 Car 等。
(3)语言层面:提供一个公共基类 Object。

我们逐个分析:
(1)对于案例一,我们提供一个 LinearList 的目的是我们可以不考虑传入数据的内部实现。更好地做法应该是设计一个迭代器接口。
(2)对于案例二,我们觉得 Manager is a Employer。但实际上,他们相同的东西很少,不过还是一种比较合理的设计。我觉得将 Employer 设计为虚基类是最好的。
(3)对于案例三,我们希望设计处理的类可以通用程度更好(参数可以使用 Object)。不少语言都实现了一个 Object 类,但方式有所不同,比如 ruby,虽然其定义了 Object 类,但 Object 类并没有定义什么方法,而是从 Kernel 模块中继承了一些东西。这个与 C++ 的一些设计思想有所相似,C++ 中一般提供的类其上都会有一个 basic_? 类,而我们直接使用的类一般也没有定义什么新的方法,使用这种做法进行隔离以便以后扩充和维护。

对于 Object,我们一般会做什么呢?函数参数。
而现实情况下,很多人就会过分依赖反射,但我必须强调,如果你是用来调试和分析程序,这是很好的,如果是用于一般的程序设计,就该强烈批评了。

如果是为了设计一个通用的工具类,那么我们往往对参数有一些要求,这种情况下,最好选择接口参数。当然,有时我们只是要求传入的参数具备某一特征,比如都是数值类型,那么有一个命名为 Value 的抽象基类当然是最好的,否则通过接口限定带有一个 GetValue 函数也是较好的。
如果对传入类是没有行为的要求,但类有一定的关系,比如三角形,长方形都是多边形,那么提供一个抽象类是比较好的。为什么强调是抽象类,原因是抽象类能把附加影响降到最低。

模板

常见于系统类库,同样也是为了让类的通用程度更高。
最有名的莫过于 STL 库了,其它的基本是属于模仿级别。
C# 基于类型安全的考虑,模板的使用比 C++ 的范围会小一些。

接口

接口本质上是函数对象在类级别上的实现方式,同时,他也是契约式编程思想在面向对象层面上的体现。
为什么会诞生接口呢,有两个原因:
(1)有些参数的需要满足一些要求,以便于一些方法的实现,比如排序函数需要知道对应类型的大小关系如何确定,这样就需要参数提供一个供比较大小的方法。
(2)模板参数是没有类型推断能力的,为了类型安全,一般不允许使用到类型转换的功能,那么我们就不能对该类型有关的参数进一步操作了。想想,如果提供了一个结构,声明传入的模板参数是支持某些方法的,那么我们不仅无需类型转换,又能实现实际的需求,并且这是合法的。

然而,由于继承思想使用泛滥,一开始,接口便被打上了继承的烙印。此外,由于是静态语言,一旦封装成包,和模板一样,别人就不好往里面添加其它的了。比如在 C# 里,表示某个类实现了某个接口,就需要继承某个接口,以至于,接口都不允许有变量声明存在(虽然这个不是大问题,可以接受);在 Java 里,实现 Cloneable 接口意味着 Java 换一种方式强制让你重写 Clone() 函数,当然这是 Object 类的设计问题。顺便提一句,Java 的 Object 类也提供了一个 equal() 函数,同样也是一个设计问题。这也是为什么 Go 设计为鸭子接口的原因。比如生活中,插座是支持风扇、电视等使用的,但你不能说风扇、电视都是用电器,就提供一个用电器基类。风扇、电视等有各自的工作方式。而提供一个接口,那么就合情合理了。

小结

总体来说,不要使用普通基类,而是抽象基类;
抽象基类用于有关系的类参数,接口用于有行为要求的类参数,模板可用于一些算法的设计,如果不需要类型转换,是比较好的,可以专有化,又是编译期处理的。

内部类、枚举、结构体

内部类

不得不说,Java 总是喜欢乱设计。
一般情况下,内部类(嵌套类)一般有几种用法:
(1)存放某一类成员变量,目的是操作更方便、更有条理,此时有点类似于结构体。之所以列入结构体,是因为除了 C++ 其他语言的结构体都是与类差不多的了。当然,内部类可以有自己的执行逻辑,因此,我们可以将一些东西放进去,达到隔离效果,又能简化操作和程序设计。
(2)错误处理。可能这点大家会比较陌生,如果大家看过设计模式,相信会有一些了解。我们放到下面说。

在情况一中,一般使用的是私有的内部类,因为有些逻辑是不该外部使用的,如果设计为公有的,还不如考虑将它放到外面来,在里面定义一个私有类变量好。是不是真的没必要将其定义为公有的,答案是否定的。如果你需要明确的表示某两个类之间的关系,或防止与其他的类冲突,而且用户需要用到该类,定义一个公有内部类是比较好的做法。

枚举

一般用来存储一些状态信息。

结构体

一般用来存放一组有关联的变量,同样是优化逻辑和方便操作。

错误处理

异常处理

自 C++ 起,很多语言都会用到异常处理,尤其是 Java,可以说是 Java 里的异常是泛滥成灾了。几乎每个类都有异常,随便翻了个文件,数了下,同个异常用了 4 处,抛出了 11 次。大概看了下,不考虑注释,基本每十行一个异常。

而 C++ 呢,整个类库,异常也不超过 10 个,并且都是不到非不得已,是不抛出异常的。

事实上,Java 有几个根深蒂固的设计问题:
(1)滥用继承;
(2)滥用 protected;
(3)滥用异常。

异常的使用一般是:
(1)遇到非逻辑错误,是外在条件导致的非人物能够预期的错误;
(2)系统异常,这种一般是积累性的,比如内存不足,我们没法自己处理。偶尔一些系统我们是能够处理的,那就应该捕抓异常。
(3)用户的错误操作导致的,比如将对 XP 系统依赖较大的程序放到 win7 运行,缺乏相应的系统支持,导致抛出异常(不过这种情况可以通过加一段环境监测,友好退出)。又比如某个驱动模块被用户删除,找不到对应模块无法执行。

无论是哪一种,都是无奈之举。

返回值

这是从 C 带来的,不过,很多人都不怎么去检查函数的返回值,错了都不知道。或许,这也是 java 为什么使用了大量的异常而只有少部分通过返回值指示错误。如果用户不检查,很多情况下会导致运行期错误。Go 使用了多返回值的做法,也是考虑到能让用户既获得想要的东西,也可以同时检查运行是否出错。C# 不也提供了一个可空类型的概念。

大部分通过返回值表示错误一般是使用 false、-1、非零、枚举值、null。

null object

这是设计模式里提出的一种思想。既然大家都不检查错误,那就提供个方法,错了也正常执行。
做法就是在类里定义一个名称为 null 的只读量(只能在构造函数中初始化),操作的类型是什么,变量的类型就是什么,因此错误信息由我们提供,如果越界操作或者引用到不存在的对象,就返回 null 的引用。这是一种友好的方式提示错误并且是透明化的。

其他

当然,语言的支持不只如此,委托(函数对象在 C#、Java 里的称呼,目的是差不多的)、事件、lambda、yield、多线程等的运用可以对程序的结构和组织进行更好地处理。(某种程度上是对流程结构的补充扩展,有效简化了程序逻辑)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值