精华内容
下载资源
问答
  • 但是其中文分词效果不是很理想,如“贵州财经大学”总是切分成“贵州”、“财经”和“大学”,这是因为词典这些词的权重较高。这篇文章主要介绍最经典的自然语言处理工具之一——Jieba,包括中文分词、添加自定义...

    本系列文章主要结合Python语言实现知识图谱构建相关工程,具有一定创新性和实用性,非常希望各位博友交流讨论,相互促进成长。前面两篇文章详细讲解了哈工大Pyltp工具,包括中文分词、词性标注、实体识别、依存句法分析和语义角色标注等。但是其中文分词效果不是很理想,如“贵州财经大学”总是切分成“贵州”、“财经”和“大学”,这是因为词典中这些词的权重较高。这篇文章主要介绍最经典的自然语言处理工具之一——Jieba,包括中文分词、添加自定义词典及词性标注等内容。

    知识图谱系列文章:
    [知识图谱实战篇] 一.数据抓取之Python3抓取JSON格式的电影实体
    [知识图谱实战篇] 二.Json+Seaborn可视化展示电影实体
    [知识图谱实战篇] 三.Python提取JSON数据、HTML+D3构建基本可视化布局
    [知识图谱实战篇] 四.HTML+D3+CSS绘制关系图谱
    [知识图谱实战篇] 五.HTML+D3添加鼠标响应事件显示相关节点及边
    [知识图谱实战篇] 六.HTML+D3实现点击节点显示相关属性及属性值
    [知识图谱实战篇] 七.HTML+D3实现关系图谱搜索功能
    [知识图谱实战篇] 八.HTML+D3绘制时间轴线及显示实体

    环境下载地址:https://download.csdn.net/download/eastmount/11226539

    原文地址:
    [Python知识图谱] 一.哈工大pyltp安装及中文分句、中文分词、导入词典基本用法
    [Python知识图谱] 二.哈工大pyltp词性标注、命名实体识别、依存句法分析和语义角色标注

    参考前文:
    [python] 使用Jieba工具中文分词及文本聚类概念 - eastmount
    杨秀璋. Python网络数据爬取及分析从入门到精通(分析篇). 北京:北京航空航天大学出版社[M]. 2018


    一.Jieba工具

    在数据分析和数据挖掘中,通常需要经历前期准备、数据爬取、数据预处理、数据分析、数据可视化、评估分析等步骤,而数据分析之前的工作几乎要花费数据工程师近一半的工作时间,其中的数据预处理也将直接影响后续模型分析的好坏。

    数据预处理(Data Preprocessing)是指在进行数据分析之前,对数据进行的一些初步处理,包括缺失值填写、噪声处理、不一致数据修正、中文分词等,其目标是得到更标准、高质量的数据,纠正错误异常数据,从而提升分析的结果。
    下图是中文文本预处理的基本步骤,包括中文分词、词性标注、数据清洗、特征提取(向量空间模型存储)、权重计算(TF-IDF)等

    “结巴”(Jieba)工具是最常用的中文文本分词和处理的工具之一,它能实现中文分词、词性标注、关键词抽取、获取词语位置等功能。其在Github网站上的介绍及下载地址为:https://github.com/fxsjy/jieba

    作者推荐大家使用PIP工具来安装Jieba中文分词包,安装语句如下:

    pip install jieba
    

    调用命令“pip install jieba”安装jieba中文分词包如下图所示。

    安装过程中的会显示安装配置相关包和文件的百分比,直到出现“Successfully installed jieba-0.37”命令,表示安装成功。同时,在安装过程中会遇到各种问题,大家一定要学会独立搜索答案解决这些问题,才能提升您独立解决问题的能力。


    Jieba工具具有有以下特点:

    • 支持三种分词模式,包括精确模式、全模式和搜索引擎模式
    • 支持繁体分词
    • 支持自定义词典
    • 代码对Python2和Python3均兼容
    • 支持多种编程语言,包括Java、C++、Rust、PHP、R、Node.js等

    Jieba工具主要使用的算法包括:(1)通过基于前缀词典实现高效的词图扫描,构建句子中汉字所有可能生成词情况的有向无环图;(2)采用动态规划查找最大概率路径,寻找基于词频的最大切分组合;(3)对于未登录词,采用基于汉字成词能力的HMM模型结合Viterbi算法。


    二.中文分词

    1.基础知识

    由于中文词语之间是紧密联系的,一个汉语句子是由一串前后连续的汉字组成,词与词之间没有明显的分界标志,所以需要通过一定的分词技术把句子分割成空格连接的词序列,这就是所谓的中文分词技术。

    中文分词(Chinese Word Segmentation)指将汉字序列切分成一个个单独的词或词串序列,它能够在没有词边界的中文字符串中建立分隔标志,通常采用空格分隔。中文分词是数据分析预处理、数据挖掘、文本挖掘、搜索引擎、知识图谱、自然语言处理等领域中非常基础的知识点,只有经过中文分词后的语料才能转换为数学向量的形式,继续进行后面的分析。同时,由于中文数据集涉及到语义、歧义等知识,划分难度较大,比英文复杂很多。

    下面举个简单示例,对句子“我是程序员”进行分词操作。

    输入:我是程序员
    输出1:我\是\程\序\员
    输出2:我是\是程\程序\序员
    输出3:我\是\程序员

    这里分别采用了三种方法介绍中文分词。“我\是\程\序\员”采用的是一元分词法,将中文字符串分隔为单个汉字;“我是\是程\程序\序员”采用二元分词法,将中文汉字两两分隔;“我\是\程序员”是比较复杂但更实用的分词方法,它根据中文语义来进行分词的,其分词结果更准确。

    中文分词方法有很多,常见的包括基于字符串匹配的分词方法、基于统计的分词方法和基于语义的分词方法等。这里介绍比较经典的基于字符串匹配的分词方法。

    基于字符串匹配的分词方法又称为基于字典的分词方法,它按照一定策略将待分析的中文字符串与机器词典中的词条进行匹配,若在词典中找到某个字符串,则匹配成功,并识别出对应的词语。该方法的匹配原则包括最大匹配法(MM)、逆向最大匹配法(RMM)、逐词遍历法、最佳匹配法(OM)、并行分词法等。

    正向最大匹配法的步骤如下,假设自动分词词典中的最长词条所含汉字的个数为n。

    • 从被处理文本中选取当前中文字符串中的前n个中文汉字作为匹配字段,查找分词词典,若词典中存在这样一个n字词,则匹配成功,匹配字段作为一个词被切分出来。
    • 若分词词典中找不到这样的一个n字词,则匹配失败,匹配字段去掉最后一个汉字,剩下的中文字符作为新的匹配字段,继续进行匹配。
    • 循环前两个步骤进行匹配,直到匹配成功为止。

    例如,现在存在一个句子“北京理工大学生前来应聘”,使用正向最大匹配方法进行中文分词的过程如下所示。

    2.基本用法

    首先读者看一段简单的结巴分词代码,主要调用两个函数实现。

    • jieba.cut(text,cut_all=True)
      分词函数,第一个参数是需要分词的字符串,第二个参数表示是否为全模式。分词返回的结果是一个可迭代的生成器(generator),可使用for循环来获取分词后的每个词语,更推荐读者转换为list列表再使用。
    • jieba.cut_for_search(text)
      搜索引擎模式分词,参数为分词的字符串,该方法适合用于搜索引擎构造倒排索引的分词,粒度比较细。
    #encoding=utf-8  
    import jieba  
      
    text = "小杨毕业于北京理工大学,从事Python人工智能相关工作。"  
    
    #全模式
    data = jieba.cut(text,cut_all=True)
    print(type(data))
    print(u"[全模式]: ", "/".join(data))
    
    #精确模式  
    data = jieba.cut(text,cut_all=False)
    print(u"[精确模式]: ", "/".join(data))
    
    #默认是精确模式 
    data = jieba.cut(text)  
    print(u"[默认模式]: ", "/".join(data))
    
    #搜索引擎模式 
    data = jieba.cut_for_search(text)    
    print(u"[搜索引擎模式]: ", "/".join(data))
    
    #返回列表
    seg_list = jieba.lcut(text, cut_all=False)
    print("[返回列表]: {0}".format(seg_list))
    

    输出结果如下所示。

    最终的分词结果比较理想,其中精确模式输出的“小/杨/毕业/于/北京理工大学/,/从事/Python/人工智能/相关/工作/。”比较精准。下面简单叙述结巴中文分词的三种分词模式。

    全模式
    该模式将语料中所有可以组合成词的词语都构建出来,其优点是速度非常快,缺点是不能解决歧义问题,并且分词结果不太准确。其分词结果为“小/杨/毕业/于/北京/北京理工/北京理工大学/理工/理工大/理工大学/工大/大学///从事/Python/人工/人工智能/智能/相关/工作//”。

    精确模式
    该模式利用其算法将句子最精确地分隔开,适合文本分析,通常采用这种模式进行中文分词。其分词结果为“小/杨/毕业/于/北京理工大学/,/从事/Python/人工智能/相关/工作/。”,其中“北京理工大学”、“人工智能”这些完整的名词被精准识别,但也有部分词未被识别,后续导入词典可以实现专有词汇识别。

    搜索引擎模式
    该模式是在精确模式基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。其结果为“小/杨/毕业/于/北京/理工/工大/大学/理工大/北京理工大学/,/从事/Python/人工/智能/人工智能/相关/工作/。”。

    3.基于HMM模型的中文分词

    隐马尔可夫模型(Hidden Markov Model, HMM)是一种基于概率的统计分析模型,用来描述一个系统隐性状态的转移和隐性状态的表现概率。到目前为止,HMM模型被认为是解决大多数自然语言处理问题最为快速、有效的方法之一。它成功解决了语义识别、机器翻译等问题。

    在Jieba工具中,对于未登录到词库的词,使用了基于汉字成词能力的 HMM 模型和 Viterbi 算法,其大致原理是采用四个隐含状态,分别表示为单字成词、词组的开头、词组的中间和词组的结尾。通过标注好的分词训练集,可以得到 HMM的各个参数,然后使用 Viterbi 算法来解释测试集,得到分词结果。

    #encoding=utf-8  
    import jieba  
      
    text = "他来到了网易杭研大厦工作,我继续去北理读研。"  
    #精确模式  
    data = jieba.cut(text, cut_all=False, HMM=False)
    print(u"[精确模式]: ", "/".join(data))
    #精确模式+HMM
    data = jieba.cut(text, cut_all=False, HMM=True)
    print(u"[精确模式]: ", "/".join(data))
    

    输出结果如下图所示,未启用HMM模型时,它无法识别“杭研”、“北理”词语,将其拆分成了“杭”、“研”和“北”、“理”,而启用HMM模型时,它有效识别了新词“杭研”、“北理”。

    4.繁体中文分词

    Jieba工具支持中文繁体字的识别,将前面示例转换为繁体字,即“小楊畢業於北京理工大學,從事Python人工智能相關工作。”,调用Jieba分词的代码如下所示。

    #encoding=utf-8  
    import jieba  
      
    text = "小楊畢業於北京理工大學,從事Python人工智能相關工作。"  
    
    #全模式
    data = jieba.cut(text, cut_all=True)
    print(type(data))
    print(u"[全模式]: ", "/".join(data))
    
    #精确模式  
    data = jieba.cut(text, cut_all=False)
    print(u"[精确模式]: ", "/".join(data))
    
    #搜索引擎模式 
    data = jieba.cut_for_search(text)    
    print(u"[搜索引擎模式]: ", "/".join(data))
    

    输出结果如下所示:

    [全模式]://///北京/北京理工/理工/理工大/工大//////Python/人工/人工智能/智能///工作//
    [精确模式]://畢業//北京理工/大學//從事/Python/人工智能/相關/工作/[搜索引擎模式]://畢業//北京/理工/北京理工/大學//從事/Python/人工/智能/人工智能/相關/工作/

    三.添加自定义词典

    1.添加词典

    在进行中文分词过程中,通常会遇到一些专用词语无法精准的切分,比如“乾清宫”会被分割为“乾”、“清宫”,“黄果树瀑布”会被分割为“黄果树”、“瀑布”,“云计算”会被分割为“云”、“计算”,“贵州财经大学”会被分割为“贵州”、“财经大学”等。虽然Jieba工具有新词识别能力,但也无法识别出所有Jieba词库里没有的词语,但它为开发者提供了添加自定义词典功能,从而保证更好的分词正确率。其函数原型如下:

    • load_userdict(f)
      该函数只有一个参数,表示载入的自定义词典路径,f 为文件类对象或自定义词典路径下的文件。词典的格式为:一个词占一行,每行分为三部分:
    word  freq  word_type
    

    其中,word为对应的词语,freq为词频(可省略),word_type为词性(可省了),中间用空格隔开,顺序不可以改变。注意,文件必须为UTF-8编码。

    下面举例讲解,第一个代码是未导入自定义词典的中文分词。

    #encoding=utf-8  
    import jieba  
      
    text = "小杨在贵州财经大学工作,擅长大数据、云计算,喜欢乾清宫、黄果树瀑布等景区。"  
    
    #精确模式  
    data = jieba.cut(text, cut_all=False)
    print(u"[原始文本]: ", text)
    print(u"[精确模式]: ", " ".join(data))
    

    输出结果未将“贵州财经大学”、“大数据”、“云计算”、“乾清宫”、“黄果树瀑布”等词汇正确分割。

    接着导入自定义词典,其本地词典命名为“dict.txt”,如图所示,包括设置“贵州财经大学”的词性为机构名词“nt”,“大数据”、“云计算”的词性为名词“n”,也有省略词性和词频的“乾清宫”。

    完整代码如下:

    #encoding=utf-8  
    import jieba  
    
    text = "杨秀璋在贵州财经大学工作,擅长大数据、云计算,喜欢乾清宫、黄果树瀑布等景区。"  
    
    #导入自定义词典
    jieba.load_userdict("dict.txt")
    #精确模式  
    data = jieba.cut(text, cut_all=False)
    print(u"[原始文本]: ", text)
    print(u"[精确模式]: ", " ".join(data))
    

    此时的输出结果有效地提取了“贵州财经大学”、“云计算”、“乾清宫”。但也有两个未识别出的词语,“黄果树瀑布”不在词典中,故被拆分成了“黄果树”和“瀑布”,“大数据”虽然在词典中,却仍然拆分成了“大”和“数据”。

    2.动态修改词典

    在Jieba工具中,可以在程序中动态修改词典,通过add_word(word, freq=None, tag=None)函数添加新词语,通过del_word(word)函数删除自定义词语。

    #encoding=utf-8  
    import jieba  
    
    text = "小杨在贵州财经大学工作,擅长大数据、云计算,喜欢乾清宫、黄果树瀑布等景区。"  
    
    #导入自定义词典
    jieba.load_userdict("dict.txt")
    
    #添加自定义词语
    jieba.add_word("小杨")
    jieba.add_word("黄果树瀑布")
    jieba.add_word("自然语言处理", freq=10, tag="nz")
    
    #删除自定义词语
    jieba.del_word("北理工")
    
    #精确模式  
    data = jieba.cut(text, cut_all=False)
    print(u"[原始文本]: ", text, "\n")
    print(u"[精确模式]: ", " ".join(data))
    

    该代码增加了新词语“小杨”、“黄果树瀑布”和“自然语言处理”,删除了“北理工”,其运行结果如下所示,它有效地将“小杨”和“黄果树瀑布”进行了精准识别。

    此时,读者可能会有一个疑问,为什么“大数据”被拆分成了“大”和“数据”呢?这是因为Jieba词库中“大”和“数据”的重要程度更高,我们可以使用suggest_freq(segment, tune=True)函数调节单个词语的词频,使其被分割出来。

    代码如下所示:

    #encoding=utf-8  
    import jieba  
    
    text = "小杨在贵州财经大学工作,擅长大数据、云计算,喜欢乾清宫、黄果树瀑布等景区。"  
    
    #导入自定义词典
    jieba.load_userdict("dict.txt")
    
    #添加自定义词语
    jieba.add_word("小杨")
    jieba.add_word("黄果树瀑布")
    jieba.add_word("自然语言处理", freq=10, tag="nz")
    
    #删除自定义词语
    jieba.del_word("北理工")
    
    #调节词频
    jieba.suggest_freq('大数据', True)
    
    #精确模式  
    data = jieba.cut(text, cut_all=False)
    print(u"[原始文本]: ", text, "\n")
    print(u"[精确模式]: ", " ".join(data))
    

    最终的输出结果为“小杨贵州财经大学 工作 , 擅长 大数据云计算 , 喜欢 乾清宫黄果树瀑布 等 景区 。”

    终于将“小杨”、“贵州财经大学”、“大数据”、“云计算”、“乾清宫”、“黄果树瀑布”等专有词语识别出来。同时,如果自定义词典存在很多专用名词,并且需要设置它们的高权重,可以使用下面的代码循环设置每一个词语的词频。

    #循环设置词频重要程度
    fp = open("dict.txt", 'r', encoding='utf8')
    for line in fp:
        line = line.strip()
    jieba.suggest_freq(line, True)
    
    #第二种方法
    [jieba.suggest_freq(line.strip(), True) for line in open("dict.txt",'r',encoding='utf8')]
    

    四. 词性标注

    词性标注(Part-Of-Speech Tagging, POS Tagging)也被称为语法标注(Grammatical Tagging)或词类消疑(Word-category Disambiguation),是将语料库内单词的词性按其含义和上下文内容进行标记的文本数据处理技术。通过词性标注处理,可以将分词得到的词序列中每个单词标注一个正确的词性。

    在Jieba工具中,调用jieba.posseg.POSTokenizer(tokenizer=None)函数新建自定义分词器。tokenizer参数可指定内部使用的jieba.Tokenizer分词器,jieba.posseg.dt为默认词性标注分词器。Jieba工具采用和Ictclas 兼容的标记法,标注句子分词后每个词的词性通过循环输出。表4-1为Jieba工具的各个词性及含义。

    官方文档给出的示例如下所示,通过“import jieba.posseg as pseg”语句导入扩展包,接着循环输出word(词语)和flag(词性)值。

    #encoding=utf-8  
    import jieba.posseg as pseg
    
    #词性标注
    words = pseg.cut("我爱北京天安门")
    for word, flag in words:
        print('%s %s' % (word, flag))
    

    输出结果如图所示,其中“我”表示代词,对应“r”;“爱”对应动词,对应“v”,“北京”和“天安门”对应地点名词,对应“ns”。

    上面小节的示例对应的词性标注代码如下所示。

    #encoding=utf-8  
    import jieba  
    import jieba.posseg
    import jieba.analyse
    
    text = "小杨在贵州财经大学工作,擅长大数据、云计算,喜欢乾清宫、黄果树瀑布等景区。"  
    
    #导入自定义词典
    jieba.load_userdict("dict.txt")
    
    #添加自定义词语
    jieba.add_word("小杨")
    jieba.add_word("黄果树瀑布")
    #调节词频
    jieba.suggest_freq('大数据', True)
    
    #精确模式  
    data = jieba.cut(text, cut_all=False)
    print(u"[原始文本]: ", text, "\n")
    print(u"[精确模式]: ", " ".join(data), "\n")
    
    #词性标注
    sentence_seged = jieba.posseg.cut(text)
    outstr = ''
    for x in sentence_seged:
        outstr += "{}/{}  ".format(x.word, x.flag)
    print(u'[词性标注]:', outstr)
    

    输出结果如图所示。


    五.总结

    希望这篇基础性文章对你有所帮助,如果有错误或不足之处,还请海涵。下一篇将详细讲解Jieba工具实现关键词抽取、事件抽取和命名实体识别等例子。望喜欢~同时,后续分享更多的是网络安全方面的文章了,从零开始很难,但秀璋会一路走下去的,加油。

    故人应在千里外,
    不寄梅花远信来。
    武汉第一周学习结束,寄了第一封家书给女神,接下来这几年,应该会写上200来封吧,很期待,也很漫长,但我俩的故事将继续书写。

    今天早上做了我来这的第一次PPT汇报,是关于网络安全和数据挖掘、人工智能的融合,也听了学弟们的汇报,真的学到好多,也记录了无数未知的词汇,接下来要学的更多。这一周给我最深刻的印象就是:学弟学妹们怎么这么刻苦?早上去得很早,中午不回去午休,晚上还继续敲代码,而且本科大三的学弟们也很多,在没有老师的情况下,小组自发组织PPT汇报探讨,仿佛我是那个最“偷懒”的学生,我猜:肯定是外面太热,他们都想蹭空调!

    真的很感激这次来之不易的学习机会,感谢所有的老师、师兄师姐、师弟师妹,更感激家人朋友和女神。也谢谢CSDN认识的博友请我吃饭,武大测绘本硕博连读让我甚是佩服,真的通过CSDN认识了很多朋友,大家分散五湖四海,共同进步。

    未来的路还很长,优秀的人真的太多,我们只有做好自己,不忘初心,享受生活,砥砺前行。明天周末继续奋斗,晚安娜,记得收信

    (By:杨秀璋 2019-07-28 周日下午5点写于武大 https://blog.csdn.net/Eastmount )

    展开全文
  • 面向对象设计的基本原则

    千次阅读 2014-01-11 15:38:13
    面向对象设计的基本原则 此文译自CodeProject上一文,该文章在Top Articles上排名第3,读了之后觉得非常好,就翻译出来,供不想读英文的同学参考学习。作者(Shubho)的妻子(Farhana)打算重新做一 名软件工程师...
    面向对象设计的基本原则
    此文译自CodeProject上一文,该文章在Top  Articles上排名第3,读了之后觉得非常好,就翻译出来,供不想读英文的同学参考学习。作者(Shubho)的妻子(Farhana)打算重新做一 名软件工程师(她本来是,后来因为他们孩子出生放弃了),于是作者就试图根据自己在软件开发设计方面的经验帮助她学习面向对象设计(OOD)。

      自作者从事软件开发开始,作者常常注意到不管技术问题看起来多复杂,如果从现实生活的角度解释并以对答的方式讨论,那么它将变得更简单。现在他们把在OOD方面有些富有成效的对话分享出来,你可能会发现那是一种学习OOD很有意思的方式。

      

    下面就是他们的对话:

    OOD简介

    Shubho:亲爱的,让我们开始学习OOD吧。你了解面向对象原则吗?

    Farhana:你是说封装,继承,多态对吗?我知道的。

    Shubho:好,我希望你已了解如何使用类和对象。今天我们学习OOD。

    Farhana:等一下。面向对象原则对面向对象编程(OOP)来说不够吗?我的意思是我会定义类,并封装属性和方法。我也能根据类的关系定义它们之间的层次。如果是,那么还有什么?

    Shubho:问得好。面向对象原则和OOD实际上是两个不同的方面。让我给你举个实际生活中的例子帮你弄明白。

    再你小时候你首先学会字母表,对吗?

    Farhana:嗯

    Shubho:好。你也学了单词,并学会如何根据字母表造词。后来你学会了一些造句的语法。例如时态,介词,连词和其他一些让你能造出语法正确的句子。例如:

    "I" (代词) "want" (动词) "to" (介词) "learn" (动词) "OOD"(名词)。

    看,你按照某些规则组合了单词,并且你选择了有某些意义的正确的单词结束了句子。

    Farhana:OK,这意味着什么呢?

    Shubho:面向对象原则与这类似。OOP指的是面向对象编程的基本原则和核心思路。在这里,OOP可以比作英语基础语法,这些语法教你如何用单词构造有意义且正确的句子,OOP教你在代 码中构造类,并在类里封装属性和方法,同时构造他们之间的层次关系。

    Farhana:嗯..我有点感觉了,这里有OOD吗?

    Shubho:马上就有答案。现在假定你需要就某些主题写几篇文章或随笔。你也希望就几个你擅长主体写几本书。对写好文章/随笔或书来说,知道如何造句是不够的,对吗?为了使读者能更轻 松的明白你讲的内容,你需要写更多的内容,学习以更好的方式解释它。

    Farhana:看起来有点意思...继续。

    Shubho:现在,如果你想就某个主题写一本书,如学习OOD,你知道如何把一个主题分为几个子主题。你需要 为这些题目写几章内容,也需要在这些章节中写前言,简介,例子和其他段落。  你需要为写个整体框架,并学习一些很好的写作技巧以便读者能更容易明白你要说的内容。这就是整体规划。

    在软件开发中,OOD是整体思路。在某种程度上,设计软件时,你的类和代码需能达到模块化,可复用,且灵活,这些很不错的指导原则不用你重新发明创造。确实有些原则你已经在你的类和对象中已经用到了,对吗?

    Farhana:嗯...有个大概的印象了,但需要继续深入。

    Shubho:别担心,你马上就会学到。我们继续讨论下去。

     

    为什么要OOD?

    Shubho:这是一个非常重要的问题。当我们能很快地设计一些类,完成开发并发布时,为什么我们需要关心OOD?那样子还不够吗?

    Farhana:嗯,我早先并不知道OOD,我一直就是开发并发布项目。那么关键是什么?

    Shubho:好的,我先给你一句名言:

    走在结冰的河边不会湿鞋,开发需求不变的项目畅通无阻(Walking on water and developing software from a specification are easy if both are frozen)

    -Edward V. Berard

    Farhana:你的意思是软件开发说明书会不断变化?

    Shubho:非常正确!软件开发唯一的真理是“软件一定会变化”。为什么?

    因为你的软件解决的是现实生活中的业务问题,而现实生活中得业务流程总是在不停的变化。

    假设你的软件在今天工作的很好。但它能灵活的支持“变化”吗?如果不能,那么你就没有一个设计敏捷的软件。

    Farhana:好,那么请解释一下“设计敏捷的软件”。

    Shubho:"一个设计敏捷的软件能轻松应对变化,能被扩展,并且能被复用。"

    并且应用好"面向对象设计"是做到敏捷设计的关键。那么,你什么时候能说你在代码中很好的应用了OOD?

    Farhana:这正是我的问题。

    Shubho:如果你代码能做到以下几点,那么你就正在OOD:

    面向对象

    复用

    能以最小的代价满足变化

    不用改变现有代码满足扩展

    Farhana:还有?

    Shubho:我们并不是孤立的。很多人在这个问题上思考了很多,也花费了很大努力,他们试图做好OOD,并为OOD指出几条基本的原则(那些灵感你能用之于你的OOD)。他们最终也确实总结出了一些通用的设计模式(基于基本的原则)。

    Farhana:你能说几个吗?

    Shubho:当然。这里有很多涉及原则,但最基本的是叫做SOLID的5原则(感谢Uncle Bob,伟大OOD导师)。

    S = 单一职责原则 Single Responsibility Principle

    O = 开放闭合原则 Opened Closed Principle

    L = Liscov替换原则 Liscov Substitution Principle

    I = 接口隔离原则 Interface Segregation Principle

    D = 依赖倒置原则 Dependency Inversion Principle

    接下去,我们会仔细探讨每一个原则。

    单一职责原则

    Shubho:我先给你展示一张海报。我们应当谢谢做这张海报的人,它非常有意思。

    单一职责原则海报

    它说:"并不是因为你能,你就应该做"。为什么?因为长远来看它会带来很多管理问题。

    从面向对象角度解释为:"引起类变化的因素永远不要多于一个。"

    或者说"一个类有且只有一个职责"。

    Farhana:能解释一下吗?

    Shubho:当然,这个原则是说,如果你的类有多于一个原因会导致它变化(或者多于一个职责),你需要一句它们的职责把这个类拆分为多个类。

    Farhana:嗯...这是不是意味着在一个类里不能有多个方法?

    Shubho:不。你当然可以在一个类中包含多个方法。问题是,他们都是为了一个目的。如今为什么拆分是重要的?

    那是因为:

    每个职责是轴向变化的;

    如果类包含多个职责,代码会变得耦合;

    Farhana:能给我一个例子吗?

    Shubho:当然,看一下下面的类层次。当然这个例子是从Uncle Bob那里得来,再谢谢他。

    违反单一职责原则的类结构图

    这里,Rectangle类做了下面两件事:

    计算矩形面积;

    在界面上绘制矩形;

    并且,有两个应用使用了Rectangle类:

    计算几何应用程序用这个类计算面积;

    图形程序用这个类在界面上绘制矩形;

    这违反了SRP(单一职责原则);

    Farhana:如何违反的?

    Shubho:你看,Rectangle类做了两件事。在一个方法里它计算了面积,在另外一个方法了它返回一个表示矩形的GUI。这会带来一些有趣的问题:

    在计算几何应用程序中我们必须包含GUI。也就是在开发几何应用时,我们必须引用GUI库;

    图形应用中Rectangle类的变化可能导致计算几何应用变化,编译和测试,反之亦然;

    Farhana:有点意思。那么我猜我们应该依据职责拆分这个类,对吗?

    Shubho:非常对,你猜我们应该做些什么?

    Farhana:当然,我试试。下面是我们可能要做的:

    拆分职责到两个不同的类中,如:

    Rectangle:这个类应该定义Area()方法;

    RectangleUI:这个类应继承Rectangle类,并定义Draw()方法。

    Shubho:非常好。在这里,Rectangle类被计算几何应用使用,而RectangleUI被图形应用使用。我们甚至可以分离这些类到两个独立的DLL中,那会允许我们在变化时不需要关心另一个就可以实现它。

    Farhana:谢谢,我想我明白SRP了。SRP看起来是把事物分离成分子部分,以便于能被复用和集中管理。我们也不能把SRP用到方法级别吗?我的意思是,我们可以写一些方法,它们包含做很多事的代码。这些方法可能违反SRP,对吗?

    Shubho:你理解了。你应当分解你的方法,让每个方法只做某一项工作。那样允许你复用方法,并且一旦出现变化,你能购以修改最少的代码满足变化。

     

    开放闭合原则

    Shubho:这里是开放闭合原则的海报

    开放闭合原则海报

    从面向对象设计角度看,它可以这么说:"软件实体(类,模块,函数等等)应当对扩展开放,对修改闭合。"

    通俗来讲,它意味着你应当能在不修改类的前提下扩展一个类的行为。就好像我不需要改变我的身体而可以穿上衣服。

    Farhana:有趣。你能够按照你意愿穿上不同的衣服来改变面貌,而从不用改造身体。你对扩展开放了,对不?

    Shubho:是的。在OOD里,对扩展开发意味着类或模块的行为能够改变,在需求变化时我们能以新的,不同的方式让模块改变,或者在新的应用中满足需求。

    Farhana:并且你的身体对修改是闭合的。我喜欢这个例子。当需要变化时,核心类或模块的源代码不应当改动。你能用些例子解释一下吗?

    Shubho:当然,看下面这个例子。它不支持"开放闭合"原则。

    违反开发闭合原则的类结构

    你看,客户端和服务段都耦合在一起。那么,只要出现任何变化,服务端变化了,客户端一样需要改变。

    Farhana:理解。如果一个浏览器以紧耦合的方式按照指定的服务器(比如IIS)实现,那么如果服务器因为某些原因被其他服务器(如Apache)替换了,那么浏览器也需要修改或替换。这确实很可怕!

    Shubho:对的。下面是正确的设计。

    遵循开放闭合原则的类结构

    在这个例子中,添加了一个抽象的服务器类,客户端包含一个抽象类的引用,具体的服务类实现了抽象服务类。那么,因任何原因引起服务实现发生变化时,客户端都不需要任何改变。

    这里抽象服务类对修改是闭合的,实体类的实现对扩展是开放的。

    Farhana:我明白了,抽象是关键,对吗?

    Shubho:是的,基本上,你抽象的东西是你系统的核心内容,如果你抽象的好,很可能在扩展功能时它不需要任 何修改(就像服务是一个抽象概念)。如果在实现里定义了抽象的东西(比如IIS服务器实现的服务),代码要尽可能以抽象(服务)为依据。这会允许你扩展抽 象事物,定义一个新的实现(如Apache服务器)而不需要修改任何客户端代码。

     

    Liskov's 替换原则

    Shubho:"Liskov's替换原则(LSP)"听起来很难,却是很有用的基本概念。看下这幅有趣的海报:

    Liskov替换原则海报

    这个原则意思是:"子类型必须能够替换它们基类型。"

    或者换个说法:"使用基类引用的函数必须能使用继承类的对象而不必知道它。"

    Farhana:不好意思,听起来有点困惑。我认为这个OOP的基本原则之一。也就是多态,对吗?为什么一个面向对象原则需要这么说呢?

    Shubho:问的好。这就是你的答案:

    在基本的面向对象原则里,"继承"通常是"is a"的关系。如果"Developer" 是一个"SoftwareProfessional",那么"Developer"类应当继承"SoftwareProfessional"类。在类设计 中"Is a"关系非常重要,但它容易冲昏头脑,结果使用错误的继承造成错误设计。

    "Liskov替换原则"正是保证继承能够被正确使用的方法。

    Farhana:我明白了。有意思。

    Shubho:是的,亲爱的,确实。我们看个例子:

    Liskov替换原则类结构图

    这里,KingFisher类扩展了Bird基类,并继承了Fly()方法,这看起来没问题。

    现在看下面的例子:

    违反Liskov替换原则类结构图

    Ostrich(鸵鸟)是一种鸟(显然是),并从Bird类继承。它能飞吗?不能,这个设计就违反了LSP。

    所以,即使在现实中看起来没问题,在类设计中,Ostrich不应该从Bird类继承,这里应该从Bird中分离一个不会飞的类,Ostrich应该继承与它。

    Farhana:好,明白了。那么让我来试着指出为什么LSP这么重要:

    如果没有LSP,类继承就会混乱;如果子类作为一个参数传递给方法,将会出现未知行为;

    如果没有LSP,适用与基类的单元测试将不能成功用于测试子类;

    对吗?

    Shubho:非常正确。你能设计对象,使用LSP做为一个检查工作来测试继承是否正确。

     

    接口分离原则

    Shubho:今天我们学习"接口分离原则",这是海报:

    接口分离原则海报

    Farhana:这是什么意思?

    Shubho:它的意思是:"客户端不应该被迫依赖于它们不用的接口。"

    Farhana:请解释一下。

    Shubho:当然,这是解释:

    假设你想买个电视机,你有两个选择。一个有很多开关和按钮,它们看起来很混乱,且好像对你来说没必要。另一个只有几个开关和按钮,它们很友好,且适合你使用。假定两个电视机提供同样的功能,你会选哪一个?

    Farhana:当然是只有几个开关和按钮的第二个。

    Shubho:对,但为什么?

    Farhana:因为我不需要那些看起来混乱又对我没用的开关和按钮。

    Shubho:以便外部能够知道这些类有哪些可用的功能,客户端代码也能根据接口来设计.现在,如果接口太大,包含很多暴露的方法,在外界看来会很混乱.接口包含太多的方法也使其可用性降低,像这种包含了无用方法的"胖接口"会增加类之间的耦合.你通过接口暴露类 的功能,对.同样地,假设你有一些类,

    这也引起了其他问题.如果一个类想实现该接口,那么它需要实现所有的方法,尽管有些对它来说可能完全没用.所以说这么做会在系统中引入不必要的复杂度,降低可维护性或鲁棒性.

    接口隔离原则确保实现的接口有他们共同的职责,它们是明确的,易理解的,可复用的.

    Farhana:你的意思是接口应该仅包含必要的方法,而不该包含其它的.我明白了.

    Shubho:非常正确.一起看个例子.

    下面是违反接口隔离原则的一个胖接口

    注意到IBird接口包含很多鸟类的行为,包括Fly()行为.现在如果一个Bird类(如Ostrich)实现了这个接口,那么它需要实现不必要的Fly()行为(Ostrich不会飞).

    Farhana:确实如此。那么这个接口必须拆分了?

    Shubho:是的。这个"胖接口"应该拆分未两个不同的接口,IBird和IFlyingBird,IFlyingBird继承自IBird.

    这里如果一种鸟不会飞(如Ostrich),那它实现IBird接口。如果一种鸟会飞(如KingFisher),那么它实现IFlyingBird.

    Farhana:所以回头看包含了很多开关和按钮的电视机的例子,电视机制造商应该有一个电视机的图纸,开关和按钮都在这个方案里。不论任何时候, 当他们向制造一种新款电视机时,如果他们想复用这个图纸,他们将需要在这个方案里添加更多的开关和按钮。那么他们将没法复用这个方案,对吗?

    Shubho:对的。

    Farhana:如果他们确实需要复用方案,它们应当把电视机的图纸份为更小部分,以便在任何需要造新款电视机的时候复用这点小部分。

    Shubho:你理解了。

     

    依赖倒置原则

    Shubho:这是SOLID原则里最后一个原则。这是海报

    它的意思是:高层模块不应该依赖底层模块,两者都应该依赖其抽象

    Shubho:考虑一个现实中的例子。你的汽车是由很多如引擎,车轮,空调和其它等部件组成,对吗?

    Farhana:是的

    Shubho:好,它们没有一个是严格的构建在一个单一单元里;换句话说,它们都是可插拔的,因此当引擎或车轮出问题时,你可以修理它(而不需要修理其它部件),甚至可以换一个。

    在替换时,你仅需要确保引擎或车轮符合汽车的设计(如汽车能使用任何1500CC的引擎或任何18寸的车轮)。

    当然,汽车也可能允许你在1500CC引擎的地方安装一个2000CC的引擎,事实上对某些制造商(如丰田汽车)是一样的。

    现在,如果你的汽车的零部件不具备可插拔性会有什么不同?

    Farhana:那会很可怕!因为如果汽车的引擎出故障了,你可能修理整部车或者需要买一个新的。

    Shubho:是的,那么该如何做到"可插拔性"呢?

    Farhana:这里抽象是关键,对吗?

    Shubho:是的,在现实中,汽车是高级模块或实体,它依赖于低级模块或实体,如引擎或车轮。

    相比直接依赖于引擎或车轮,汽车应依赖于某些抽象的有规格的引擎或车轮,以便于如果任何引擎或车轮符合抽象,那么它们都能组合到汽车中,汽车也能跑动。

    一起看下面的类图

    Shubho:注意到上面Car类有两个属性,它们都是抽象类型(接口)。引擎和车轮是可插拔的,因为汽车能接受任何实现了声明接口的对象,并且Car类不需要做任何改动。

    Farhana:所以,如果代码中不用依赖倒置,我们将面临如下风险:

    使用低级类会破环高级代码;

    当低级类变化时需要很多时间和代价来修改高级代码;

    产生低复用的代码;

    Shubho:你完全掌握了,亲爱的!

    总结

    Shubho:除SOLID原则外还有很多其它的面向对象原则。如:

    "组合替代继承":这是说相对于继承,要更倾向于使用组合;

    "笛米特法则":这是说"你的类对其它类知道的越少越好";

    "共同封闭原则":这是说"相关类应该打包在一起";

    "稳定抽象原则":这是说"类越稳定,越应该由抽象类组成";

    Farhana:我应该学习那些原则吗?

    Shubho:当然可以。你可以从整个网上学习。仅仅需要Google一下那些原则,然后尝试理解它。当然如果有需要,尽管问我。

    Farhana:在那些设计原则之上我听说过很多设计模式。

    Shubho:对的。设计模式只是对一些经常出现的场景的一些通用设计建议。这些灵感主要来自于面向对象原则。你可以把设计模式看作"框架",把OOD原则看作"规范".

    Farhana:那么接下去我将学习设计模式吗?

    Shubho:是的,亲爱的。

    Farhana:那会很有意思,对吗?

    Shubho:是,那确实令人兴奋。

    简介
    http://www.cnblogs.com/niyw/archive/2011/05/30/2062071.html 

      自上篇翻译<如何向妻子解释OOD>后收到了很好的反应。故特继续翻译作者的<How I explained Design Patterns to my wife: Part 1>一文,以飨读者。在此文中,作者依旧通过与妻子浅显易懂的对话,向读者解释了什么是设计模式。

    设计模式是什么?

    Shubho:通过我们关于面向对象设计原则(OODP,即SOLID原则)的对话,我想你已经对面向对象设计原则(OODP)有了基本的认识。希望你不要介意我把对话分享到博客上。你可以在这找到它:<如何向妻子解释OOD>.

    设计模式是这些原则在某些特定公共场景下标准化的应用,接下来让我们通过一些例子学习什么是设计模式。

    Farhana: 当然,我喜欢例子。

    Shubho: 让我们以汽车为例讨论一下。汽车是一个很复杂的对象,由成千上万的其它对象组成,如发动机,车轮,方向盘,车座,车体等等其他不同的部分或部件。

    汽车部件

    当装配汽车时,制造商需要集中并装配这些更小的自成汽车子系统的不同部件。而这些不同的小部件同样也是复杂的对象,其它制造商同样要生产并组装它们。在生产汽车时,汽车公司并不会为怎么生产组装这些部件操心(前提是他们要确保这些对象/设备的质量)。当然,汽车制造商更加关心怎么装配这些不同部件以便能生产不同型号的汽车。

    通过遵循不同的设计,组装不同的部件,生产不同型号的汽车

    Farhana: 汽车制造公司必须有如何生产不同型号汽车的设计图或蓝图,对吗?

    Shubho: 当然,并且这些设计都是良好的,他们花费大量的时间和精力来做这些设计。一旦设计完成,生产汽车就仅仅是照葫芦画瓢了。

    Farhana: 嗯。如果事先有一些好的设计,就能在短时间内遵照这些设计生产不同产品,并且制造商在每次生产某一个型号产品时就不需要重新设计或重新发明车轮,他们只需要按照已有的设计办事就行了。

    生产不同型号产品(汽车)的不同设计图

    Shubho: 你抓到重点了。现在假设我们是软件生产商,我们使用基于需求而来的不同组件或功能构建各种不同的软件程序。当生产这些不同软件系统时,我们常常需要为一些不同软件系统中存在的相同情况开发代码,对吗?

    Farhana: 是的,在开发不同软件程序时经常遇到相同的设计问题。

    Shubho: 我们尝试使用面向对象的方式开发软件,并尝试应用OOPD来让代码能易于维护,可复用,可扩展。无论什么时候,当我们遇到这些设计问题时,如果我们有一组经过谨慎开发,良好测试的对象以供使用会不会更好呢?

    Farhana: 是的,这样能够节省时间,生产出更好的软件,且利于以后维护。

    Shubho: 很好!从设计上来说,它的好处是你不需要开发那些对象。经过多年发展,人们已经遇到过一些类似的设计问题,并已经形成有一些公认的,良好的已标准化的设计方案。我们称之为设计模式。

    我们一定好感谢四人组,他们在《设计模式:可复用面向对象软件设计》中总结出了23种基本的设计模式。四人组由Erich Gamma, Richard Helm, Ralph Johnson, 和John Vlissides组成。实际中有很多面向对象设计模式,但这23种模式被公认为是所有其他设计模式的基础。

    Farhana: 我能发明一个新的模式吗?这可能吗?

    Shubho: 当然,亲爱的,为什么不能呢?!设计模式不是由科学家发明创造的。它们是被发现找到的。这意味着任何通用问题场景中都有一些好的设计方案在那。如果我们能够指出一个能够解决一个新的设计相关问题的面向对象设计,那么这将会是一个由我们定义的新的设计模式。谁知道呢?!如果我们发现找到一些设计模式,或许将来有一天人们会称我们为二人组,哈哈。

    Fahana: :)

    我们将如何学习设计模式?

    Shubho: 我一直认为例子是学习的最好途径。在我们的学习方法中,我们不会先讨论理论后讨论实现。我认为这是很糟糕的方式。设计模式不是基于理论的发明。事实上,问题场景首先出现,其次是基于这些问题的来龙去脉和需求,然后是一些设计方案的演化,最后其中的一些被标准化为模式。所以对每一个我们讨论的设计模式,我们将尝试理解并分析一些现实生活中的例子,然后一步步尝试归纳一个设计,并最后总结一些与某些模式匹配设计。设计模式就是在这些相似过程中发现的。你认为呢?

    Farhana:我想这种方式对我更有用。如果我能通过分析问题和归纳方案得出设计模式,我就不用死记那些设计模式和定义了。请按照你的方式继续。

    一个常见的设计问题和它的解决方案

    Shubho: 让我们考虑下面的场景:

    我们房间里有些电器(电灯,风扇等)。这些设备按照某些方式布局,并由开关控制。任何时候你都能替换或排查一个电器而不用碰到其他东西。例如,你可以换一个电灯而不需要换开关。同样,你可以换一个开关或排查它而不需要碰到或替换相应的电灯或风扇;甚至你可以用把电灯连接到风扇的开关上,把风扇连到电灯的开关上,而不需要碰到开关。

    电器:风扇和电灯

    风扇和电灯的两种不同开关,一个普通点,另一个别致点

    Farhana: 是的,但就是这样子,对吗?

    Shubho: 是的,确实如此,就该如此布局。当不同东西联系在一起时,它们应该按照一定方式联系:修改或替换一个系统时不会影响到另一个,或者说即便有,也应该最小化。这能够让你的系统易于管理,且成本低。想想一下,如果改一下房间里的灯同时需要改开关,你会乐意在你房子上花钱并安装这个系统吗?

    Farhana: 当然不会。

    Shubho: 现在,让我们思考一下电灯或风扇如何连接到开关上才能达到改变一个不会影响到另一个。你认为该如何?

    Farhana: 用电线!

    Shubho: 很好。把电灯/风扇和开关联系到一起的是电线和电器布局。我们可以它们看做不同系统间相互联系的桥梁。其基本的思想是,一个事物不能和另一外一个事物直接联系。当然啦,它们应当通过某些桥梁或接口联系在一起。用软件术语来说,这叫“松耦合”。

    Farhana: 我知道了。

    Shubho: 现在,让我们尝试推断在电灯/风扇和开关例子中的几个关键问题,并尝试推断它们是如何设计并联系起来的。

    Farhana: 好,我们试一下。

    例子中我们有开关,可能有几种开关,如普通的开关,漂亮的开关,但通常来说它们还是开关,并且每种开关都能够打开和关闭。

    所以下面我们会有一个开关基类Switch

    复制代码
    public class Switch {
    public void On() {
    //打开开关
    }
    public void Off() {
    //关闭开关
    }
    }
    复制代码

    接下来我们可以有一些具体的开关,例如一个漂亮开关,一个普通开关等等,当然,我们会让类FancySwitchNormalSwitchnd继承类Switch

    public class NormalSwitch : Switch {
    }
    public class FancySwitch : Switch {
    }
    这里的两个具体类有自己的特征和行为,只是此时此刻,我们简单化以下。

    Shubho: 非常棒,接下来电灯和风扇怎么办?

    Farhana: 我试试. 根据OODP的开放闭合原则,我们知道只要可能,就应该尝试抽象,对吗?

    Shubho: 对

    Farhana: 跟开关不一样,风扇和电灯等是两种不同的事物。对于开关,我们能够使用一个开关基类Switch,但风扇和电灯是两个不同的事物,相比定义一个基类,接口可能更合适。一般来说,他们都是电器。所以我们可以定义一个接口,如IElectricalEquipment,作为对电灯和风扇的抽象,可以吗?

    Shubho: 可以

    Farhana: 好,每种电器都有些相同的功能。他们能够打开和关闭。所以接口可能如下:

    public interface IElectricalEquipment {
    void PowerOn(); //每种电器都能打开
    void PowerOff(); //每种电器都能关闭
    }
    Shubho: 太好了,你很善于抽象东西。现在我们需要一座桥梁。在现实中,电线是桥梁。在我们对象设计中,开关知道如何打开和关闭电器,电器以某种方式联系到开关。这里我们没有电线,让电器连接到开关的唯一方式是封装。

    Farhana: 是的,但开关不能直接知道风扇或电灯。开关应当知道一个电器IElectricalEquipment能够打开或关闭。这意味着,ISwitch应该有一个IElectricalEquipment实例,对吗?

    Shubho: 对,对风扇或电灯的封装的实例是一个桥梁。所以让我们修改Switch类以便封装一个电器:

    复制代码
    public class Switch {
    public IElectricalEquipment equipment {
    get;
    set;
    }
    public void On() {
    //开关打开
    }
    public void Off() {
    //开关关闭
    }
    }
    复制代码
    Farhana: 明白。让我们定义真实的电器:风扇和电灯。如我所见,一般来说它们都是电器,所以它们都简单实现了IElectricalEquipment接口。

    下面是风扇类:

    复制代码
    public class Fan : IElectricalEquipment {
    public void PowerOn() {
    Console.WriteLine(
    "风扇打开");
    }
    public void PowerOff() {
    Console.WriteLine(
    "风扇关闭");
    }
    }
    复制代码

    下面是电灯类:

    复制代码
    public class Light : IElectricalEquipment {
    public void PowerOn() {
    Console.WriteLine(
    "电灯打开");
    }
    public void PowerOff() {
    Console.WriteLine(
    "电灯关闭");
    }
    }
    复制代码

    Shubho:太好了。现在让开关工作。当开关打开关闭的时候它应当能够打开关闭电器(它连接到的) 。

    这里的关键点是:

    • 当开关按下开时,连接的电器也应该打开。
    • 当开关按下关时,连接的电器也应该关闭。

    大致的代码如下:

    复制代码
    static void Main(string[] args) {
    //构造电器设备:风扇,开关
    IElectricalEquipment fan = new Fan();
    IElectricalEquipment light 
    = new Light();

    //构造开关
    Switch fancySwitch = new FancySwitch();
    Switch normalSwitch 
    = new NormalSwitch();

    //把风扇连接到开关
    fancySwitch.equipment = fan;

    //开关连接到电器,那么当开关打开或关闭时电器应该打开/关闭
    fancySwitch.On();
    fancySwitch.Off();

    //把电灯连接到开关
    fancySwitch.equipment = light;
    fancySwitch.On(); 
    //打开电灯
    fancySwitch.Off(); //关闭电灯
    }
    复制代码

    Farhana: 明白。开关的On()方法应当内部调用电器的TurnOn()方法,Off()方法应当内部调用TurnOff()方法,所以开关类Switch应如下:

    复制代码
    public class Switch {
    public IElectricalEquipment equipment {
    get;
    set;
    }
    public void On() {
    Console.WriteLine(
    "开关打开");
    equipment.PowerOn();
    }
    public void Off() {
    Console.WriteLine(
    "开关关闭");
    equipment.PowerOff();
    }
    }
    复制代码

    Shubho: 很好。这自然允许你把风扇从一个开关接到另一个上。不过你看,反过来也可以。这意味着你可以改变风扇或电灯的开关而不需要碰到风扇或电灯。例如,你可以很轻松的把点灯的开关从FancySwitch换到NormalSwitch上,如下:

    normalSwitch.equipment = light;
    normalSwitch.On(); 
    //打开电灯
    normalSwitch.Off(); //关闭电灯
    你看,连接一个抽象电器到一个开关(通过封装)能够让你改变开关和电器而不会对对方产生影响。这个设计是优雅的,良好的。四人组为该模式取名为:桥接模式。

    Farhana: 太棒了。我想我明白这个了。从根本上说,两个系统不应当直接联系或依赖与对方。 当然,他们应该联系或依赖于抽象(如依赖倒置原则和开放闭合原则所讲),所以他们是松耦合的,因此我们可以在需要时改变我们的实现而不会对系统其他部分产生过多影响。

    Shubho: 你理解了,亲爱的.我们看下桥接模式的定义:

    "将抽象部分与实现部分分离,使它们都可以独立的变化"

    你看我们的实现完美遵循该定义。如果你有一个类设计器(如Visual Studio或其他支持该功能的IDE环境),你会看到类似的如下类图:

    桥接模式类图

    在这里, Abstraction 是开关基类Switch RefinedAbstraction 是具体开关类 (FancySwitchNormalSwitch 等等。)。 Implementor是电器接口IElectricalEquipmentConcreteImplementorA 和ConcreteImplementorB 是电灯类Light和风扇类Fan

    Farhana: 问你个问题,只是好奇啊。如你所说有很多其他的设计模式,为什么你以桥接模式开始呢?有重要原因吗?

    Shubho: 这个问题很好。是的,我以桥接模式而不以其他开始是因为一个理由。我认为桥接模式是所有面向对象模式的基础。理由如下:

    • 它教导如何思考抽象,这是面向对象设计模式的关键概念。
    • 它实现了基本的OOD原则。
    • 它容易理解。
    • 如果正确理解该模式,学习其他模式会很容易。

    Farhana: 你认为我理解的对吗?

    Shubho: 我认为你理解的非常正确。

    Farhana: 那么接下来是什么?

    Shubho: 通过理解桥接模式,我们仅仅是开始理解设计模式的思想。在我们接下的对话中,我们将会学习其他的设计模式,我希望你不会觉得它们无聊。

    Farhana:不会的,相信我。

     

    展开全文
  • 公共技术点之面向对象六大原则

    千次阅读 多人点赞 2015-02-24 23:14:21
    再看那些知名的开源,它们大多有着整洁的代码、清晰简单的接口、职责单一的类,这个时候我们通常会捶胸顿足而感叹:什么时候老夫才能写出这样的代码! 在做开发的这些年中,我渐渐的感觉到,其实国内的一些初、...

    概述

    在工作初期,我们可能会经常会有这样的感觉,自己的代码接口设计混乱、代码耦合较为严重、一个类的代码过多等等,自己回头看的时候都觉得汗颜。再看那些知名的开源库,它们大多有着整洁的代码、清晰简单的接口、职责单一的类,这个时候我们通常会捶胸顿足而感叹:什么时候老夫才能写出这样的代码!

    在做开发的这些年中,我渐渐的感觉到,其实国内的一些初、中级工程师写的东西不规范或者说不够清晰的原因是缺乏一些指导原则。他们手中挥舞着面向对象的大旗,写出来的东西却充斥着面向过程的气味。也许是他们不知道有这些原则,也许是他们知道但是不能很好运用到实际代码中,亦或是他们没有在实战项目中体会到这些原则能够带来的优点,以至于他们对这些原则并没有足够的重视。

    今天,我们就是以剖析优秀的Android网络框架Volley为例来学习这六大面向对象的基本原则,体会它们带来的强大能量。

    在此之前,有一点需要大家知道,熟悉这些原则不会让你写出优秀的代码,只是为你的优秀代码之路铺上了一层栅栏,在这些原则的指导下你才能避免陷入一些常见的代码泥沼,从而让你专心写出优秀的东西。另外,我是个新人,以下只是我个人的观点。如果你觉得还行,可以顶个帖支持一下;如果你觉得它不行,还请分享你的经验。

    Volley相关资料

    单一职责原则 ( Single Responsibility Principle )

    简述

    单一职责原则的英文名称是Single Responsibility Principle,简称是SRP,简单来说一个类只做一件事。这个设计原则备受争议却又及其重要的原则。只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的。因为单一职责的划分界限并不是如马路上的行车道那么清晰,很多时候都是需要靠个人经验来界定。当然最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。

    试想一下,如果你遵守了这个原则,那么你的类就会划分得很细,每个类都有自己的职责。恩,这不就是高内聚、低耦合么! 当然,如何界定类的职责这需要你的个人经验了。

    示例

    在Volley中,我觉得很能够体现SRP原则的就是HttpStack这个类族了。HttpStack定义了一个执行网络请求的接口,代码如下 :

    /**
     * An HTTP stack abstraction.
     */
    public interface HttpStack {
        /**
         * 执行Http请求,并且返回一个HttpResponse
         */ 
        public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError;
    
    }

    可以看到,HttpStack只有一个函数,清晰明了,它的职责就是执行网络请求并且返回一个Response。它的职责很单一,这样在需要修改执行网络请求的相关代码时我们只需要修改实现HttpStack接口的类,而不会影响其它的类的代码。如果某个类的职责包含有执行网络请求、解析网络请求、进行gzip压缩、封装请求参数等等,那么在你修改某处代码时你就必须谨慎,以免修改的代码影响了其它的功能。但是当职责单一的时候,你修改的代码能够基本上不影响其它的功能。这就在一定程度上保证了代码的可维护性。注意,单一职责原则并不是说一个类只有一个函数,而是说这个类中的函数所做的工作必须要是高度相关的,也就是高内聚。HttpStack抽象了执行网络请求的具体过程,接口简单清晰,也便于扩展。

    我们知道,Api 9以下使用HttpClient执行网络请求会好一些,api 9及其以上则建议使用HttpURLConnection。这就需要执行网络请求的具体实现能够可扩展、可替换,因此我们对于执行网络请求这个功能必须要抽象出来,HttpStack就是这个职责的抽象。

    优点

    • 类的复杂性降低,实现什么职责都有清晰明确的定义;
    • 可读性提高,复杂性降低,那当然可读性提高了;
    • 可维护性提高,可读性提高,那当然更容易维护了;
    • 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

    里氏替换原则 ( Liskov Substitution Principle)

    简述

    面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

    示例

    还是以HttpStack为例,Volley定义了HttpStack来表示执行网络请求这个抽象概念。在执行网络请求时,我们只需要定义一个HttpStack对象,然后调用performRequest即可。至于HttpStack的具体实现由更高层的调用者给出。示例如下 :

    
        public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
            File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
    
            String userAgent = "volley/0";
            // 代码省略
            // 1、构造HttpStack对象
            if (stack == null) {
                if (Build.VERSION.SDK_INT >= 9) {
                    stack = new HurlStack();
                } else {
                    // Prior to Gingerbread, HttpUrlConnection was unreliable.
                    // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
                    stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
                }
            }
            // 2、将HttpStack对象传递给Network对象
            Network network = new BasicNetwork(stack);
            // 3、将network对象传递给网络请求队列
            RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
            queue.start();
    
            return queue;
        }

    BasicNetwork的代码如下:

    /**
     * A network performing Volley requests over an {@link HttpStack}.
     */
    public class BasicNetwork implements Network {
        // HttpStack抽象对象
        protected final HttpStack mHttpStack;
    
        protected final ByteArrayPool mPool;
    
        public BasicNetwork(HttpStack httpStack) {
    
            this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
        }
    
    
        public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
            mHttpStack = httpStack;
            mPool = pool;
        }
    }

    上述代码中,BasicNetwork构造函数依赖的是HttpStack抽象接口,任何实现了HttpStack接口的类型都可以作为参数传递给BasicNetwork用以执行网络请求。这就是所谓的里氏替换原则,任何父类出现的地方子类都可以出现,这不就保证了可扩展性吗?! 此时,用手撑着你的小脑门作思考状… 任何实现HttpStack接口的类的对象都可以传递给BasicNetwork实现网络请求的功能,这样Volley执行网络请求的方法就有很多种可能性,而不是只有HttpClient和HttpURLConnection。

    喔,原来是这样!此时我们放下装逼的模样,呈现一副若有所得的样子。细想一下,框架可不就是这样吗? 框架不就是定义一系列相关的逻辑骨架与抽象,使得用户可以将自己的实现传递进给框架,从而实现变化万千的功能。

    优点

    • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
    • 提高代码的重用性;
    • 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,很多开源框架的扩展接口都是通过继承父类来完成的;
    • 提高产品或项目的开放性。

    缺点

    • 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
    • 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
    • 增强了耦合性。当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构。

    依赖倒置原则 (Dependence Inversion Principle)

    简述

    依赖倒置原则这个名字看着有点不好理解,“依赖”还要“倒置”,这到底是什么意思?
    依赖倒置原则的几个关键点如下:

    • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
    • 抽象不应该依赖细节;
    • 细节应该依赖抽象。

    在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。软件先驱们总是喜欢将一些理论定义得很抽象,弄得神不楞登的,其实就是一句话:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。面向接口编程是面向对象精髓之一。

    示例

    采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
    第二章节中的BasicNetwork实现类依赖于HttpStack接口( 抽象 ),而不依赖于HttpClientStack与HurlStack实现类 ( 细节 ),这就是典型的依赖倒置原则的体现。加入BasicNetwork直接依赖了HttpClientStack,那么HurlStack就不能传递给了,除非HurlStack继承自HttpClientStack。但这么设计明显不符合逻辑,HurlStack与HttpClientStack并没有任何的is-a的关系,而且即使有也不能这么设计,因为HttpClientStack是一个具体类而不是抽象,如果HttpClientStack作为BasicNetwork构造函数的参数,那么以为这后续的扩展都需要继承自HttpClientStack。这简直是一件不可忍受的事了!

    优点

    • 可扩展性好;
    • 耦合度低;

    开闭原则 ( Open-Close Principle )

    简述

    开闭原则是 Java 世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是 : 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误。因此当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。

    示例

    在软件开发过程中,永远不变的就是变化。开闭原则是使我们的软件系统拥抱变化的核心原则之一。对扩展可放,对修改关闭给出了高层次的概括,即在需要对软件进行升级、变化时应该通过扩展的形式来实现,而非修改原有代码。当然这只是一种比较理想的状态,是通过扩展还是通过修改旧代码需要根据代码自身来定。

    在Volley中,开闭原则体现得比较好的是Request类族的设计。我们知道,在开发C/S应用时,服务器返回的数据格式多种多样,有字符串类型、xml、json等。而解析服务器返回的Response的原始数据类型则是通过Request类来实现的,这样就使得Request类对于服务器返回的数据格式有良好的扩展性,即Request的可变性太大。

    例如我们返回的数据格式是Json,那么我们使用JsonObjectRequest请求来获取数据,它会将结果转成JsonObject对象,我们看看JsonObjectRequest的核心实现。

    /**
     * A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an
     * optional {@link JSONObject} to be passed in as part of the request body.
     */
    public class JsonObjectRequest extends JsonRequest<JSONObject> {
    
       // 代码省略
        @Override
        protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
            try {
                String jsonString =
                    new String(response.data, HttpHeaderParser.parseCharset(response.headers));
                return Response.success(new JSONObject(jsonString),
                        HttpHeaderParser.parseCacheHeaders(response));
            } catch (UnsupportedEncodingException e) {
                return Response.error(new ParseError(e));
            } catch (JSONException je) {
                return Response.error(new ParseError(je));
            }
        }
    }

    JsonObjectRequest通过实现Request抽象类的parseNetworkResponse解析服务器返回的结果,这里将结果转换为JSONObject,并且封装到Response类中。

    例如Volley添加对图片请求的支持,即ImageLoader( 已内置 )。这个时候我的请求返回的数据是Bitmap图片。因此我需要在该类型的Request得到的结果是Request,但支持一种数据格式不能通过修改源码的形式,这样可能会为旧代码引入错误。但是你又需要支持新的数据格式,此时我们的开闭原则就很重要了,对扩展开放,对修改关闭。我们看看Volley是如何做的。

    
    /**
     * A canned request for getting an image at a given URL and calling
     * back with a decoded Bitmap.
     */
    public class ImageRequest extends Request<Bitmap> {
        // 代码省略
    
        // 将结果解析成Bitmap,并且封装套Response对象中
        @Override
        protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
            // Serialize all decode on a global lock to reduce concurrent heap usage.
            synchronized (sDecodeLock) {
                try {
                    return doParse(response);
                } catch (OutOfMemoryError e) {
                    VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
                    return Response.error(new ParseError(e));
                }
            }
        }
    
        /**
         * The real guts of parseNetworkResponse. Broken out for readability.
         */
        private Response<Bitmap> doParse(NetworkResponse response) {
            byte[] data = response.data;
            BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
            Bitmap bitmap = null;
            // 解析Bitmap的相关代码,在此省略
    
            if (bitmap == null) {
                return Response.error(new ParseError(response));
            } else {
                return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
            }
        }
    }

    需要添加某种数据格式的Request时,只需要继承自Request类,并且实现相应的方法即可。这样通过扩展的形式来应对软件的变化或者说用户需求的多样性,即避免了破坏原有系统,又保证了软件系统的可扩展性。

    优点

    • 增加稳定性;
    • 可扩展性高。

    接口隔离原则 (Interface Segregation Principle)

    简述

    客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。

    可能描述起来不是很好理解,我们还是以示例来加强理解吧。

    示例

    我们知道,在Volley的网络队列中是会对请求进行排序的。Volley内部使用PriorityBlockingQueue来维护网络请求队列,PriorityBlockingQueue需要调用Request类的compareTo函数来进行排序。试想一下,PriorityBlockingQueue其实只需要调用Request类的排序方法就可以了,其他的接口它根本不需要,即PriorityBlockingQueue只需要compareTo这个接口,而这个compareTo方法就是我们上述所说的最小接口。当然compareTo这个方法并不是Volley本身定义的接口方法,而是Java中的Comparable接口,但我们这里只是为了学习本身,至于哪里定义的无关紧要。

    public abstract class Request<T> implements Comparable<Request<T>> {
    
        /**
         * 排序方法,PriorityBlockingQueue只需要调用元素的compareTo即可进行排序
         */
        @Override
        public int compareTo(Request<T> other) {
            Priority left = this.getPriority();
            Priority right = other.getPriority();
    
            return left == right ?
                    this.mSequence - other.mSequence :
                    right.ordinal() - left.ordinal();
        }
    }

    PriorityBlockingQueue类相关代码 :

    
    public class PriorityBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    
        // 代码省略
    
            // 添加元素的时候进行排序
            public boolean offer(E e) {
            if (e == null)
                throw new NullPointerException();
            final ReentrantLock lock = this.lock;
            lock.lock();
            int n, cap;
            Object[] array;
            while ((n = size) >= (cap = (array = queue).length))
                tryGrow(array, cap);
            try {
                Comparator<? super E> cmp = comparator;
                // 没有设置Comparator则使用元素本身的compareTo方法进行排序
                if (cmp == null)
                    siftUpComparable(n, e, array);
                else
                    siftUpUsingComparator(n, e, array, cmp);
                size = n + 1;
                notEmpty.signal();
            } finally {
                lock.unlock();
            }
            return true;
        }
    
        private static <T> void siftUpComparable(int k, T x, Object[] array) {
            Comparable<? super T> key = (Comparable<? super T>) x;
            while (k > 0) {
                int parent = (k - 1) >>> 1;
                Object e = array[parent];
                // 调用元素的compareTo方法进行排序
                if (key.compareTo((T) e) >= 0)
                    break;
                array[k] = e;
                k = parent;
            }
            array[k] = key;
        }
     }

    从PriorityBlockingQueue的代码可知,在元素排序时,PriorityBlockingQueue只需要知道元素是个Comparable对象即可,不需要知道这个对象是不是Request类以及这个类的其他接口。它只需要排序,因此我只要知道它是实现了Comparable接口的对象即可,Comparable就是它的最小接口,也是通过Comparable隔离了PriorityBlockingQueue类对Request类的其他方法的可见性。妙哉,妙哉!

    优点

    • 降低耦合性;
    • 提升代码的可读性;
    • 隐藏实现细节。

    迪米特原则 ( Law of Demeter )

    简述

    迪米特法则也称为最少知识原则(Least Knowledge Principle),虽然名字不同,但描述的是同一个原则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,这有点类似接口隔离原则中的最小接口的概念。类的内部如何实现、如何复杂都与调用者或者依赖者没关系,调用者或者依赖者只需要知道他需要的方法即可,其他的我一概不关心。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

    迪米特法则还有一个英文解释是: Only talk to your immedate friends( 只与直接的朋友通信。)什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。

    示例

    例如,Volley中的Response缓存接口的设计。

    /**
     * An interface for a cache keyed by a String with a byte array as data.
     */
    public interface Cache {
        /**
         * 获取缓存
         */
        public Entry get(String key);
    
        /**
         * 添加一个缓存元素
         */
        public void put(String key, Entry entry);
    
        /**
         * 初始化缓存
         */
        public void initialize();
    
        /**
         * 标识某个缓存过期
         */
        public void invalidate(String key, boolean fullExpire);
    
        /**
         * 移除缓存
         */
        public void remove(String key);
    
        /**
         * 清空缓存
         */
        public void clear();
    
    }

    Cache接口定义了缓存类需要实现的最小接口,依赖缓存类的对象只需要知道这些接口即可。例如缓存的具体实现类DiskBasedCache,该缓存类将Response序列化到本地,这就需要操作File以及相关的类。代码如下 :

    
    public class DiskBasedCache implements Cache {
    
        /** Map of the Key, CacheHeader pairs */
        private final Map<String, CacheHeader> mEntries =
                new LinkedHashMap<String, CacheHeader>(16, .75f, true);
    
        /** The root directory to use for the cache. */
        private final File mRootDirectory;
    
        public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
            mRootDirectory = rootDirectory;
            mMaxCacheSizeInBytes = maxCacheSizeInBytes;
        }
    
        public DiskBasedCache(File rootDirectory) {
            this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
        }
    
        // 代码省略
    }

    在这里,Volley的直接朋友就是DiskBasedCache,间接朋友就是mRootDirectory、mEntries等。Volley只需要直接和Cache类交互即可,并不需要知道File、mEntries等对象的存在。这就是迪米特原则,尽量少的知道对象的信息,只与直接的朋友交互。

    优点

    • 降低复杂度;
    • 降低耦合度;
    • 增加稳定性。

    杂谈

    面向对象六大原则在开发过程中极为重要,上述的例子中可能有的不是很合适的示例,我们这里只是抛砖引玉,更深刻的理解需要大家自行学习。如果能够很好地将这些原则运用到项目中,再在一些合适的场景运用一些前人验证过的模式,那么开发出来的软件在一定程度上能够得到质量保证。其实稍微一想,这几大原则最终就化为这么几个关键词: 抽象、单一职责、最小化。那么在实际开发过程中如何权衡、实践这些原则,笔者也在不断地学习、摸索。我想学习任何的事物莫过于实践、经验与领悟,在这个过程中希望能够与大家分享知识、共同进步。

    展开全文
  • 成熟的框架有很多,这里选用Asio网络库原因有:简单方便、作者写的代码比较规范、可能进入下一代的C++标准库中。 可移植性好: 在windows下可能使用iocp,在linux下可能使用epoll,在bsd下可能使用kqueue。 可扩展...

    84、01

    • 服务器编程一般的思路: 1、选择一个平台、熟悉API、构建框架;2、找一个成熟的框架,学习它。通过学习它掌握服务器编程方面的知识。
    • 成熟的框架有很多,这里选用Asio网络库原因有:简单方便;作者写的代码比较规范;可能进入下一代的C++标准库中。
    • 主要作用: 主要用在服务端编程中处理服务器和客户端的socket。不限于此,类似于文件、时间都可以处理。
    • 可移植性好: 在windows下可能使用iocp,在linux下可能使用epoll,在bsd下可能使用kqueue。
    • 可扩展性高: 可以自己进行扩展。
    • 效率高: 有的库虽然可移植,但是效率可能发生变化。
    • 接口相近: API与原生的SocketsAPI基本一致。易于理解和使用。
    • 易于使用: 接口明确清晰(不会有二义性)。但是过于相近。
    • 基于更多的抽象: 不管是TCP还是UDP,提供的接口是一致的。不是基于网络,基于文件(流)的也可以套用Asio。
    • 两种方式: 1、同步方式(最简单);2、异步方式(最常用)。

    85、编程模型

    • 基于事件驱动的编程模型: Reactor模型(Lighttpd、libevent、libev、poco),Proactor模型(Asio、Iocp)。

    89、例子详解01

    • boost::asio::io_service io;用于调用系统函数,这种调用操作分为阻塞和非阻塞。
    • timer t(io, 5s); 添加监听事件;
    • t.wait();
    • t.async_wait(); 绑定回调函数。
    • io.run(): 相当于一个while循环,返回退出条件有两种:1、调用io本身的stop函数;2、消息池里没有注册函数了(所有的事件全部处理完毕)。

    问题1: 实现非阻塞中的阻塞。改动例子中的代码,要求先执行2秒倒计时,2秒倒计时结束后再执行5秒倒计时。
    问题2: boost库中的timer只能执行一次,自己实现一个timer类:1、可以执行多次或者无限次;2、可以取消掉;3、可以中途延时(暂停)。

    90、例子详解02,捎带C++黑暗面

    • lambda取代bind: io.async_wait传入一个函数(可调用的模板参数),需要带参数怎么办?以前C++03的做法是传入bind,实际就是std::function也是来自boost::function。现在可以用lambda代替,看起来简单便于理解,并且可以是inline(编译时展开的)。
    • auto作为lambda的形参,要在C++14版本才支持。修改方式:1、不用auto,写上正确类型;2、修改编译文件makefile,指定C++14。
    • expires_at(): 不传参返回timer结束时间,传参则设置timer结束时间。
    • 生命周期注意事项:io.run()中绑定的参数,要确保生命周期比run要长,否则会出问题。
    • 奇怪了: 想要说明C++的黑暗面,即指针变成野指针了,但是数据结构还是没有变,行为看起来是正确的。于是老师最后5分钟快速举例子,想要弄一个出错的,但是好像没有成功,因为打印出来的012341234234344我觉得是正确的。老师却说0000011111222223333344444才对。

    91、例子详解03 - lambda的本质

    • 语法糖: 没有什么新东西,只是编译器完成了部分工作。
    • 本质: 在外部定义了一个类,重载了()操作符,如果有参就加参,有外部引用就加成员变量。
    • 解决: 上节课遗留的问题是,打印出来的地址都是一样的。(老师纠正后的数字打印,验证了我的想法是没有错的)。通过传入智能指针的方式来延长指针的生命周期,修正了错误。
    • 新问题: 万一就是不想让智能指针的生命周期那么长呢?二一个就是将智能指针作为接口函数,可能会强迫调用者也要使用智能指针。(这里asio编程没有问题,大多用的智能指针,但别的地方可能有问题,老师个人观点。)

    92、06

    • 如何用bind绑定成员函数: 官方文档介绍的是用bind方法。
    • 奇怪: 绑定成员函数的时候,貌似不用传入boost::system::error_code的引用参数。第一个参数为成员函数地址&printer::print,第二个参数为调用者this。用lambda的时候还是需要传入那个参数。
    • 解答: boost库bind函数在使用时,实际有很多默认的参数,所以可以忽略掉。
    • 反复强调: 绑定函数中使用到的参数(指针),生命周期一定要大于整个事件。服务器编程回调的Bug,很多都是参数失效造成的。
    • 疑问: timer析构的时候,本身会调用cancel,这个可以理解。但是为什么会在printer的构造函数中就会发出error为boost::asio::error::operation_aborted呢?

    93、07 io_service在多线程下如何使用

    • 通常设计: 主线程跑逻辑、额外线程跑io_service(收发玩家消息或者监听timer)。
    int main(int argc, char** argv) {
    	boost::asio::io_service io;
    	printer p(io);
    	std::thread t([&io] {io.run(); });
    	t.join();
    	system("pause");
    	return EXIT_SUCCESS;
    }
    
    • -lpthread: 多线程编程时,在编译时加-lpthread,来连接这个库。linux和bsd操作系统下标准的实现。
    • g++编译优化: 可以加参数O1O2O3,数字越大优化程度越高。推荐使用O2O3反而会劣化)。
    • strand_wrap(): 用来处理多线程下同时触发事件,使用同一变量的情况。使用方法:timer.async_wait中的回调函数用它包裹起来。
    printer(boost::asio::io_service &io)
          : strand_(io), timer1_(io, boost::posix_time::seconds(1)),
            timer2_(io, boost::posix_time::seconds(1)), count_(0) {
        timer1_.async_wait(strand_.wrap([this](const auto&){this->print1(); }));
        timer2_.async_wait(strand_.wrap([this](const auto&){this->print2(); }));
      }
    
    • 如果只是在一个线程中,默认有一个global_thread概念,回调函数不会错乱,可以不用strand_wrap()
    • io.run()结束的时候会将状态设置为stop,即使再次注册事件,调用io.run(),也没有用,会立即返回。
    // 重新开启线程
    if (io.stopped())
    	io.reset();
    

    94、08 同步阻塞的客户端

    • 域名解析会先查看hosts文件,如果没有才通过DNS解析,域名翻墙就是在hosts文件中自己写入对于IP地址。
    • 端口号和服务名对应关系,在/etc/services中配置,小端口号通常是系统端口号,用户使用1024以下的需要开启权限。
    • std::cout.write(): 直接按照定好的类型输出,而std::cout <<则会进行一定的类型转换。
    • 测试程序: 连接本地localhost或者127.0.0.1,可能显示失败。连接time.nist.gov会成功显示时间。
    #include <iostream>
    #include <boost\array.hpp>
    #include <boost\asio.hpp>
    
    using boost::asio::ip::tcp;
    
    int main(int argc, char** argv) {
    	try
    	{
    		if (argc != 2) {
    			std::cerr << "Usage: client <host>" << std::endl;
    			return 1;
    		}
    
    		boost::asio::io_service io_service;
    
    		tcp::resolver resolver(io_service); // 解析器
    		tcp::resolver::query query(argv[1], "daytime"); // 绑定IP和端口号
    		tcp::resolver::iterator endpoint_iterator = resolver.resolve(query); // 执行解析
    
    		tcp::socket socket(io_service);
    		boost::asio::connect(socket, endpoint_iterator);
    
    		for (;;) {
    			boost::array<char, 128> buf;
    			boost::system::error_code error;
    
    			size_t len = socket.read_some(boost::asio::buffer(buf), error);
    
    			if (error == boost::asio::error::eof)
    				break;
    			else if (error)
    				throw boost::system::system_error(error);
    
    			std::cout.write(buf.data(), len);
    		}
    	}
    	catch (const std::exception& e)
    	{
    		std::cerr << e.what() << std::endl;
    	}
    	return EXIT_SUCCESS;
    }
    

    95、09 时间服务器

    • 头文件的包含风格: 1、包含顺序:C、C++、第三方库、自己,中间按字母顺序排列(vim下有自动排序命令sort)。2、正好是反过来的顺序(因为自己的库有可能包含前面的一些头文件,对编译速度有一点点提升)。但是自己养成了习惯就按统一的来。
    • 头文件发展历史: <time.h>是C的头文件,<ctime>是C++头文件(通常去除后面的.h,如果是C的就在前面加c)。前者没有包含在标准命名空间std内。
    • 培养潜意识: 很多东西在多线程下编程时不安全的,多看一些官方文档(C/C++标准库的函数其实很少的),脑中要有这样的印象。
    • boost::asio::write(): 如果最后不传入error_code参数,当出错的时候就会抛出异常。一个客户端的出错就让整个服务端停止循环是错误的做法。
    • 有些服务就采用同步阻塞的方式就够了,而且效率也不差,不用考虑“异步”、“多线程”之类的。
    #include <ctime>
    
    #include <iostream>
    #include <string>
    
    #include <boost/asio.hpp>
    
    using boost::asio::ip::tcp;
    
    std::string make_daytime_string() {
    	using namespace std;
    	auto now = time(nullptr);
    	char buf[128];
    	ctime_s(buf, sizeof(buf), &now); // ctime()被修改成ctime_s()
    	return std::string(buf);
    }
    
    int main() {
    	try {
    		//1、创建io
    		boost::asio::io_service io_service;
    
    		//2、创建acceptor
    		tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 13));
    		std::cout << "start service\n";
    
    		for (;;) {
    
    			//3、创建socket
    			tcp::socket socket(io_service);
    
    			//4、用acceptor监听socket
    			acceptor.accept(socket);
    
    			std::string message = make_daytime_string();
    
    			boost::system::error_code ignored_error;
    
    			//5、发送数据
    			boost::asio::write(socket, boost::asio::buffer(message), ignored_error);
    		}
    		std::cout << "bye\n";
    	}
    	catch (std::exception &e) {
    		std::cerr << e.what() << std::endl;
    	}
    
    	return 0;
    }
    

    96、10 异步事件服务器

    • 工厂类:
    • make_shared<> 比原生指针好,但也有一定局限性(当构造函数为private)。

    97、Echo服务器

    • 复习: 1、enable_from_this;2、重载std::move操作符(是不是A(A&& m_i)?);3、shared_from_this。

    98、Echo服务器对应的客户端

    • 自己写工具: 实际工作中不可能一直等客户端那边联调,要自己写个简单模拟客户端的工具,方便测试。
    • 比较耗时的函数: 如果使用了同步的函数resolver.resolve(),就要小心了,它负责解析IP,有可能非常耗时。

    聊天服务器

    99、服务端1

    100、服务端2

    • chat_message.h: 存包头和包体的数据。
    • Session: start()开始后做的事情:1、加入房间;2、以前的消息发给这个客户端;3、等待接收消息;4、读message的包头,不出错再读包体;5、读包体不出错处理逻辑(这里是分发消息)然后继续读包头,循环下去。

    101、客户端1

    • io_service_.post(): 主线程的任务交由io_service去做,它会自己调用合适的某个线程完成。

    102、客户端2

    • close(): 主线程在输入出错后,也需要调用close()并且是由子线程去执行的,否则主线程将一直等待子线程的结束,导致无法正常停止。
    • ctrl+d: 输入状态正常退出,在客户端主线程中应用。如果上面close和join的顺序颠倒了,就无法退出,要ctrl+c才行。
    • 改写: 删除了chat_session的基类participant,觉得没有必要存在。

    消息协议的设计

    103、原始时代的C结构1

    • POD: (Plain Old Data)没有很特殊的结构数据。
    • 要求: 1、客户端和服务端都是C++编写;2、字节对齐方式一致;3、大小端一致。
    • string.find_first_of(" "): 与别的容器不同,返回的是位置,不是迭代器。

    104、原始时代的C结构2

    • 缺点: 1、结构绑的太死,C++服务端对应Flash客户端就有的搞了;2、消息定长,实际消息少了浪费,并且多了又装不下。3、要求太高,字节对齐、编码对齐、只能存POD(处理消息的工作很冗长(=或者memcpy),容易出错。)。4、不灵活,增加消息就要做一大堆操作。5、除非是维护老代码,不然这么干真的很蠢。
    • 经验: 教程中说这两节课顺带复习了C++的关键字reinterpret_cast,但实际编码中如果出现,心里就要嘀咕一下,是不是设计上有问题。
    • 疑问: 但是为什么公司的代码中没有用到

    105、超越原始的结构体,序列化c++的类01

    • 好处: 对象可以是流对象、文件对象、内存(网络收发到的消息,我自己的理解)。
    • 头文件: <boost/archive/text_iarchive.hpp><boost/archive/text_oarchive.hpp>

    106、超越原始的结构体,序列化c++的类02

    • makefile: 链接第三方库用-l。比如asio用到-lboost_system、序列化用到-lboost_serialization、多线程用到-lpthread
    • 扩展: 还有一个version没有用到,是用于类结构升级后正确处理的。

    107、使用简单易懂的json

    • 原因: 前面的两种方式都要求前后端使用C++,目前只有端游和少部分手游使用C++,所以非常不适用。
    • 进阶: JavaScript语言之间使用Json格式数据通信,目前也比较流行接近于标准化了。
    • 比较: 相对于Xml格式:1、数据量小;2、可读性强。
    • 库: C++处理json数据流行的库有七八种,这里使用boost自带的。
    • 头文件: <boost/property_tree/ptree.hpp><boost/property_tree/json_parser.hpp>。还能处理xml多种格式数据。
    • 优点: 前后端检错方便,可直接打印。中途也可用抓包工具查看。不像二进制,还需要对字节。
    • 缺点: 服务端客户端两边的修改要同步,虽然前面的也有这个问题,但用的是同一种数据结构,出错率稍低。1、太过灵活,比如key的值拼错了;2、可读性好代表的是冗余量大。
    • 进阶: 后面介绍更好的方法protobuf。但是1、json的可读性还是最好的,抓包什么的;2、并不是所有客户端都支持protobuf,但是基本都支持json。

    108、效率和可读性的结合使用protobuf 01

    • 原因: 总结前面三种方法,都存在消息冗余的问题。很多公司发现了这个问题后都自己研究了消息协议,Google的Protocol Buffer(简称protobuf)最为流行。后面内部人员又创建了FlatBuffer,听说在解析上面是PB的百倍,但使用方法稍复杂。
    • 优点: 有自己的文档,前后端有可参照的东西。写完了之后可以用Google的工具生成各语言对应的代码文件,省去人工操作,减少犯错率。
    • 原理: 头文件中会生成对应的类,派生自Message,Message中主要用到的有四个接口,序列化和反序列化。ParseFromIstream,实际用的比较多的是ParseFromString,来自Message的基类MessageLite。
    • 疑问: 数据结构中从什么时候开始,不需要用char[],而使用string的?

    109、效率和可读性的结合使用protobuf 02

    • 不做也可,做了更好的工作: 在main函数第一行调用宏GOOGLE_PROTOBUF_VERIFY_VERSION;,在运行程序前会检测安装的版本是否一致;main函数最后return之前,调用google::protobuf::ShutdownProtobufLibrary();本身程序结束时会释放的,但是其他检测工具可能会以为有内存泄露。

    110、效率和可读性的结合使用protobuf 03

    • 重点: 单线程异步。写这个聊天室作为起手,熟练之后再去写多线程下的服务端编程就容易多了。跑之前先学会走。

    参考自《C++游戏服务器开发入门到掌握》教学视频。

    如有侵权,请联系本人删除。

    展开全文
  • 面向对象纲要

    千次阅读 2017-05-17 19:30:01
    面向对象的基本纲要 供复习使用
  • JavaScript浏览器对象之history对象

    千次阅读 2019-05-06 18:21:45
    1 history对象概述 1.1 属性说明 1.2 方法说明 2 history.pushState(),history.replaceState() 3 history.state属性 4 popstate事件 1 history对象概述 浏览器窗口有一个history对象,用来保存浏览历史。...
  • JSTL fn 标签的使用

    千次阅读 2017-02-08 13:51:24
    在jstl的fn标签也是我们在网页设计经常要用到的很关键的标签,在使用的时候要先加上头 http://java.sun.com/jsp/jstl/functions" prefix="fn"%>就可以使用fn标签了。 使用此标签可以帮我们在jsp页面不再嵌入...
  • Java面向对象

    千次阅读 2013-10-07 19:47:17
    Java面向对象 ...而this他代表当前对象,当然所有的资源都可以访问. 在构造函数,如果第一行没有写super(),编译器会自动插入.但是如果父类没有不带参数的构造函数,或这个函数被私有化了(用private修饰)
  • 排索引、搜索引擎

    千次阅读 2017-03-27 21:05:07
    通常可以采用以下方式之一:   1: Wtf = 1 + log(TF)   2: Wtf = a + (1- a)* TF /Max (TF)  其中a为调节因子,经验取值a=0.5 最新研究表明是0.4效果更好。     单词 的文档频率DF...
  • 类与对象

    千次阅读 2012-10-06 19:43:02
    1、面向对象的特征有哪些方面,OOP的好处  类是具备某些共同特征的实体的集合,它是一种抽象的概念,用程序设计的语言来说,类是一种抽象的数据类型,它是对所具有相同特征实体的抽象。类定义了类的元素(属性和...
  • 原题目:开放世界游戏的大地图背后有哪些实现技术?   题目链接 补充说明:诸如GTA,武装突袭之类的游戏,开发者是如何实现超大地形的?对于这一问题有什么主流的解决方案? 补充:例如一些开发者提到的...
  • ZeroMQ:云时代极速消息通信

    千次阅读 2015-03-23 10:22:50
    ZeroMQ:云时代极速消息通信(大规模|可扩展|低成本|高效率解决之道,大规模分布式|多线程应用程序|消息传递架构构建利器) 【美】Pieter Hintjens(皮特.亨特金斯)著  卢涛 李颖 译 ISBN 978-7-121-25311-9 ...
  • JFugue4.0 中文说明

    千次阅读 2020-09-18 18:24:15
    Jfugue 可以简单并且允许工程师去快速创建音乐的原因是 MusicString,一个特殊格式描述音乐的字符串对象。 例如,播放 C(哆) 音符,可以使用如下简单的程序 Player player = new Player(); player.play("C"); ...
  •  缺点:没有面向对象易维护、易复用、易扩展 面向对象 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护 缺点:性能比面向过程低...
  • 常用iOS、Mac框架和及常用中文开发博客 文章来源:http://www.th7.cn/Program/IOS/201507/506590.shtml 目录 UI 下拉刷新模糊效果AutoLayout富文本图表表相关隐藏与显示HUD与Toast对话框其他UI ...
  • 简介(Introduction)Martin Odersky和Lex Spoon在许多人看来,新的集合框架是Scala 2.8最显著的改进。此前Scala也有集合(实际上新框架大部分地兼容了旧框架),但2.8的集合类在通用性、一致性和功能的丰富性上...
  • 在《道法自然——面向对象实践指南》一书,我们采用了一个对立统一的辩证关系来说明“模板方法”模式—— “正向依赖 vs. 依赖倒置”(参见:《道法自然》第15章[王咏武, 王咏刚 2004])。这种把“好莱坞”...
  • Here are the three ways you can add trailers to your movie collection (with their respective benefits and shortcomings): 您可以通过以下三种方式将预告片添加到电影收藏(具有各自的优点和缺点): ...
  • 面向对象设计六大基本原则-以Volley为例 作者 : Mr.Simple 这里的显示效果较好 : 原文链接 概述 在工作初期,我们可能会经常会有这样的感觉,自己的代码接口设计混乱、代码耦合较为严重、一个类的代码过多...
  •  编译ACE_TAO的方法我就不介绍了,在网上可以找到很多介绍。这里主要讲述我在编译过程遇到的一些问题以及解决办法,我遇到的问题主要有以下3个:  1、编译错误;  2、将编译后的加载到vxworks_6.9...
  • 第一章 对象的概念

    千次阅读 2020-04-02 23:05:47
    第一章 对象的概念 “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言表现出来并在无意识给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred ...
  • redis+结巴分词做排索引

    千次阅读 2019-03-14 21:15:23
    之前爬取过一百万的歌曲,包括歌手名,歌词等,最近了解到排索引,像es,solr这种太大,配置要求太高,对于一百万的数据量有些小题大做,所以想到了redis做一个排索引。 我的配置 这里说一下我的配置,后面用的...
  • ,为字体增加了阴影、内阴影和渐变色等,可以被用在任何标准的UILabel。FXLabel还提供了更多控件,可以对字体行距、字体间距等进行调整。 WFReader - 一款简单的coretext阅读器,支持文本选择、高亮以及字体大小...
  • 第三方大全

    千次阅读 2016-01-04 18:19:13
    自己总结的iOS、mac开源项目及,持续更新。。。。 github排名 https://github.com/trending,github搜索:https://github.com/search 目录 UI 下拉刷新模糊效果AutoLayout富文本图表表相关与Tabbar隐藏与...
  • MJRefresh – 仅需一行代码就可以为UITableView或者CollectionView加上下拉刷新或者上拉刷新功能。可以自定义上下拉刷新的文字说明。具体使用看“使用方法”。 (国人写)XHRefreshControl – XHRefreshCon

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 16,304
精华内容 6,521
关键字:

以下哪些对象可以添加到库中