MFC之设计模式分析

MFC之设计模式分析

all

程序的起点 命运之始

既然是C++程序,必然是由main函数(MFC中是WinMain)开始的.MFC作为一个成熟的软件框架,自然有着老练的一套-一个程序的实例需要有唯一的初始化(这里并不表示一个程序只能打开一次(即一个实例),仅能有一个实例这是通过自己重载initApplication实现的,但是这并不是其初衷).因此在程序尚未初始化的时候,就利用全局函数AfxGetApp取得该程序theApp的对象指针,然后用该指针指导后续界面,功能的加载.

显然,这里用的是Singleton模式.不直接对类进行初始化转而利用函数获得该类的指针以保证仅有一个实例.

pApp->InitApplication()

pApp->InitInstance()

Imgur

当然这里的Singleton和我们一般学习的不太相同,但是这也体现了设计模式并不是照搬的这一重要特点.设计模式并不是传统的模式,更应该说是一种设计思想,其实现方式可以各种各样.一般的Singleton采用将构造函数设置为protected,这样就可以避免client类错误初始化(Java Script就没有,所以需要用其他方法),然后设计一个静态static的指针返回该类指针.MFC中使用的更类似JS的方法:ASSERT(AfxGetThread() == NULL);.即,如果AfxGetThread() == NULL,那么初始化,否则失败.
借由Singleton模式,对程序进行初始化.基本顺序是

1
2
3
4
5
6
7
1. pApp->InitInstance()
2. CmyWinApp::InitInstance()
{
m_pMainWnd=new CMyFrameWnd
;

}
3. CMyFrameWnd::Create(){CreateEx();}
4. CWnd::CreateEx(){PreCreateWindow();}

由于有RTTI的原因,所以有很多地方就不需要用到常用的设计模式了,因为已经能够自己识别自己的类型,就不需要另外的抽象和实现的分离.简单说一下,RTTI分为两种,均用C风格的MACRO来编写,动态识别分别为DECLARE_DYNAMICIMPLEMENT_DYNAMIC,动态创建为DECLARE_DYNCREATEIMPLEMENT_DYNCREATE.这里主要讨论设计模式,所以对RTTI不作细究.当然RTTI贯穿整个MFC,是非常重要的元素.

另外,并不仅仅只有pApp才用了Singleton模式,其他的例如CDC也有使用.

1
2
3
4
5
6
void CScribbleView::OnDraw(CDC* pDC)
{
CScribbleDoc* pDoc = GetDocument();

ASSERT_VALID(pDoc)//(此处和ASSERT功能类似,都是通过宏来判定指针是否NULL,需不需要初始化)
//......
}

Document-View 牵一发发动全身

Document为模式中的subject,负责管理数据,view则是observer,用于显示document中的数据,使用者通过view看到document,也可以通过view改变document,view是document对外显示的接口.但是,view并不是独立的,而是保存在document frame的窗口内的.当document变化的时候,就会调用updateAllView进行对所有观察者的更新.因为在单个view下可以有多个数据的存在,所以此时需要一种一对多的依赖关系,并且在document的状态改变的时候,所有依赖它的对象都会得到消息并且更新.这也是observer模式的初衷.
observer2

后面根据具体代码分析.CFrameWnd(派生于CObject的类都可以接受消息,消息网路这里不讨论了)接受到系统发出的WM_CREATE消息,然后引发CFrameWnd::OnCreate()->CFrameWnd::OnCreateHelper()->CFrameWnd::OnCreateClient()->CFrameWnd::CreateView();

1
2
3
4
5
6
void CFrameWnd::CreateView(CCreateContext* pContext,..)
{
CWnd* pView=(CWnd* ) pContext->m_pNewViewClass->CreateObject();
pView->Create(....)
//......
}

observer

建立了多个View以后,CDocument有m_ViewList维护一系列的View.CFrameWnd仅有CView* m_pViewActive.指示现在正在活动的View.如果CDocument发生改变,就会调用UpdateAllViews()函数,使整个m_ViewList里面的View都update.这就是observer模式了.


文件serialization 永久机制

