昨天开始学C#,强迫自己用C#写一个应用(当然是不是工作中急需的,只是练练手而已),发现在C#里,类的声明和实现必须放在一起,这真是#@¥×&×……
/// 日志记录器接口
interface ILogger {
void append(string msg);
};
/// 创建日志记录器实例的工厂
class LoggerFactory {
public static ILogger execute() {
//注意这里,由于声明和实现必须放在一起,所以
//不得不让Logger的使用者知道有LoggerFlatFile这个冬冬的存在
return new LoggerFlatFile();
}
};
/// ILogger的平面文件实现(如果声明和实现可以分开,本来可以让外界不知道它的存在)
class LoggerFlatFile : ILogger {
public void append(string msg) {
...
}
};
考虑一个这样的实现:我需要一个日志记录器,但不想让使用者知道它的实现细节(以后我也许会将它改成单例、也许会记到SQLServer数据库中、也许会打印到控制台),所以我设计了一个ILogger接口,既然有了接口,那么就必须有个工厂来创建实现这个接口的派生类实例,问题在于,由于工厂类的声明和实现必须写在一起,所以知道工厂类的人,也就知道了LoggerFlatFile这个具体类,本来我是想把这一点隐藏起来的。
现在,假定有一个程序员张三,他准备用ILogger来记录一些日志,从IDE的智能感知里,他一下子就发现了LoggerFlatFile这个类是派生自ILogger并实现了其接口的,立刻就兴高采烈地用了一句“ILogger logger = new LoggerFlatFile()”来创建了实例并满意地开始使用……由于他并未注意到Factory的存在,造成这一后果的原因,其实并不是因为他的粗心,而恰恰是编译器没有能预防住他构造派生类实例!
而另一个程序员李四,他事先查看了文档,知道要创建ILogger实例就必须用LoggerFactory的execute()这一静态方法,但是,有一天,他丢了钱包导致方寸大乱,在写一段新代码时,忘记了要用工厂,而写了能使编译顺利通过的new……这仍然不能怪他,只能怪编译器……而编译器会很负责任地说,它放过的代码是符合语法的,真正的罪魁祸首,应该是类的设计者!
如果是C++,我可以将LoggerFlatFile这个类的声明和实现都放在CPP文件里,H文件里只放ILogger和LoggerFactory的声明,而LoggerFactory::execute()的实现却是放在CPP文件里的……再退一步,如果我非要把这一切都放在H文件里,我还可以令LoggerFlatFile的构造函数为私有,同时令LoggerFactory成为它的友元。
再从工程的构建上看,只要一个H文件不变,不管它的CPP如何改变,所有引用了该H文件的CPP都不用再重新编译,在很慢的一些编译器上(比如Borland C++ Builder),节约了相当的时间。
就我目前所能看到的“将声明和实现放在一起”的唯一“好处”,似乎只是“维护起来方便”,但这难道不是本末倒置的吗?类的设计应该是方便使用者(就是我的同事甚至我自己),而不是方便设计者,要尽量让使用者少犯、不犯错误……而在DELPHI里,每个DFM文件都对应一个PAS文件,在ASP.NET里,每个ASPX文件都对应一个.CS文件,在VB里也是同样,为什么使用者都不觉得维护起来麻烦,单单要指摘CPP和H的分开呢?在现代IDE里,在声明和实现之间切换也就是一个快捷键或双击鼠标的事情,何来麻烦之说?(在我的VS2003里,声明和实现的切换是快捷键ALT+G)
隐藏实现细节,向来是软件设计的追求,typedef也是一种隐藏细节,命名常量替代魔数也是,类的封装、接口的设计等,都是隐藏细节,为什么我非得把声明和实现放在一起呢?
BTW:我的好友说这么多年来,还从来没有人抱怨过这个问题,言下之意,就是说只是我一个人在JJYY,绝对的是RPWT:)