前一段时间负责负责论坛的迁移工作,对其架构进行了简单的整理。前几天看到有人说
discuz的介绍很少,因此整理了一下,发布出来。
也是第一次发表文章,大侠们手下留情。
Discuz整体架构如下图所示:

横向表示 同一层次中涉及的各个模块(项目)
纵向表示 不同层次之间模块的关系,某些关系是如何在各层次中传递(穿越)
Discuz架构上采用了比较流行的三层架构,即表现层,业务逻辑层,数据访问层来进行设计,并结合自己的情况进行了特殊处理。
表现层:
表现层即为上图中蓝色虚线表示,主要包括:Web,Services,UI,Control。各项目主要功能为:
UI 定义各种页面基类,提供Ajax访问访问接口。
Control存放Discuz用到的自定义服务器端控件。
Services提供外部访问接口。
Discuz引入了一种模板引擎的机制,来实现表现层的多样化。
主要设计思想为:针对设计人员,提供纯静态页面,并提供了一套约定的语法和标签(具体位置在:templates)。模板制作完成后,要进行模板导入,此时discuz会将静态模板进行解析将其转换成 aspx页面,然后放到aspx/1..n下。如果你打开这下面的文件,会发现前端只是一个字符串拼接的过程。要进行的逻辑判断,都放到了后台代码中。后台代码只有一份,所有的 aspx模板引用同一个后台处理类。由此实现web表现的多样化
当用户进行页面浏览时,首先确定显示哪个模板,然后采用地址重写技术,将其转移到实际的处理文件。在web.config配置为

可见Discuz对所有的请求进行了控制,其代码如下 (以Index.aspx为例):

首先程序会先查找Cookie,找到TemplateId,然后重定向到相应的模板文件。
综上所述:模板+重定向实现了表现层的多样化。
业务逻辑层:
业务逻辑,顾名思义就是处理与业务相关的代码。Discuz采用的也是中小型项目的常用的“贫血模式”,即在业务逻辑层只是进行实体的获取,转发和赋值,几乎没有业务操作。
本该封装在此层的业务代码进行了分散,一部分前移至表现层(比如发帖时的加分操作,附件处理),一部分后移到了存储过程(比如发帖后更新我的发帖列表)。
注:关于贫血模式的论述详见 Martin Fowler的相关著作<企业应用架构模式>等
在业务层,使用了Discuz缓存。主要是更改了存储体,将其存储在xml中(为啥这么喜欢用xml呢,印象中它是很慢的),调用方法和通常情况下几乎无差别。
个人感觉其业务逻辑层是项目中设计最失败的地方。拿发帖举例,如果我进行设计,我的方案可能会是这样:
时间关系,有时间再写一篇文章。
顺便说一句:如果要进行Discuz的整合,主要调用的就是此层的代码。
主要项目为:
Discuz.Forum
Discuz.Space
数据访问层:
Discuz基于商业考虑和版本限制等因素,迄今为止已有多种数据源:access,mysql,sqlserver等。为了实现三种数据库的接口统一,此处使用了接口和抽象类进行规范。
其类库结构如下(调用方以Post为例):

各个数据库中的PostManage都使用DbHelper进行通用数据库的访问。DbHelper本身并没有指定具体的数据库链接类型,参数类型,而是使用.Net自带的抽象类DbProviderFactory来创建。具体数据库的加载要等其静态属性Provider,Factory调用时,读取配置文件,以反射形式进行初始化。
代码如下:

通过此种形式,可以实现各种数据接口的调用的统一,同时方便数据库类型的拓展。比如要加入Oracle的支持,只需要继承IDbProvider实现OracleProvider,新的PostManage继承IDataProvider重写部分方法即可。
而业务层(Posts)的调用通过IDataProvider接口来进行统一,避免了和数据库类型的耦合,可以在不改变业务层,表现层的代码基础上实现数据库之间的迁移。这正是大型项目所需要的,以接口来实现层与层之间的通讯,将更多的可变因素,扩充点实现配置化。
其他子模块的介绍
1. 配置
对配置的管理,小型项目可以直接使用web.config,中大型项目一般使用自己的配置解决方案。原因是:
1. 中大型项目配置文件过多,直接使用web.config来会造成其体积过大
2. web.config直接使用字符串进行读取不方便,
试着比较一下:
ConfigurationManager.AppSetting[“SiteName”];
SiteInfo.Name
3. 每次都需要进行类型转换
Discuz实现了自己的配置类,其类结构如下(以Email为例)