MFC之所以为应用程序框架,一个重要的特征就是能够将管理数据和负责数据显示的代码分离开来.看到这个特征,很自然的想起Bridge这样一种模式.正如无数设计模式的书上说的一样,设计模式是一种思想,它不立足于实现,一个很简单的例子:你一看到刚才那个特征,你就应该反应到这是桥接模式,但是你可以不知道桥接模式是怎么实现的,你只需要先知道概念,通过需求推导出模式,后面才是实现细节.这才是设计模式的根本.
bridge

CArchive和CFile的client类是CDocument.例如CDocument::OnSaveDocument()函数里面,

1
2
3
4
5
6
7
void CDocument::OnSaveDocument(..)
{
CFile* pFile=GetFile(...)
CArchive saveArchive(pFile,CArchive::Store);

Serialize(saveArchive);
//......
}

CArchive(实际上是文件缓冲区)和CFile之间的串行化设计采用了桥接模式.CArchive实现client需要的一系列接口(包括输入输出流的重载(>>&<<)),数据则由CFile来操作,不考虑文件究竟以何种形式储存,数据的形式以及处理都是由CFile处理,实现了抽象于具体的分离.使得两个类CArchive和CFile可以在互不影响的情况下进行修改.所有需要写入文件读取文件的操作都在CArchive里面 ,它获得CFile作为参数,从而获取文件名,请求类型以及其他必要信息.

Design_pattern_first

面向对象范式&&设计模式

抽象类

抽象类经常被描述为“不能实例化的类”。这个定义是正确的-在实现层次上。但是在概念层次定义抽象类才更有助于理解。其概念为其他类的占位符

那也就是说,他们给我们为一组相关的类命名的方法。这让我们可以把一组类当作一个概念来处理。

事实上,需要理解抽象类并不困难。尤其从概念上理解而不是从语法和实现层次上。生物这是一种人为产生的概念,其本质就是一种抽象类,而且生物所共有的特性就是所谓的纯虚函数,例如 1.应激性 2.繁殖与发展 3.摄食……(生物的特征本来有7个来着,不过我初中一结束就全部忘掉了)。生物是不能够实例化的,你也无法说出一种名为“生物”的生物。而且生物界是一个多重继承的关系,生物下面还有动物,然后脊椎动物,然后鸟类,然后界门纲目科属种我就不举例了。实际上能够实例化的类只有“杜鹃”这种已经没有纯虚函数的物种。它overload了所有的函数,并且有自己的行为。

里氏代换原则(LSP)

Liskov Substitution Principle(里氏代换原则):子类型(subtype)必须能够替换它们的基类型。

反过来的代换不成立
“软件单位的功能不受到影响”


设计方法

一种面对对象的设计方法是:观察问题->提取名词->创建对象->提取动词(即对象行为)->添加方法(特指类成员函数)->提取抽象(形成继承及多态).但是这样将注意力集中与名词和动词往往导致臃肿的类体系,也可以说是组合爆炸.更优秀的方法应该是在创建对象的时候使用共同点/变化点分析.

  • 发现并封装变化点
  • 优先使用对象组合,而不是类继承

变化点是可以体现在类继承当中的,当一个基类派生出多个子类.这些不同子类之间的差别就是变化点.之所以推荐优先使用对象组合,是因为这样可以讲变化点独立开来,从而使将来的变化(不断变化的需求)不会影响到现在的代码.

自顶向下,逐步求精

很早之前就接触过这个概念,我当然能看懂每字每句的含义,但是一直没有理解到底什么叫做自顶向下,也许只是因为我接触的太早了, 在我们英文的C++教材,how to program里面过早的告诉我们这点,也许作者希望我们能够先有设计模式这样一种概念,然后再慢慢加深理解.

