>面向对象之数值的悲剧
面向对象之数值的悲剧
上午的时候里团队里开了一个会,说了一点开发技术问题,在这里同大家分享一下我的一些想法。
枚举的悲剧
枚举基本用法大家应该都很熟悉,在我们去避免硬编码,增强代码可读性,可维护性方面很好用。不过话说枚举底层还是比较复杂的,以前看过一些关于枚举的说明。至少C#里边的枚举是很复杂的,本身是结构类型(值类型)的,而却继承与类System.Enum(引用类型),而引用类型System.Enum派生与值类型System.ValueType。够乱吧,其实话说我也没搞很明白,所以大家千万不要随随便便就说我很了解某个东西之类的话。
好吧,转入正题(写东西喜欢扯,没办法 ╮(╯▽╰)╭)。
先说一下枚举所带来的问题。例如,我们有个类型变量,需要存储到数据库中。当然存储到数据库中的时候,表里边存的肯定还是数值(因为枚举终究还是数值)。
例如定义枚举
public enum Language
{
SimplifiedChinese,,
English,
French,
Japanese
}
然后假定我们有一个用户的名单,这里边有个字段记录这该用户所使用的语言。那么数据库中记录的就可能是 A 0 B 1 C 3 D 0 E 0 F 1 。OK,现在我们需要添加一种语言,我们在枚举中要加一个繁体中文TraditionalChinese,我想很有可能代码会变成成这样子。
public enum Language
{
SimplifiedChinese,
TraditionalChinese,//新添加的繁体中文
English,
French,
Japanese
}
好吧,悲剧就这样诞生了。
数据中所有原来已英语为语言的人都改为繁体中文了!!是不是很崩溃,而且,这个bug的代码错误还隐藏的如此之深,是一个枚举量的顺序所导致的。因为TraditionalChinese 和 1具有了对应关系。
当然这个问题解决起来不是那么复杂,目前,如果不变枚举,有两种方法:
- 直接在添加新的枚举量的时候,补在Japanese后边,不改变原来的顺序
- 硬性指定各个枚举的值
public enum Language
{
SimplifiedChinese=0,
TraditionalChinese=4,
English=1,
French=2,
Japanese=3
}
我不会誓死阻止你使用第一个方法,但是很不建议,因为你无法防范你的朋友不这样做或者忽略这么一条怪异的理由。对于第二个方法,比第一方法来说会好一点(可是看着还是有那么一点不爽),或者,你可以加一个标注“各个已有的值不可修改”╮(╯▽╰)╭
其他解决方案
- 既然是由于数据库中存储数值所导致的,那么我们为什么不直接把枚举.ToString()存进去呢?取出的时候,再做一次强制转换,这样不仅解决了上述问题,而且还是在查看数据库是可读性也增强了不少。
- 换掉枚举
这里说一下换掉枚举,即枚举的替代
我们可以使用静态常量来代替枚举。如下
public static class Language
{
public static readonly string SimplifiedChinese = "SimplifiedChinese";
public static readonly string TraditionalChinese = "TraditionalChinese";
public static readonly string English = "English";
public static readonly string French = "French";
public static readonly string Japanese = "Japanese";
}
调用的时候,也可以向枚举一样,Language.English,可读性和可维护性都是不错的。我们还可以定义一些方法,实现枚举类似的功能。我们也可从.NET的一些设计上看到这些影子 如WebRequestMethods.Http.Put 等为const string(我还没有去想用const string 和 static readonly string 的差异,不过实现思想一样)
。在Android中一些设计上也有类似的思想,例如R类的设计。
那么,在数据库中存储的也就是这些变量对应的字符串,这个地方和上一种方案是相同的,变数值为字符串。
关于枚举就暂时说到这里。
索引的悲剧
问题的起因在于,我们要做一个菜单,菜单都有索引,比如,最初 索引 0 1 2 3 对应的操单操作是 增,删,改,查。在代码中 我们很肯能会依据索引值来判断要执行什么操作(啊,这是多么危险啊)。后来又多一个操作为 清空数据。我们想把它放在菜单项的第一个位置,这样,原有的索引已被打乱,0 1 2 3 所表述的意义已经发生变化了,导致出错。同样的,做一个下拉列表也是同样的问题。解决方案不复杂,通常这里边会提供一个value,我们在创建item 时,会用这个value来唯一标识该item.这样,在添加新数据时,他也有自己唯一的value。当然这个value做的时候,也有一些代码的技巧。比如我们
先定义一些常量,让他们的名字分别标识每个item,然后,将这些item的value赋为这些变量。
像是这样:
private readonly string MenuItemAdd = "0";
private readonly string MenuItemDel = "1";
private readonly string MenuItemUpdate = "2";
private readonly string MenuItemSelect = "1";
private readonly string MenuItemClear = "1";
MenuItem item = new MenuItem("增?加", MenuItemAdd);
MenuItem item = new MenuItem("增?加", MenuItemDel);
当然你可以用一个类去维护这些readonly string(做这个类的时候,千万不要就是一个真正的单独类,这样化,你的项目中必将有很多零散的小类,也会使你崩溃,假如你做的是一个Android 应用程序,或许你可以定义ActivitiesStr类,里边在定义一个 DataMangActivityStr类,这个类里边再写DataMenuItemValueStr类,这样统一维护会好点,至少,我这么认为。)
Ok,这些问题讲完了,大家会说这与面向对象有关系么?还什么面向对象之数值的悲剧,这不是标题党么?
我们来分析一下这些上述问题是怎么出现的,他们的共性是什么
抽象分析
面向对象与数值的悲剧
上述两个问题,很明显,很有 相同之处,基本上为 我们原要以一个数值代表某种含义,然而由于某种改变,这个数值本省代表的含义发生了变化,这样导致出现错误。细想一下,其实我们这种想法还是很普遍的,做标记啊,什么的,传参数是定义1 标识执行A,传2时执行B,或者男为1,女为2,终究都是我们用一个数值(一个不具有实际意义的东西)去代表另外一个具有实际意义的东西。我们又为什么要用 一些不具有实际意义的1 2 3去代表 具有实际意义的物体呢?数字使我们发明出来描述客观世界的没有错,比如说,我们说这里有几个人,有几张桌子,这都没有错。但是,男,女问题,操作类型问题,他们本身根本不具有数值的意义,只不过使我们在为了方便标识二者时候,给他们的代号,在现实世界中,这个对应是根本不存在的。换句话说,这些都是我们人为强加给这些事物的性质,一旦发生改变,我们临时加给这些事物的性质就无法对抗新的变化,导致错误的出现。我们只是在写程序,而不是在描述这个世界,所以,我们的程序永远应对不了这个世界的变化。可我们是如何解决这些问题呢?
想想为枚举量强制赋固定数值的方案,English怎么变,代表的都是英语,然而1 这个数字是无辜的,他要么代表一个人,一张桌子,是一个代表数量东西,与英语没什么关系。所以,一旦后来的人不小心改了以前的某个枚举量的值(因为他不知道这个规则啊,他不可能想像1 与英语的关系,更不能想象把1改成2了英语就变成法语了)。如果我们用一些具有实际意义的string来标识,string 本身就是一个描述性的东西。比如,我描述这是一个desk,那么谁来都知道这是一个desk,(如果我说1,正常的他就不知道是什么意思)。这就是静态常量的解决方案。那个menuitem也是一样的问题,或许更好的是,我们将value也换做是一个字符串?(但至少有一个MenuItemUpdate
= "2" 就比原来好点了,只是这个2又让人郁闷了一下 )。这些问题还是很有意思的,不过也常常使人纠结。
我写了这么多,不知道各位看官有没有明白我在说什么。
或许,我们应该去努力描述这个世界,而不是去做程序,去实现某个功能。客观世界是什么,就是什么,我一直这么想。
谢谢大家读下去。非常欢迎大家和我讨论各种问题。