IConfigInfo为空接口,没有定义任何方法,主要是方便DefaultConfigFileManager传递,方便以后扩充。对配置文件的解析也没有使用.Net自带的接口,而是重新定义了接口,同时使用了xml反序列化实现配置文件的加载和类型转换。
代码见: DefaultConfigFileManager.DeserializeInfo

比较疑惑的是这个项目中某些类给出了实现,却没有发现调用。可能是兼容或者扩充问题留下的,谁对这方面了解的,也可以跟帖说下。
这些类有:ConfigProvider,IConfigFileManager
2. 数据库表的设计
数据库设计中有两个引人注意的地方:
1. 主题表分离
如果由我们来设计主题表和回帖表,通常的做法是如下。

这样在获取主题列表时,直接使用分页算法提取Topics;查看某一帖子时,还需要对Topics,Posts进行jion链接。
此种设计的缺陷为:
1. Topics表存储Content的内容,其体积将会很大,对大体积表进行分页,性能很慢。
2. 显示Posts内容时将进行join操作,损耗性能
而Discuz的做法是进行如下设计。

将Topics里的Content拆分到Posts中去,同时Topics的主题帖也作为回帖放置到Posts里面,这样就解决了上面我们提出的两个问题。这是典型的违反数据库设计范式以换取更好性能的示例。
2. 对Posts表进行水平拆分
原来以为每一百万帖子,discuz会自动进行拆分,后来发现在discuz后台能够进行设置,手动进行分表,discuz建议每30-50万帖子进行一次拆分。
进行拆分后,每个表的体积将会减少,保证了查询的效率
Discuz的整体架构还有很多其他值得细说的地方,例如插件、扩展等,这些需要感兴趣的人自己一一去研究,在此就不多讲了.
缘起:以前写过一篇文章《Discuz!NT持续集成实战》介绍CruiseControl.net 来做Discuz!NT项目的持续集成 。最近喜欢上了 Powershell ,由于本人向来懒惰,遂将日常工作中一些琐碎的,重复的事情用 powershell 来做了。为此某不才,读了 PowerShell 自带的 《GettingStarted》和《UserGuide》 ,基本的东西也就差不多了。在实践的过程中也曾被一些小问题卡住,在ps自带的文档中没有发现解决办法。幸而得到高人相助,一句话道破天机。(悄悄说:这位高人最近酝酿一件关于ps的作品)

废话少说,上代码:
Code dnt_publish.ps1

#dnt_publish.ps1 Create by 戏水 2008年12月23日13:05:22 zjneter.cnblogs.com
#环境变量
Set-Location -path E:\publish
. .\functions.ps1 #引入外部文件,相当于include
$Src = "E:\source\dnt\3"
$SetupPublishTo = "E:\publish\pub_dnt26"
$SourcePublishTo = "E:\publish\source\dnt26"
$ZipSetupFileName = ".\dnt_26_n2.zip"
$ZipSourceFileName = ".\dnt26_n2_src"
$ExcludeFile = get-content -path .\ExcludeFile.txt
$ExcludeDir = get-content -path .\ExcludeDir.txt
$IsZipSetup=Read-Host("是否生成安装包?[y\n]")
$IsZipSource=Read-Host("是否生成源码包?[y\n]")
$IsUploadSetupToDntWebServer=Read-Host("是否上传安装包到nt官方?[y\n]")
$IsUploadSetupToDownloadServer=Read-Host("是否上传安装包到下载服务器?[y\n]")
$IsUploadSourceToDownloadServer=Read-Host("是否上传源码包到下载服务器?[y\n]")
#SVN更新
svn up $Src
#编译解决方案
C:\Windows\Microsoft.NET\Framework\v3.5\MSBuild.exe $Src\Discuz_vs08.sln /t:Build /v:m /fileLoggerParameters:LogFile=MyLog.log;
#生成模板
#处理安装包
if ($IsZipSetup -eq "y") #如果变量IsZipSetup=="y"
{
#复制及清理文件
robocopy $Src\Discuz.Web $SetupPublishTo\upload_files /E /XF $ExcludeFile /XD $ExcludeDir /MIR
.\clear.bat #在这个bat中进行了一些清理文件的操作
#开始压缩
wzzip $ZipSetupFileName $SetupPublishTo -r -p #调用winzip的命令行 生成压缩包,参数说明请google
}
#处理源代码包 ,ExportDNTSource 是戏水自己写的function
if ($IsZipSource -eq "y")
{
ExportDNTSource 'https://svn.bugaosuni.com/dnt' E:\publish\source\dnt26
wzzip $ZipSourceFileName $SourcePublishTo -r -p
}
#是否发布到nt.discuz.net
if ($IsUploadSetupToDntWebServer -eq "y")
{
#Write-Host "UploadSetupToDntWebServer $IsUploadSetupToDntWebServer"
UploadSetupToDntWebServer
}
#是否发布到download.comsenz.com
if ($IsUploadSetupToDownloadServer -eq "y")
{
#Write-Host "UploadSetupToDownloadServer $IsUploadSetupToDownloadServer"
UploadToDownloadServer $ZipSetupFileName "2.6"
}
if ($IsUploadSourceToDownloadServer -eq "y")
{
UploadToDownloadServer $ZipSourceFileName "src"
}
下面是一些功能函数 :