为什么要在设计模式里面说自顶向下呢?一个优秀的pattern必然是一个联系精密结构优美的整体,在这个整体的基础上,不断的分化,最终形成各个小的部分,这和自顶向下的思想是同步的.而我们一般书写程序都是从细节开始慢慢向上层写,因为细节总是很容易写的,例如我最近在写的一个LBS的软件,我先写了获取地理座标的类,获得了longitude和latitude然后自然就想到解析地址,随后再到上传地址给服务器,从服务器获取在本机周围的人的名单,最后处理表现.对于代码来说,这样来写完全符合人类思维逻辑,但是对于日常生活(还有设计模式)来说,却刚好相反,你在描述一样东西的时候是绝对不会从细节开始描述的,例如你想描述一座房子,总是整体->局部的顺序.

但是,自顶向下也有自己不可避免的弊端:接口设计和细节实现之间的不匹配以及接口的过度投资.第一点当然可以由长久的开发经验和优秀的设计来弥补,后面一点确是很难弥补的,因为无法确定最终需要的细节是什么.不过和自底向上相比,依然有着优势,在重构的时候负担会轻松的多.一个程序难免需要重构,第一次实现的时候就能确认细节,在重构的时候就可以充分考虑扩展性和灵活性来为之更新了.

参考

jeykll 记录

google analytics&&disqus

不知道原因为何,在自己建立的github pages中无法引用作者提供的analytics和disqus,那就自己修改一下吧。步骤很简单。
如下代码:

1
2
3
4
5
6
7
8
9
10
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-Y']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google- analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>

最后,可以将自己的_includesanalytics-providers文件夹和analytics都删掉,因为没用了。注意,记得用git rm来删除,否则最后上传到git上还是有的,除非你先删除整个分支,不过就显得过于麻烦了。既然删除了JB提供的套件,其实_config.yml文件里面相应的部分也可以删除了。为了简洁和减少错误着想,都删了比较好,也不会以后看到突然不知道是什么。

disqus基本操作都和analytics一样,就不复述了,记得自己去disqus.com注册申请帐号然后得到自己的shortname。选择install instruction的时候点universal code,复制粘贴到post.html文件的comment段就可以了.

<div id="disqus_thread"></div>
<script type="text/javascript">
/* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
var disqus_shortname = 'slixurd'; // required: replace example with your forum shortname    /* * * DON'T EDIT BELOW THIS LINE * * */
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>

Jekyll bootstrap架构

  • _layout文件夹里面有3个文件,default,page,post。分别对应3种页面的格式。很显然,就是自己建立的文章的layout,就像一般写blog要求在_posts文件夹下直接用rake生成一样。实质就是引用了post格式。打开3个文件都可以看到源代码。什么都没做,例如page只是引用了

    { % include JB/setup %}
    { % include themes/twitter/page.html %}

    仅此而已。应该是为了方便切换theme所设计的。

  • _includes/themes/twitter/就是主要的页面layout了。需要增加的可以自行添加html,记得留下content就可以配合markdown一起使用了。

  • _includes/JB这个文件夹就是一些作者写的扩展功能,例如taglist里面如何提取相同tag形成list的。当然用的都是liquid语言。
  • _asset是整个页面的布局,包括图片,包括css。如果想要修改颜色,背景图片,为webkit加上透明,阴影各种效果都是写在css里面。最后还有一个_site文件夹。这个文件夹是jekyll —server编译出来的,就是别人所能见到的网站,当然据说github编译的时候,都会加上safe参数,这个文件夹在github上看不到的。
  • 根目录

    • CNAME显而易见解析使用的,内容只有一句,自己的域名。当然在此之前需要先修改自己domain的A记录,改为github的地址,ping下可以得到。
    • _config.yml估计看过作者介绍的人也不会不知道这是什么了。如果不知道的话,不想看英文的话,有博客简单翻译了一部分,基本足够用。http://www.mceiba.com/develop/jekyll-introduction.html
    • **.html 这个就是你的顶栏会出现的页面,或者应该说你点进pages以后会看到的所有网页。默认的顶栏有5个。分别对应5个html文件(Archive,Tags,Categories,Pages,还有index),需要自己在顶栏增加页面参照模板来增加就可以了,例如about页面。需要的只是选择page这种layout,然后包含JB自带的设定,也就是include JB/setup这一句,剩下的部分就自己补全吧,想用md写也行,直接写html也行。最后出现在_site的结果是一样的。
  • markdown会鸟语的直接看鸟语吧,不会的可以看翻译版本http://wowubuntu.com/markdown/#autolink,鸟语版本链接:http://daringfireball.net/projects/markdown/syntax

