==========
以下是引用 精灵 于 2011-11-4 14:06:11 发表的文字:
感谢,谢老师的指点,刚才结合您的讲解我又仔细看了一下参考手册,我是否可以这样认为,例如我要在“书目统计窗”的脚本中调用系统的内部函数,那么我前面的句柄是否就是“BiblioStatisForm.Channel”?如果我使用“册统计窗”的脚本中调用系统的内部函数,那么我前面的句柄是否就是“ItemStatisForm.Channel”以此类推?
==========
对啊,非常好,你的理解很到位。
内务前端的窗口种类繁多,光我在编写手册的时候关于MDI子窗口的介绍就有40多个小节,也就是说有至少40多种重要的窗口存在。从开发程序的角度,这么多的窗口意味着有不少重复模式的编码,所以,如果能节省脑力思考和记忆的量,把窗口和各种设施的命名规范化,便于开发者和使用者类推类比,就是一个很重要的注意点。
首先,这些窗口一般都是“....Form”结尾的类名。这当然也是.NET的一种命名习惯了。所以书目统计窗的类名就是BiblioStatisForm,册统计窗的类名就是ItemStatisForm,读者统计窗的类名就是ReaderStatisForm,...。
但是,请注意,这是类名。类名是创建这个类的对象时候的模板,它是对象的定义,而不是对象。好比,一个人是“人”,人就是他的生物类属。但是我们平时不能把他喊作“人!”,比方说“人!过来吃饭了!”,那么势必100个人都会觉得在叫自己,也不像在叫自己。那样也不礼貌。我们可能会这样叫“小马!小张!”,每个具体的人都有自己的名字,这样就知道在叫谁了。
差不多,人的名字好比是一个对象的对象名。也可以称为“实例”。好比说我们身边外国人不多,我可能会把孩子老师John称为“英国人的一个实例”。
你提到的“BiblioStatisForm.Channel”,这一个称呼学问可不少。刚才我明明说了人是一种类属,而每个人都有具体的名字,按照这个比喻,难道一个BiblioStatisForm不该有自己的名字?这里的BiblioStatisForm到底指一个类的名字还是一个对象的名字?
哈哈,理论归理论,但是我们不能消化不良哦。这里稍微有点说来话长。
“BiblioStatisForm.Channel”,其实全称为“this.BiblioStatisForm.Channel”。那么这个this是个什么?this,如果是你在针对书目统计窗二次开发的时候编写的代码,那你一定是按照规定编写在一个从BiblioStatis类派生出来的叫做MyStatis的类,在这个MyStatis类里面你把MyStatis叫做this。
这个MyStatis类名不重要。可以修改为MyNewClass等等也无妨。为什么这个名字不重要是因为二次开发的当时关注的环境中只有一个这样的需要从BiblioStatis派生的类,没有别的干扰,所以叫什么都无所谓(只要不和系统缺省的某些类名或者保留字重复就行了)。
我们知道,这个MyStatis类中,宿主程序已经通过基类BiblioStatis准备好了一些设施,也就是成员变量。这里有个小问题:为什么当初设计这个体系结构的时候,不直接把书目统计窗的BiblioStatisForm作为基类,让二次开发者来派生出新的类,实现其功能呢?这个想法其实是可行的。但没有这么做,主要是习惯上的原因。因为如果这样那么BiblioStatisForm不但要照顾一次开发的需要,也要照顾二次开发的需要,这负担对它有些重。所谓二次开发的需要,就是要结构稳定,已经公布出来的成员名字不能随便改,函数名不能随便改,要不然二次开发者就头疼了,每升级一次版本就要修改代码才能运行了。所谓一次开发的需要,恰恰是要不断重构,优化,成员和函数名经常会大幅度改动。这两种需求是有些矛盾的。
所以,我们采取了一种习惯性折衷措施,就是为BiblioStatisForm相关的二次开发需求设置一个“客厅”,专门用来会客,对于客人来说,这个客厅的陈设什么的都要保持稳定,让他们一旦熟悉了之后就感觉稳定安详,不要动来动去了。所谓BiblioStatis类就是一个“客厅”类,行话就是“接口类”,专门用来做接口的。
这个BiblioStatis类的用途就是接口,代码很简洁,为了便于观赏我将它的全部代码放到本帖的最后。
所以,在二次开发的时候,要使用书目统计窗这个窗口对象,那么就要通过this.BiblioStatisForm来访问。从代码中看到,
public BiblioStatisForm BiblioStatisForm = null;
这个窗口类的类名叫做BiblioStatisForm(左边),而成员对象名也叫做BiblioStatisForm(右边)。哈哈,这真应验了上面的那个笑话,一个人的名字叫做“人”!
这里的用意是为了减轻二次开发者的记忆量。试想,如果我们不直接用类名作为变量名,而故作雅致地采用类似OneBiblioStatisForm之类的成员变量名,那还不是要沾上这个“BiblbioStatisForm”字符串,也好不到哪里去,反而平添了额外的规则,增加了记忆量。这里的关键是,BiblioStatis这个接口类中只有这唯一的一个属于BiblioStatisForm的成员,那么不把它直接用类名命名,更待何时?好比上面那个笑话,如果世界上总共只有一个人,那么把他的名字叫“人”又有何妨呢?
所以我归结为“习惯”的力量。即便在.NET的基础源代码中,你也会发现很多这样的直接用类名作为成员变量名的例子。
通过上面的解剖,我们透彻了解了系统二次开发里面的一种命名惯例的来龙去脉。不过话又说回来,即便是有二次开发者稀里糊涂连类名和对象(变量)名都分不清楚,那这种命名的方式正好也能稀里糊涂带领他走向“成功”,歪打正着了。所以这个习惯也是一种良性的习惯 --- 虽然从学习知识的角度来说未必是一种凸显概念差异的好习惯。
以上这一通文字可以称为“类名的故事”。
虽然我上面说了,从一次开发的角度,BiblioStatisForm可能会内部变来变去,但是既然它通过接口类暴露给了二次开发者,那么其中特别是Channel成员的名字就不要随意乱动了。
Channel这个成员的名字也很有一些讲究。其实它的类型是LibraryChannel。因为系统中的通讯通道种类很多,比如针对数据库内核的通道,针对图书馆应用服务器的通道,所以名字上有所区别,不再是最通用的那个称呼了。但是,因为在BiblioStatisForm这里,通道对象只有一个,所以就简化为用Channel而不是LibraryChannel作为对象名字了。
但BiblioStatis类里面的BiblioStatisForm为什么不能叫做StatisForm,或者甚至称为Form呢?因为这里也只是一个。这里我不得不谈到,当时心里有一种念头就是各种类型的统计窗是不同的,平时也尤其担心用户把其他类型的统计窗的统计方案导入到了不恰当的统计窗内,所以就故意把对象名没有采用最通用的称呼。也就是说,如果你从这个统计窗的二次开发代码片段复制到那个类型的统计窗中,this.XXXXStatisForm这个名字之间的差异会迫使二次开发者修改名字才能编译通过,借此也促使二次开发者思考一下“我现在在哪里?this代表什么?”
因为不同的统计窗之间,虽然有一些相同功能和名字的成员,但是也有一些不同功能不同名字的成员。当然,要看试图把共性突出,还是把差异突出,也就是设计时候的一念之差了。从逻辑完满,自圆其说的角度,我自己也觉得变量命名的几个因素之间是有矛盾的,一会儿你要这么说,一会儿你要那么说,好像怎么你都有理,但其实经过推敲会发现,换一种做法未必不行,未必就差。
这里我想起来一个故事,就是说许多美国人烤火鸡的时候,都是把火鸡腿砍掉再放入烤箱的。如果你要问他们为什么?他们会说:我妈妈当年就是这么做的,我也这么做。有人研究了一下,发现早先的烤箱很小,放不下整只巨大的火鸡,于是只好砍成几个部分多次烤。但是现在的烤箱太大了,放下整只火鸡没有问题。不过,习惯的力量很还大 --- 大家并不是任何时候都要问为什么的,“传统”自然就是成了自然,...。
所以,大抵上我说了自己的想法和习惯的由来,我没有底气说这些代表着多么正确。如果将来升级改版,软件还有采纳其他新想法和习惯的机会。
~~~
附:BiblioStatis类的源代码。供参考观赏用。
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Xml;
using System.Web;
using DigitalPlatform.Xml;
using DigitalPlatform.dp2.Statis;
using DigitalPlatform.CirculationClient;
using DigitalPlatform.CirculationClient.localhost;
namespace dp2Circulation
{
public class BiblioStatis
{
private bool disposed = false;
public WebBrowser Console = null;
public BiblioStatisForm BiblioStatisForm = null; // 引用
public string CurrentDbSyntax = "";
public string CurrentRecPath = ""; // 当前书目记录路径
public long CurrentRecordIndex = -1; // 当前书目记录在整批中的偏移量
public string ProjectDir = "";
public List<string> OutputFileNames = new List<string>(); // 存放输出的html文件
int m_nFileNameSeed = 1;
public XmlDocument BiblioDom = null; // Xml装入XmlDocument
string m_strXml = ""; // XML记录体
public string Xml
{
get
{
return this.m_strXml;
}
set
{
this.m_strXml = value;
}
}
public string MarcRecord = "";
public BiblioStatis()
{
//
// TODO: Add constructor logic here
//
}
// Use C# destructor syntax for finalization code.
// This destructor will run only if the Dispose method
// does not get called.
// It gives your base class the opportunity to finalize.
// Do not provide destructors in types derived from this class.
~BiblioStatis()
{
// Do not re-create Dispose clean-up code here.
// Calling Dispose(false) is optimal in terms of
// readability and maintainability.
Dispose(false);
}
// Implement IDisposable.
// Do not make this method virtual.
// A derived class should not be able to override this method.
public void Dispose()
{
Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SupressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
// Dispose(bool disposing) executes in two distinct scenarios.
// If disposing equals true, the method has been called directly
// or indirectly by a user's code. Managed and unmanaged resources
// can be disposed.
// If disposing equals false, the method has been called by the
// runtime from inside the finalizer and you should not reference
// other objects. Only unmanaged resources can be disposed.
private void Dispose(bool disposing)
{
// Check to see if Dispose has already been called.
if (!this.disposed)
{
// If disposing equals true, dispose all managed
// and unmanaged resources.
if (disposing)
{
// Dispose managed resources.
}
// 删除所有输出文件
if (this.OutputFileNames != null)
{
Global.DeleteFiles(this.OutputFileNames);
this.OutputFileNames = null;
}
/*
// Call the appropriate methods to clean up
// unmanaged resources here.
// If disposing is false,
// only the following code is executed.
CloseHandle(handle);
handle = IntPtr.Zero;
* */
try // 2008/6/26 new add
{
this.FreeResources();
}
catch
{
}
}
disposed = true;
}
public virtual void FreeResources()
{
}
// 初始化
public virtual void OnInitial(object sender, StatisEventArgs e)
{
}
// 开始
public virtual void OnBegin(object sender, StatisEventArgs e)
{
}
// 每一记录,在触发MARCFilter之前
public virtual void PreFilter(object sender, StatisEventArgs e)
{
}
// 每一记录处理
public virtual void OnRecord(object sender, StatisEventArgs e)
{
}
// 结束
public virtual void OnEnd(object sender, StatisEventArgs e)
{
}
// 打印输出
public virtual void OnPrint(object sender, StatisEventArgs e)
{
}
public void ClearConsoleForPureTextOutputing()
{
Global.ClearForPureTextOutputing(this.Console);
}
public void WriteToConsole(string strText)
{
Global.WriteHtml(this.Console, strText);
}
public void WriteTextToConsole(string strText)
{
Global.WriteHtml(this.Console, HttpUtility.HtmlEncode(strText));
}
// 获得一个新的输出文件名
public string NewOutputFileName()
{
string strFileNamePrefix = this.BiblioStatisForm.MainForm.DataDir + "\\~biblio_statis";
string strFileName = strFileNamePrefix + "_" + this.m_nFileNameSeed.ToString() + ".html";
this.m_nFileNameSeed++;
this.OutputFileNames.Add(strFileName);
return strFileName;
}
// 将字符串内容写入文本文件
public void WriteToOutputFile(string strFileName,
string strText,
Encoding encoding)
{
StreamWriter sw = new StreamWriter(strFileName,
false, // append
encoding);
sw.Write(strText);
sw.Close();
}
// 删除一个输出文件
public void DeleteOutputFile(string strFileName)
{
int nIndex = this.OutputFileNames.IndexOf(strFileName);
if (nIndex != -1)
this.OutputFileNames.RemoveAt(nIndex);
try
{
File.Delete(strFileName);
}
catch
{
}
}
List<ItemInfo> m_itemInfos = null;
public void ClearItemDoms()
{
this.m_itemInfos = null;
}
public List<ItemInfo> ItemInfos
{
get
{
// 优化速度
if (this.m_itemInfos != null)
return this.m_itemInfos;
// 如果当前书目库下没有包含实体库,调用会抛出异常。特殊处理
// TODO: 是否需要用hashtable优化速度?
string strBiblioDBName = Global.GetDbName(this.CurrentRecPath);
string strItemDbName = this.BiblioStatisForm.MainForm.GetItemDbName(strBiblioDBName);
if (String.IsNullOrEmpty(strItemDbName) == true)
return new List<ItemInfo>(); // 返回一个空的数组
this.m_itemInfos = new List<ItemInfo>();
long lPerCount = 100; // 每批获得多少个
long lStart = 0;
long lResultCount = 0;
long lCount = -1;
for (; ; )
{
EntityInfo[] infos = null;
string strError = "";
long lRet = this.BiblioStatisForm.Channel.GetEntities(
null,
this.CurrentRecPath,
lStart,
lCount,
"",
"zh",
out infos,
out strError);
if (lRet == -1)
throw new Exception(strError);
lResultCount = lRet; // 2009/11/23 new add
if (infos == null)
return this.m_itemInfos;
for (int i = 0; i < infos.Length; i++)
{
EntityInfo info = infos[i];
string strXml = info.OldRecord;
if (String.IsNullOrEmpty(strXml) == true)
continue;
ItemInfo item_info = new ItemInfo();
item_info.RecPath = info.OldRecPath;
item_info.Timestamp = info.OldTimestamp;
item_info.OldRecord = strXml;
item_info.Dom = new XmlDocument();
try
{
item_info.Dom.LoadXml(strXml);
}
catch
{
continue;
}
this.m_itemInfos.Add(item_info);
}
lStart += infos.Length;
if (lStart >= lResultCount)
break;
if (lCount == -1)
lCount = lPerCount;
if (lStart + lCount > lResultCount)
lCount = lResultCount - lStart;
} // end of for
return this.m_itemInfos;
}
}
// 保存修改过的册信息。
// 调用本函数前,要修改Dom成员
// return:
// -1 error
// 0 succeed
public int SaveItemInfo(List<ItemInfo> iteminfos,
out string strError)
{
List<EntityInfo> entityArray = new List<EntityInfo>();
for (int i = 0; i < iteminfos.Count; i++)
{
ItemInfo item = iteminfos[i];
EntityInfo info = new EntityInfo();
if (String.IsNullOrEmpty(item.RefID) == true)
{
item.RefID = Guid.NewGuid().ToString();
}
info.RefID = item.RefID;
DomUtil.SetElementText(item.Dom.DocumentElement,
"parent", Global.GetID(CurrentRecPath));
string strXml = item.Dom.DocumentElement.OuterXml;
info.OldRecPath = item.RecPath;
info.Action = "change";
info.NewRecPath = item.RecPath;
info.NewRecord = strXml;
info.NewTimestamp = null;
info.OldRecord = item.OldRecord;
info.OldTimestamp = item.Timestamp;
entityArray.Add(info);
}
// 复制到目标
EntityInfo[] entities = null;
entities = new EntityInfo[entityArray.Count];
for (int i = 0; i < entityArray.Count; i++)
{
entities[i] = entityArray[i];
}
EntityInfo[] errorinfos = null;
long lRet = this.BiblioStatisForm.Channel.SetEntities(
null, // this.BiblioStatisForm.stop,
this.CurrentRecPath,
entities,
out errorinfos,
out strError);
if (lRet == -1)
return -1;
// string strWarning = ""; // 警告信息
if (errorinfos == null)
return 0;
strError = "";
for (int i = 0; i < errorinfos.Length; i++)
{
if (String.IsNullOrEmpty(errorinfos[i].RefID) == true)
{
strError = "服务器返回的EntityInfo结构中RefID为空";
return -1;
}
// 正常信息处理
if (errorinfos[i].ErrorCode == ErrorCodeValue.NoError)
continue;
strError += errorinfos[i].RefID + "在提交保存过程中发生错误 -- " + errorinfos[i].ErrorInfo + "\r\n";
}
if (String.IsNullOrEmpty(strError) == false)
return -1;
return 0;
}
}
// 册信息
public class ItemInfo
{
public string RecPath = "";
public byte[] Timestamp = null;
public XmlDocument Dom = null;
public string OldRecord = "";
public string RefID = "";
}
}