Code function.ps1
#function.ps1 Create by 戏水 2008年12月23日 13:16:43 zjneter.cnblogs.com
#上传ftp
function global:Upload
{
#param定义函数接受的参数,可以指定默认值
param([string]$h, [string]$u, [string]$p,[string]$f,[string]$cd="\")
$cmd = "open $h
user $u $p
binary
cd $cd
put $f"
$cmd | ftp -n
}
function UploadSetupToDntWebServer
{
Write-Host -foregroundcolor green "开始ftp上传到官方网站"
Upload -h 221.194.139.228 -u nicai -p !@#$%^&*() -f $ZipSetupFileName
if ($?)
{
Write-Host -foregroundcolor green "上传到官方网站成功!"
}
else
{
Write-Host -foregroundcolor red "上传到官方网站失败!"
}
#$?变量表示最后一个操作成功与否,成功返回true
}
function UploadToDownloadServer
{
param([string]$filename,[string]$cd)
Write-Host -foregroundcolor green "开始ftp上传到下载服务器"
Upload -h download.comsenz.com -u nizaicai -p !@#$%^&*() -f $filename -cd $cd
if ($?)
{
Write-Host -foregroundcolor green "上传到下载服务器成功!"
}
else
{
Write-Host -foregroundcolor red "上传到下载服务器失败!"
}
}
function ExportDNTSource
{
param([string]$url,[string]$path)
Remove-Item $path -force -recurse
svn export $url $path 从svn中导出无版本控制的源代码
if ($?)
{
Write-Host -foregroundcolor green "导出源代码成功!"
}
else
{
Write-Host -foregroundcolor red "导出源代码失败!"
}
}
这里需要注意的是 dnt_publish.ps1文件中 引入 function.ps1文件的方法:文件名前用了两个点,而且两个点中间需要有空格。
闲言碎语:不才初步体验了PowerShell 一下,感觉很不错哦,像粘合剂一样 把一些小工具 ,WMI .Net Framework 等联系起来 组成超级强大的武器。 语法么还是稍稍感觉有些怪,不太舒服,不过习惯就好了。 另外如果VS能支持 PowerShell 就好了,感觉 PowerShellPlus 用起来还是稍稍不爽。自从写了这个脚本,一切只需轻轻的点一下鼠标,真个过程就自动ok了 (过程: svn check out -> MSBuild 编译解决方案 -> 上传到FTP服务器) 。当然 结合前天代震军同学翻译的文章
http://www.cnblogs.com/daizhj/archive/2008/12/22/1352019.html ,就可以实现整个的打包发布流程咯。
行文仓促,如有谬误 ,还请指正
编辑器不好用,上面的代码可能有问题, 提供源代码下载:http://files.cnblogs.com/zjneter/dnt_publish.rar
一个人自娱自乐的写个小程序,跟一帮人一起写个大程序。真的是不一样。
自己一个人,根本就不存在交流,相互理解的问题。人越多,理解他人意图,向他人解释意图就越来越花时间。只要是需要交流的任务,并非是人越多越好。有人加入,为了使加入的人有事做,原来的事就要重新划分,而分开之后要配合,又要花时间交流。发觉很多重要的软件开始都是几个人做出来的。而漫画中,进行任务也采用小组模式,好像<幽游>, <猎人>都是四人小组。
这里,最基本的问题就是任务的划分,最理想的划分是相互独立。而要做到这种独立,正交的划分,是很困难的,更困难的是你会发现那最初的任务会随着时间变动。传统的软件工程会说,首先是定义需求,跟着大体设计,详细设计,编码,单元测试,整体测试等等。但真正实施起来,发觉没有那样理想,很多项目都不是重新编写,而是在原有的代码上加强,很可能原来的根本就是沙地,很是流沙,而却要在上面起高楼。假如一个项目规定是半年完成,花上一个月去定义需求和设计,跟着去编码了,就会发现很多事情是想不到的,有些看来很简单的事一卡就卡上一个月,毫无进展。发觉这本来是很简单的问题牵涉到项目的结构,而之前结构很难修改,又或者不舍得去修改,为解决这问题,就使出一些歪招,看起来好像很巧妙,却打乱了原来的结构,跟着这些古古怪怪,想不到的问题一个个冒出,原来的设计渐渐偏离,又没有去修改文档。时间越来越紧,项目拖后,公司上层发觉不对路了,就加人,新人来,拿起最初的设计文档看,发觉根本对不上。旧人就又花时间会帮他理解项目。到原定的发布限期,程序却一运行就死机,最终结果迟上两三个月发布,发布之后很多bug, 再花两三个月改错,发个补丁包。一年就此过去。
软件中常说的任务划分,其实就是要确立边界条件。比如划分出组件,组件中划分出类,组件与组件,类和类交互都要通过一些接口。边界条件最容易出问题。本来我这个类好好的,一和另一个类交互,就出问题了,又比如这个函数好好的,一到线程切换那一瞬间就出问题。接口的定义很重要。软件还没有完全做到硬件那样即插即用。先确立接口,跟着找不同的人做实现,最后嵌起来就可以用,这也是很理想的一种情况。很可能最初那样划分接口就错了,另一种可能是实现者理解错了接口的含义。很多事,做完了才知道是做错了。比如考试,通常考完出来就知道答案,想着下次好好准备,考好点,却发觉下次还是有题目是不会做。
我现在自己的意见(以后可能也会变), 项目初期是无论如何都想不出具体的细节的,所以只要有个大方向就可以了, 要尽快动手做。既然之前没有想出所有细节,就强调要可以很容易的调整代码的结构,添加更多的细节,也就是重构。注意,重构并非是添加新功能,而是在不改变程序表现的情况下整理代码,使原来的代码更合理,之后再添新功能就容易了。而所谓的添个if来判断空指针,再对话框上面加个按钮等等,就不算是重构了。
为修改代码更容易,应该注意某些小细节。特别是工程比较大的时候。有些习惯要一开始就养成,不要想着只是做些小玩意玩玩,没有关系啦。一开始就应向着这行业最top的那批人看齐,这样才有可能做到专业。
1. 减少编译连接时间,特别尽量不要在头文件上加头文件。
以前也很随意的乱加头文件,反正小工程,一下就编译完了。现在发觉只要我改某个头文件,等它编译完够看一章书了,经过编译折磨,才发觉这很重要。因为头文件一修改,包含或者间接包含的cpp文件就要被编译。乱包含,会发现底层的文件一改,几乎整个工程都编译,是很费时间的。另外就不要将所有类定义都写在一个地方,这样包含依赖会减少一些。
如果编译时间很长,明明知道有些地方不妥,也不会去修改的。好像写程序的都很怕麻烦。
2. 变量用到才定义,不要一开始就定义。
很多人还保留C的习惯,将所有用到的变量都定义在函数开头。这习惯其实很不好,一方面没必要的构造析构会被调用。更重要的是,你会发觉以后想将原来的函数分解成一些小函数时候,会很麻烦,因为定义在开头,作用域是整个函数的,你想提出一段代码,很难确定那些变量要用到,那些不用。另外变量定义一定要附上初始值,这个错误看起来很弱智,但很多人会犯,特别是喜欢将所有变量就放在开头的那种C风格的人。
3. 用类管理资源,获取资源跟释放资源尽量在同一个地方,不要分开在两处。特别是不要在同一个函数中不要new, delete同一对象或者数组。
资源是很广义的,比如取gdi对象和释放,常说的是内存。要复制一个字串,或要暂时读一内存,开始又不知道长度,很多人会new 一个char数组,跟着函数末尾在释放。这样就有问题,比如中间有一个return, 跳过了释放的语句,就有资源泄露。对此很多人坚持一个函数一个出口,但是这样往往有一些变量标记性着是否结束循环等等,远不如一个return直接。另外换另一个人来修改代码,他很可能不信奉一函数一出口。随着函数修改变长,对应的获取释放相隔越来越远。怕麻烦,为了不在每个return之前添上释放代码,有人就用goto out: 替代return, out:之后做释放。
只要出现goto, 资源跟释放隔太远,以后想将一些代码提出来,做成另一个函数,就很麻烦。
所以C++有个惯用法RAII, 简单理念是用类来管理资源,在构造函数中获取,在析构释放。因为标准保证每个出口,已生成的对象其析构会被调用,包括发生异常。如果要分配char数组,可以用std::vector<char> mem; mem.resize(memSize)来替代, new char[memSize];, 之后的指针可以写成&mem[0]。 有人可能会说,如果对象太大,可能直接定义会引起栈溢出,所以要new. 如果真的是这样,很可能是那个对象的设计本身就有问题。要是老是写这样一些释放的辅助类很烦人,看看Loki::ScopeGuaid.
4. 风格保持一致
风格看起来是很个人,很细微的事情,但对于一帮人保持协调也是很重要,最忌的是团队每个人,或者个人在不同时候的风格都不一样。因为风格不同,在从一种风格逃到另一种风格时,思维上会卡一卡。另外接口命名的风格不一样,就很容易的将接口用错。
比如,有个迭代器类,要判断前进是否合理,有千奇百怪的名字,hasMove, hasMoveElements, isCurValie, isValie, isOk isLegal。又或者判断有几个元素,有名字getLength, length, size, numElemnts, totalElements。你用那些类,大概会骂最初写代码的那批人太白痴。但想想,有几个人是会将风格保持一致的。
其实风格本身没有好坏,最怕的是不统一,选择一种,跟着用就是了。但现实种往往是一批人同做一项目,各人都有自己写法,还鄙视他人的写法。假如A是取名成stl那风格,小写加下划线,B取命成MFC那种,C取名成java那种,看起来会很累。这个问题是很普遍的,写代码的很多人都很聪明,也很自傲,往往觉得自己做的才是最好的。
另一点是,相同功能类的那些函数接口,如果有同样的函数,应该将名字取成一致,不要搞得很乱。
一帮人一起会做一件事,会确立规矩,比如一起会玩游戏啊,出去玩啊,会商议定个时间。有类人当别人商量规矩时候,他不出声,或者是做别的事,比如玩手机发短信之类。规矩定好了,自己不清楚,就老是问,或者犯规了就怪规矩定得不合理。这类人是很影响士气的,观察一下,你身边应该会有类似的人。
5. 最后一条,老原则,Keep it simple and stupid.
程序首先是给人看的,之后才是被计算机看的。一定要简单。比如接口设计开始要最简化,不要想着这接口以后会用到就加上,要想着以后可能没有用就去掉。如果你自我陶醉,觉得自己很聪明,这样巧妙的代码也可以写得出,将来还有什么软件写不出来,注意,这样想会有问题。因为越是巧妙的代码,以后修改的人会越难理解,越容易出错。其实简单的代码才难写。最好的设计应是理所当然,顺理成章的,感觉不到有多巧妙。<孙子>中有句话,善战者,无功名,无勇功。因为他们去打仗,打之前就赢定了,根本就不需要很勇敢,很激烈才能打赢,一切顺理成章,好像没有什么难度,自然不会被人去歌颂。同样,需要很巧妙的方法才能解决某问题,本身就落下乘。