新し始まる時


随着经历的事情越多,随着时间的流逝,才会发现很多是和自己想象的不同的,或者应该说,随着心智的成长,才能够承认很多东西是自己无法做到自己比不上别人的。就像文章,很多年以前,语文老师对另外一个同学说:你的逻辑思维很好,在文章里面处处都有体现。当时的我却无法承认这点,我觉得我也能写成这样,但是现在,才逐渐敢于承认,无论怎样去用心,写出来的东西依然如同一堆散沙,即使刻意的围绕一个中心话题去讲,最后还是会偏离到其他的地方去,不过很矛盾的是,记录本身难道不是对事情的叙述而已么?和感想不同,不,其实我也并不明白自己记录的意义,担心在远的无法记住的将来丢失了对此刻的感觉还是担心忘记了此刻的事件本身。虽然知道有很多片段已经独立于事件本身能多次想起,但是对于很多片段的感觉却无法回忆,每次想到那应该是很重要的东西,不能忘记,就会头脑发热有种莫名的执着。

前面说了这么多,还是要说一句:

occupied! hello world
在得知jekyll+github很久很久以后才最终下定决心来建立自己的页面,当然,还欠缺自己的修改,在目录中翻来翻去寻找着顶栏到底是怎么写的,还有jeykll莫名其妙的语法 { include JB/setup }还有markdown+jekyll带来的特殊的问题,如何输入“%”,当然我说的是在刚才的代码当中,加入以后就会告知编译错误。今天已经花了很多时间在这上面了,以后有时间再来慢慢修改,包括配色,包括侧栏,包括顶栏,包括扩展功能,总是需要慢慢折腾的。GITHUB之前也是如此,我在7个月前就建立了那个游戏的repo,但是花了1个小时,不管怎样也无法将自己的仓库push到服务器端,今天从头开始,最后还是OK了。下面就记录一下顺序以及错误的地方。



git remote add origin 时出现错误: fatal: remote origin already exists.
执行git remote rm origin后再git remote add origin
ssh-key建立生成成功以后但是在ssh连接的时候出现Agent admitted failure to sign using the key的时候,需要手动执行添加
Identity added: /home/user/.ssh/id_rsa
另外安装完ruby之后如果无法用rem 安装jeykll,需要安装rake以后才行。安装rake和ruby就很简单了,sudo apt-get install ruby1.9.1 rake就可以了,如果发现还是失败= =,那就回滚ruby1.8吧。



最近终于开始做第二个android的应用了,很久很久没有自己写过android的东西,有点手生,2个小时仅仅写了170行,后面还需要将不少模块单独成class并且扩展功能,我也是第一次接触到httpclient和服务器之间的传输。虽然现在有人在写服务器,不过我还是想自己动手看看服务器到底是个什么样的东西,我们用的是apache,等先把手头上需要写完的类都写完了,抽空看看apache。难得突然如此有干劲的想要学习,上一次有这个感觉的时候是暑假的中段的时候了。虽然当时看的WINDOWS PROGRAMMING 和MFC随着我现在使用UBUNTU完全废弃了,不过也许以后有用吧,尽管我学习的深度完全只能用来装B骗骗完全不懂不懂的人而已。



现在每天的日常就是背英语单词,背日语单词,打开ubuntu看看还有什么东西是没有接触过的好好学习使用一个新系统所需要的东西。今天就看到了lsof+nethogs+ifconfig+ps -a来查看程序占用的网络带宽+读取文件。主要是因为git在push的时候会一直停留在一个界面,当时想查看是否死机了,所以就想到查看被读取文件。事实证明,完全可以,虽然可能有更好的工具组合,不过目前使用足够了,这也让我对未来将会开设的tcp协议倍感兴趣(看起来MS没有什么关系,不过问题不大)。背一下单词然后准备继续金工实习。晚安。