精华内容
下载资源
问答
  • 机器辨别事物最基本的方法是计算,原则上是计算机要分析的事物与标准模板的相似程度进行计算。因此首先要从度量中看出不同事物之间的差异,才能分辨当前要识别的事物
  • 信息分类的基本原则与方法

    万次阅读 2013-09-30 13:47:29
    信息分类应根据信息内容的属性或特征,按照一定的规范和标准直,为了方便信息的交流与共享,应遵循以下原则: 1. 科学性 :在分类时,应选准信息的最稳定的本质属性,作为分类的基础和依据,确保一个稳定的分类...

    信息分类应根据信息内容的属性或特征,按照一定的规范和标准直,为了方便信息的交流与共享,应遵循以下原则:

    1.  科学性 :在分类时,应选准信息的最稳定的本质属性,作为分类的基础和依据,确保一个稳定的分类体系。

    2.  系统性: 在分类时,将选定信息的属性或特征按一定的排列顺序予以系统化,形成一个科学合理的分类体系。

    3.  可扩展性: 在分类时,分类应满足事物的不断发展和变化的需要。

    4.  兼容性: 在分类时,分类应兼容国际、国家相关标准及要求。

    5.  实用性: 在分类时,分类应考虑良好的可操作性,满足管理和应用的实际需求[1]。

    信息分类是企业资产管理的基础性工作, 同时又是一项系统性工程。因此, 在信息分类的过程中, 一定要以企业的具体需求为基础, 以实用为出发点, 采取正确的工作思路, 多样化的手段。信息分类常见的分类方法有两种:

    线分类法

    线分类法又称层级分类法,是指将分类对象按所选定的若干分类标志,逐次地分成相应的若干层级类目,并排列成一个有层次逐级展开的分类体系。分类体系的一般表现形式是大类、中类、小类等级别不同的类目逐级展开,体系中各层级所选用的标志不同,同位类构成并列关系,上下位类构成隶属关系。由一个类目直接划分出来的下一级各类目之间存在着并列关系,不重复,不交叉。具体示例如下:

    大类

    中类

    小类

    家具

    木制家具

    金属家具

    塑料家具

    竹滕家具

    床、椅、凳、桌、箱、架、橱窗

    线分类法应遵循的基本原则:

    1.   在线分类法中,由某一上位类类目划分出的下位类类目的总范围应与上位类类目范围相同(都属于家具)。

    2.   当一个上位类类目划分成若干个下位类类目时,应选择一个划分标志(按照制作原料)。

    3.   同位类类目之间不交叉、不重复,并只对应于一个上位类(木椅、木凳、木桌、木箱、木架)。

    4.   分类要依次进行,不应有空层或加层。

    线分类法的优缺点:

    Ø  优点:层次性好,能较好地反映类目之间的逻辑关系,使用方便,既适合于手工处理信息的传统习惯,又便于计算机处理信息。

    Ø  缺点:线分类体系存在着分类结构弹性差(分类结构一经确定,不易改动)、效率较低(当分类层次较多时,代码位数较长,影响数据处理的速度)。

    面分类法

    面分类法又称平行分类法,它是将拟分类的商品集合总体,根据其本身的属性或特征,分成相互之间没有隶属关系的面,每个面都包含一组类目。将每个面中的一种类目与另一个面中的一种类目组合在一起,即组成一个复合类目。

    服装的分类就是按照面分类法组配的。把服装用的面料、款式、穿着用途分为三个互相之间没有隶属关系的“面”,每个“面”又分成若干个类目。使用时,将有关类目组配起来。如:纯毛男式西装,纯棉女式连衣裙等。具体示例如下:

    第一面 面料

    第二面 式样

    第三面 款式

    纯棉

    纯毛

    化纤

    混纺

    男式

    女式

    西服

    衬衫

    套装

    休闲服

    面分类法应遵循的基本原则:

    1.   根据需要,应将分类对象的本质属性作为分类对象的标志。

    2.   不同面的类目之间不能相互交叉,也不能重复出现。

    3.   每个面有严格的固定位置。

    4.   面的选择以及位置的确定应根据实际需要而定。

    面分类法的优缺点:

    Ø 优点:具有较大的弹性,可以较大量地扩充新类目,不必预先确定好最后的分组,适用于计算机管理。

    Ø 缺点:组配结构太复杂,不便于手工处理,其容量也不能充分利用。

    信息编码是将某一类信息赋予一定的符号,为了满足实际业务应用,编码需要具备以下基本原则:

    1.  唯一性:编码必须保证每一个编码对象对应仅有一个代码。

    2.  可扩展性: 代码结构必须能够适应编码对象不断增加的需要

    3.  简单性:在不影响代码的容量和可扩展性的情况下, 代码尽量简短明确,以减少差错, 方便阅读、抄录

    4.  一贯性: 同一级代码类型、结构以及编写格式必须统一, 一直沿用代码格式,不要中途变化格式。

    5.  可操作性: 代码应尽可能反映编码对象的特点, 有助记忆,便于填写。少使用其他符号,如‘#’、‘-’、‘*’等。

    6.  稳定性: 代码不宜频繁变动,应考虑其变化的可能性,尽可能保持代码系统的相对稳定。

    在当前的企业应用中,编码的方式主要有以下几种:

    1    英文字母法:英文字母法是指将某项物资用特定的一个字母或一组字母来表示。

    2    数字法:指将某项物资用特定的一个数字或一组数字来表示的方法。数字法还可考虑以下几种编码方法。   

    a)   连续数字法,首先要求将所有物资进行分类,并按一定的规律先后排列,然后自1号起依顺序编排流水号,此方法优点是代号连贯,但未来新增类别时,不能在中间穿插,只能在后面添加。   

    b)   阶梯式数字法,首先要求将所有物资分成若干大类,其次再将各大类按其次级类别分成若干中类。

    c)   区段数字法,是介于连续数字法与阶梯式数字法之间的一种表示方法。

    d)   国际十进制分类法,是指将所有物资分为十大类,分别以0-9之间的数字代表;然后每大类再划分为十个中类,并分别再以0-9之间的数字代表,如此进行下去。 

    3    暗示法:是指根据物资的特性,采用特定的数字或符号使之能代表物资特性的方法,又可分为数字暗示和符号暗示法。 

    4    混合法:是指将英文字母和数字结合起来使用的方法。

    根据以上编码原则与方法,下面将根据企业资产管理过程中需要进行编码的内容进行举例说明,简单直观的了解编码过程中的关键因素。

    1.客户管理信息(混合法)

    X    X    XXXX    XXXXXX

                               第四层:邮政编码

                             第三层:客户代码

                          第二层:客户类别

                        第一层:客户信息类目: 

    编码:110BSF200137

    1-客户管理,1-直接客户,0BSF-巴斯夫公司,200137-邮政编码

    2.物料分类信息(国际十进制分类法)

    6                       应用科学

    62.                    工业技术

    621.                   机械的工业技术

    621.8                  动力传动

    621.88                 挟具

    621.882.              螺丝、螺帽

    621.882.2             各种小螺丝

    621.882.21            金属用小螺丝

    621.882.215           丸螺丝

    621.682.215.3         平螺丝

        信息编码是企业资产管理的基础性工作, 是实现企业信息共享和交互的前提和基础,总结信息编码的作用可以归结如下:

    1.  实现信息的交换与共享

    实现信息交换与共享的前提和基础是各交换和共享信息应具有一致性, 即当使用一个代码或术语时, 所指的是同一信息内容。这种一致性是建立在信息分类与编码对各信息系统的对每一信息的名称、描述、分类和代码共同约定的基础上, 就是说对同一数据应具有同一格式、同一含义。

    2.  改善数据质量, 降低冗余度

    信息分类与编码的实施将形成数据标准统一, 最大程度地消除因对信息的命名、描述、分类和编码不一致所造成的误解和分歧。减少一名多物、一物多名, 对同一名称的分类和描述的不同, 以及同一信息内容具有不同代码等现象。做到事物或概念的名称和术语统一化、规范化,并确立代码与事物或概念之间的一一对应, 以改善数据的准确性和相容性, 消除定义的冗余和不一致现象。

    3.  指导信息化建设

    通过信息分类与编码的实施,用户可了解信息资源的基本内容, 发现和定位信息资源, 实现公共信息资源的增值利用。通过信息分类与编码的实施, 将为构建稳健、集成的数据模型奠定坚实的基础。 通过信息分类与编码的实施, 能推动数据仓库建设、决策与支持系统、信息系统集成和信息化建设的发展, 为新一代信息系统建设奠定基础。


    展开全文
  • 《中文新闻信息分类标准》... 《分类》适用于通讯社(新闻社)、报社、期刊社、广播电台、电视台、网络媒体,以及各种资讯机构中文新闻信息进行分类标引和编制分类目录。 2 规范性引用文件 《分类》采用下列国家标
    《中文新闻信息分类标准》编制原则 
    
    2006-06-09 11:51:38 



    《中文新闻信息分类标准》编制原则

    1 范围
    《分类》规定了中文新闻信息分类体系和分类代码。
    《分类》规定了中文新闻信息分类方法的一般原则。
    《分类》适用于通讯社(新闻社)、报社、期刊社、广播电台、电视台、网络媒体,以及各种资讯机构对中文新闻信息进行分类标引和编制分类目录。
    2 规范性引用文件
    《分类》采用下列国家标准,其随后所有的修改或最新版本适用于本《分类》。
    GB/T 2659-2000 《世界各国和地区名称代码》
    GB/T 2260-2002 《中华人民共和国行政区划代码》
    GB 3304-91 《中国各民族名称的罗马字母拼写法和代码》
    3 术语和定义
    《分类》采用下列定义。
    3.1 中文新闻信息 Chinese news
    指通讯社、报纸、刊物、广播电台、电视台和网络媒体等,用中文对新近或正在发生的、对公众具有知悉意义的事实的报道,包括文字形式和数值、图形、图片、图表、图像和声音等非文字形式。
    3.2 网络媒体
    指利用互联网(Internet)发布或传播新闻信息的网站。和报刊、广播、电视等传统媒体比较,网络媒体在广泛性、即时性、开放性、共享性和互动性等方面更具优势。
    3.3新闻信息分类News classification
    指按照新闻信息的主题内容或其它特征,依据特定的分类标引工具(分类表),将它们分门别类地组织成科学体系的过程。
    3.4 分类表
    是对新闻信息进行分类标引和检索的工具,是分类法的表现形式。一般是将同一主题从大类到小类,按照逻辑系统逐级展开。内容包括类表(基本大类、简表、详表、复分表)、类目注释、编制说明及使用说明等。
    3.5 类
    指一组具有共同属性的事物的集合,又称类目。
    3.6 基本大类
    又称分类大纲。指分类表中列出的第一级类目。
    3.7 简表
    又称基本类目表。指由基本大类直接展开而形成的一、二级类目表,是分类表的骨架。
    3.8 详表
    又称主表。指在简表的基础上,将类目逐级扩展而成,是新闻信息分类标引的主要依据。
    3.9复分表
    又称附表、辅助表。指将详表中出现的共性子类目抽取出来,单独编列成表,供详表有关类目进一步细分时使用。目的是为了缩小类目表的篇幅和细分方便。复分表包括通用复分表和专用复分表。
    使用复分表进行分类标引时需遵守各表有关规定和总的组配规则,以保证标引的一致性。
    3.9.1 通用复分表
    指适用于整个主表的复分表,供对主表有关类目进一步细分用。附在主表之后。
    《分类》有如下通用复分表:总类复分表、人物复分表、企业复分表、世界国家(地区)代码表、中国行政地区代码表、中国各民族名称代码表和新闻信息体裁表。
    3.9.2 专用复分表
    指适用于主表中某一类的复分表,供对该类有关类目进一步细分用。附在主表某类中。
    《分类》有体育比赛专用复分表,农产品专用复分表等。
    3.11 仿分
    又称仿照复分。指后面的类目仿照前面类目所列出的下位类进行细分的方法。对具有共同分类标准的临近类目,常常采用此种方法。例如“政治”中的“民主党派”
    3.12 类目注释
    是对类目的补充说明,它帮助分类标引人员理解类目,准确归类。《分类》类目的注释主要有:
    指出类名含义。例如“科学技术”的“火炬计划”类目下注“中国高新技术产业的指导性计划”。
    指明类目内容。例如“信息产业”的“电子政务”类目下注“政府部门办公自动化、网络化、电子化以及全面信息共享入此”。
    指明类目间的关系和范围。例如“政治”的国家“地理概况”类下注“领土、领海、领空、专属经济区及渔业区、深海和外层空间资源享有、领属等入此。领土纠纷或边境冲突入‘外交、国际关系’”。
    指出细分方法。例如“政治”的“地方人民代表大会”类下注“可依中国行政区划表”分。又例如“政治”的“全国政协常委会”类下注“仿全国人大常委会分”。
    4编制原则
    《分类》根据新闻包罗万象、时间性强、综合性强,既容易形成专题,又相互交叉渗透的特点,以政治、经济和文化等社会领域为分类体系的基础,以新闻主题为主要立类原则,同时注意吸收图书、档案等各种分类中适合新闻信息分类的元素,力求使《分类》既能反映新闻信息的特点,又能做到实用性和科学性、稳定性、可扩展性相统一。
    4.1科学性原则
    《分类》采用主题与学科相结合的立类方法,使分类体系具有主题的直接性和学科的系统性。
    《分类》各基本大类都采取从总到分、从一般到具体的等级分类方法。
    《分类》由主表和复分表共同构成完整的体系。
    4.2 实用性原则
    随着社会的发展,新闻信息内容变得日益广泛,内容重点发生重大变化,用户检索需求也日趋多样化。《分类》在保证类目体系科学性、逻辑性的同时,把一些新闻信息量大、用户比较关注的内容跨越逻辑层次,作为基本大类列出,以期达到重点突出,降低分类难度和类目设置相对平衡的目的。例如,按照学科分类法处于“经济”二级类目的农业、工业、金融、能源、交通运输、信息产业、贸易、服务业等在本《分类》中均成为一级大类。
    4.3 稳定性原则
    《分类》在一、二级类目的设置上,充分考虑与国计民生、社会发展息息相关的各个重要领域,总结和继承我国主要新闻媒体数十年分类工作中积累的经验,吸收和借鉴国内外一些著名的分类标准,力求使其具有稳定性和兼容性。
    4.4可扩展性原则
    《分类》的可扩展性原则体现在两方面:
    1, 随着社会的发展,新事物、新学科、新技术不断涌现,《分类》在类目扩展上预留充
    足的空间。
    2, 《分类》是为了适应各种传媒机构对中文新闻信息进行分类的共同需要而编制的。但考虑
    到每个传媒机构在新闻信息的收集重点、分类粗细、数据库规模等方面存在许多差异。为此,允许各传媒机构在不破坏《分类》类目体系和标记规则的前提下,可以制定适合自己单位执行《分类》的使用本或分类细则。
    4.5 《分类》在工业类目的设置上借鉴了GB/T 4754-2002《国民经济行业分类与代码》,在科学技术类目的设置上借鉴了 GB/T 13745-92 《学科分类与代码》。但根据新闻报道的特点对其中一些类作了调整,例如“工业”中增加了“家用电器及非电力器具” 类,将家用视听设备、家用摄录设备、家用制冷电器、家用空调器、家用通风电器、家用厨房电器、家用清洁、卫生电器、家用美容、保健电器以及燃气器具、太阳能器具等非电力家用器具相对集中。又例如,将“法学”、“军事学”、“新闻学与传播学”、“计算机科学技术”、“纺织科学技术”、“食品科学技术”、“交通运输工程”等分别合并到“法制”、“军事”、“传媒业”、“纺织业”、“食品、饮料”、“交通运输”等类中。
    4.6《分类》根据新闻信息大多综合性强、内容层次较浅的特点,在农业、工业等经济门类的类目设置上,一般将科学研究、技术开发、产品制造(生产)、市场销售等汇集在一起。
    4.7 《分类》将交叉或具有双重属性的新闻信息两处列类,但其中一处只起参见的作用,其代码用[ ]表示,类目下加注说明。例如,“工业”的“[09001400600003]航天器”类目下注“宜入‘航空、航天科学技术’”,意思是把09工业中的航天器制造方面的新闻信息集中到“航空、航天科学技术”类。这样做的目的是使航天方面的信息从发展规划、科学研究、技术发展、航天器制造、发射等都相对集中。
    4.8 《分类》中各类出现的分类层次和数量分布不均衡现象,是事物发展不平衡在新闻报道中的体现,是客观实际所决定的。

    5编码方法和代码结构
    5.1类目层次设置
    主表的类目层次最多到5级。
    复分表中,世界各国和地区、中国行政区划、中国民族名称的类目设置分别按GB/T 2659-2000 《世界各国和地区名称代码》、GB/T 2260-2002 《中华人民共和国行政区划代码》、GB 3304-91 《中国各民族名称的罗马字母拼写法和代码》执行;其它复分表最多设2级。
    5.2 类目代码
    主表类目代码用阿拉伯数字表示。
    主表采用十进分类法,每一级类目用2位阿拉伯数字表示(01-99)。二级及其以下类目代码由上位类代码加顺序码组成,顺序码前面加“0”,作为类目之间的分隔符号。
    复分表中,世界各国和地区、中国行政区划、中国民族名称的类目代码分别按GB/T 2659-2000 《世界各国和地区名称代码》、GB/T 2260-2002 《中华人民共和国行政区划代码》、GB 3304-91 《中国各民族名称的罗马字母拼写法和代码》执行;其它复分表的类目代码设置与主表的作法相同。
    5.3 类目代码结构图
    本标准主表的代码结构图如下:
    依次类推
    (数字)一级类目代码
    ××
    (数字)二级类目代码
    ×××
    (数字)三级类目代码
    ×××
    (数字)四级类目代码
    ×××
    ……














    5.4标识符号
    - 为总类标识符号。
    < > 为国家(地区)标识符号。
    ( )为中国行政区划标识符号。
    “ ”为民族标识符号。
    ‘ ’为人物标识符号。
    { } 为企业标识符号。
    [ ] 为类目交替符号。


    6使用指南
    6.1 各单位对《分类》的使用
    每个新闻媒体单位都有自己的特点和新闻信息收集重点。所以,对《分类》的使用要求也不尽一致。为此,可在统一的分类标准下,制定本单位所需的分类细则,制定合适的使用本。一般可以包括以下几个方面:
    6.1.1根据本单位的实际需要,选择《分类》主表中的一部分类目制订本单位的使用本。
    6.1.2根据本单位的需要,对主表一些下位类目进行扩充。
    6.1.3规定出适合本单位分类需要的类目仿分、复分的范围和办法。包括哪一些类目需要仿分、复分,对连续仿分、复分的注释规定出是部分采用,还是只仿分、复分到某一层次。
    6.1.4对复分表的使用作出选择。选择时要确定使用哪些复分表,不使用哪些;哪些复分表的子目是全部使用,还是使用到哪一级;哪一级类目使用复分表,哪一级类目不再使用复分表;复分表子目中的再细分注释,是采用还是不采用,等等。
    6.1.5对交替类目的使用作出选择。
    但是,在制定本单位使用本时需要遵循两个原则,一是实用,符合本单位的实际需要;二是不破坏本分类法类目体系和标记制度,一、二级类目必须照用。
    另外,要将本单位的使用本报送《分类》的管理单位备案。
    6.2 分类标引基本规则
    6.2.1以新闻信息内容为分类标引的主要依据。切忌单凭新闻标题作为分类的依据。
    6.2.2正确处理多主题新闻信息的分类。对此,应首先要对内容进行分析,找出其中最能代表报道内容本质或内容中起主导作用的主题。
    6.2.3保持同类信息的集中。内容相近的新闻信息应选取同一个类号。对某新闻信息进行改编、翻译、评论,一般要同被改编、翻译或评论的新闻信息的类号一致,连载的新闻信息要同初载的类号一致。
    6.2.4正确处理不同类型和不同载体新闻信息的分类。不同类型(图书、报刊、内部资料、论文、会议报告等)和不同载体(声像、图片、缩微等)的信息都应按照其内容进行分类,只有这样,才能方便用户按主题检索到全部信息。
    6.3 复分表的使用
    6.2.1总类复分表的使用。该表适用于任何一级类目,使用时,只需将复分号加在主表分类号之后,并用标识符号 “-”连接。
    《分类》主表中,某些基本大类因有特殊需要,设置了与总类复分表相同的下位类目。在进行新闻信息分类标引时,如果遇到这种情况,则不再按照总类复分表进行分类。
    6.2.2其它复分表的使用。使用世界国家(地区)表、中华人民共和国行政区划表、中国民族名称和代码表、新闻体裁表、人物表、企业表时,只需将复分号加在主表分类号之后,并用复分标识符号连接。



























    附录A
    (规范性附录)
    总类复分表

    1 使用说明
    ——本表适用于主表中任何一级类目复分组配。
    ——使用本表复分时,复分号均须连同专用标识符号“”一起加在主类号之后。
    ——凡主表中已列有专类者,不再用本表相关类目复分。
    2 类目
    01 综合(含历史沿革、概况和综合性新闻信息)
    02 体制
    03 理论
    04 政策
    05 法规
    06 规划
    07 管理
    08 机构
    09 交流
    11 统计
    12 趣闻
    13 常识



    附录B
    (规范性附录)
    人物复分表
    01 生平(包括年表、年谱、大事记)、简历
    02 诞辰纪念
    03 逝世纪念
    04 故乡、故居
    05 亲属
    06 公务活动(国内接见入此)
    07 国内视察、考察
    08 出席会议(国内会议、国际会议)
    09 出国访问(国外会见入此)
    11 著作、作品
    12 讲话
    13 署名文章
    14 题词、题名、作序
    15 信件、日记
    16 批示、手迹
    17 图片
    18 影视记录片
    19 荣誉称号
    21 兴趣、爱好


    附录 C
    (规范性附录)
    企业复分表
    企业管理
    企业人事
    企业财务
    企业上市
    企业投融资
    企业评级
    企业改革
    企业改制
    企业并购
    企业破产
    企业垄断行为
    企业报告
    企业文化
    附录D
    (规范性附录)
    世界国家(地区)代码表

    GB/T 2659-2000 《世界各国和地区名称代码》







































    附录E
    (规范性附录)
    中国行政地区代码表
    GB/T 2260-2002 《中华人民共和国行政区划代码》


















    附录F
    (规范性附录)
    中国各民族名称代码表
    GB 3304-91 《中国各民族名称的罗马字母拼写法和代码》




















    附录G
    (规范性附录)
    新闻信息体裁表
    1 使用说明
    ——本表适用于主表中任何一级类目复分组配。
    ——使用本表复分时,复分号均须连同专用标识符号“”一起加在主类号之后。
    2 类目


    01 消息
    02 特写
    03 评论(言论、编辑部文章)
    04 社论
    05 通讯
    06 专访
    07 新闻调查
    08 专题
    09 大事记
    10 访谈
    11 速写
    12 散记
    13 侧记
    14 札记、手记
    15 年终报道
    16 特别报道/典型报道
    17 热线
    18 简讯(标题新闻)
    19 背景资料
    20统计数据(移到总类复分)
    21 公报、公告
    22 市场行情
    23 图片
    24 图表
    25 排行榜
    26 分析报告
    27 讲话稿
    28 白皮书
    29 来信
    30 民意测验
    31 译文
    32 广告
    33 更正
    34 其它

    本文转自
    http://xxj.liuzhou.gov.cn/tblm/ztbd/t20060609_26087.htm
    展开全文
  • 面向对象六大原则

    万次阅读 多人点赞 2015-11-30 00:10:44
    1、优化代码的第一步——单一职责原则单一职责原则的英文名称是Single Responsibility Principle,简称SRP。它的定义是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该是一组相关性很高的...

    1、优化代码的第一步——单一职责原则

    单一职责原则的英文名称是Single Responsibility Principle,简称SRP。它的定义是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该是一组相关性很高的函数、数据的封装。就像秦小波老师在《设计模式之禅》中说的:“这是一个备受争议却又及其重要的原则。只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的”。因为单一职责的划分界限并不是总是那么清晰,很多时候都是需要靠个人经验来界定。当然,最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。
    对于计算机技术,通常只单纯地学习理论知识并不能很好地领会其深意,只有自己动手实践,并在实际运用中发现问题、解决问题、思考问题,才能够将知识吸收到自己的脑海中。下面以我的朋友小民的事迹说起。

    自从Android系统发布以来,小民就是Android的铁杆粉丝,于是在大学期间一直保持着对Android的关注,并且利用课余时间做些小项目,锻炼自己的实战能力。毕业后,小民如愿地加入了心仪的公司,并且投入到了他热爱的Android应用开发行业中。将爱好、生活、事业融为一体,小民的第一份工作也算是顺风顺水,一切尽在掌握中。
    在经历过一周的适应期以及熟悉公司的产品、开发规范之后,小民的开发工作就正式开始了。小民的主管是个工作经验丰富的技术专家,对于小民的工作并不是很满意,尤其小民最薄弱的面向对象设计,而Android开发又是使用Java语言,什么抽象、接口、六大原则、23种设计模式等名词把小民弄得晕头转向。小民自己也察觉到了自己的问题所在,于是,小民的主管决定先让小民做一个小项目来锻炼锻炼这方面的能力。正所谓养兵千日用兵一时,磨刀不误砍柴工,小民的开发之路才刚刚开始。

    在经过一番思考之后,主管挑选了使用范围广、难度也适中的ImageLoader(图片加载)作为小民的训练项目。既然要训练小民的面向对象设计,那么就必须考虑到可扩展性、灵活性,而检测这一切是否符合需求的最好途径就是开源。用户不断地提出需求、反馈问题,小民的项目需要不断升级以满足用户需求,并且要保证系统的稳定性、灵活性。在主管跟小民说了这一特殊任务之后,小民第一次感到了压力,“生活不容易呐!”年仅22岁至今未婚的小民发出了如此深刻的感叹!

    挑战总是要面对的,何况是从来不服输的小民。主管的要求很简单,要小民实现图片加载,并且要将图片缓存起来。在分析了需求之后,小民一下就放心下来了,“这么简单,原来我还以为很难呢……”小民胸有成足的喃喃自语。在经历了十分钟的编码之后,小民写下了如下代码:

    /**
     * 图片加载类
     */
    public class ImageLoader {
        // 图片缓存
        LruCache<String, Bitmap> mImageCache;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
        public ImageLoader() {
            initImageCache();
        }
    
        private void initImageCache() {
                // 计算可使用的最大内存
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
                // 取四分之一的可用内存作为缓存
            final int cacheSize = maxMemory / 4;
            mImageCache = new LruCache<String, Bitmap>(cacheSize) {
    
                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                }
            };
        }                   
    
        public  void displayImage(final String url, final ImageView imageView) {
            imageView.setTag(url);
            mExecutorService.submit(new Runnable() {
    
               @Override
                public  void run() {
                  Bitmap bitmap = downloadImage(url);
                    if (bitmap == null) {
                        return;
                    }
                    if (imageView.getTag().equals(url)) {
                        imageView.setImageBitmap(bitmap);
                    }
                    mImageCache.put(url, bitmap);
              }
           });
        }
    
        public  Bitmap downloadImage(String imageUrl) {
            Bitmap bitmap = null;
            try {
                URL url = newURL(imageUrl);
                final HttpURLConnection conn =         
                    (HttpURLConnection)url.openConnection();
                bitmap = BitmapFactory.decodeStream(
                      conn.getInputStream());
                conn.disconnect();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return bitmap;
        }
    }

    并且使用git软件进行版本控制,将工程托管到github上,伴随着git push命令的完成,小民的ImageLoader 0.1版本就正式发布了!如此短的时间内就完成了这个任务,而且还是一个开源项目,小民暗暗自喜,幻想着待会儿主管的称赞。

    在小民给主管报告了ImageLoader的发布消息的几分钟之后,主管就把小民叫到了会议室。这下小民纳闷了,怎么夸人还需要到会议室。“小民,你的ImageLoader耦合太严重啦!简直就没有设计可言,更不要说扩展性、灵活性了。所有的功能都写在一个类里怎么行呢,这样随着功能的增多,ImageLoader类会越来越大,代码也越来越复杂,图片加载系统就越来越脆弱……”Duang,这简直就是当头棒喝,小民的脑海里已经听不清主管下面说的内容了,只是觉得自己之前没有考虑清楚就匆匆忙忙完成任务,而且把任务想得太简单了。

    “你还是把ImageLoader拆分一下,把各个功能独立出来,让它们满足单一职责原则。”主管最后说道。小民是个聪明人,敏锐地捕捉到了单一职责原则这个关键词。用Google搜索了一些优秀资料之后总算是对单一职责原则有了一些认识。于是打算对ImageLoader进行一次重构。这次小民不敢过于草率,也是先画了一幅UML图,如图1-1所示。

    图1-1

    ImageLoader代码修改如下所示:

    /**
     * 图片加载类
     */
    public  class ImageLoader {
        // 图片缓存
        ImageCache mImageCache = new ImageCache() ;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
        // 加载图片
        public  void displayImage(final String url, final ImageView imageView) {
            Bitmap bitmap = mImageCache.get(url);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }
            imageView.setTag(url);
            mExecutorService.submit(new Runnable() {
    
                @Override
                public void run() {
                Bitmap bitmap = downloadImage(url);
                    if (bitmap == null) {
                        return;
                    }
                    if (imageView.getTag().equals(url)) {
                        imageView.setImageBitmap(bitmap);
                    }
                    mImageCache.put(url, bitmap);
                }
            });
         }
    
        public  Bitmap downloadImage(String imageUrl) {
            Bitmap bitmap = null;
            try {
                URL url = new URL(imageUrl);
                final HttpURLConnection conn = 
                (HttpURLConnection) 
                            url.openConnection();
                bitmap = BitmapFactory.decodeStream(conn.getInputStream());
                conn.disconnect();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return bitmap;
        }
    }   

    并且添加了一个ImageCache类用于处理图片缓存,具体代码如下:

    public class ImageCache {
        // 图片LRU缓存
        LruCache<String, Bitmap> mImageCache;
    
        public ImageCache() {
            initImageCache();
        }
    
        private void initImageCache() {
             // 计算可使用的最大内存
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
            // 取四分之一的可用内存作为缓存
            final int cacheSize = maxMemory / 4;
            mImageCache = new LruCache<String, Bitmap>(cacheSize) {
    
                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getRowBytes() *  
                        bitmap.getHeight() / 1024;
               }
            };
         }
    
        public void put(String url, Bitmap bitmap) {
            mImageCache.put(url, bitmap) ;
        }
    
        public Bitmap get(String url) {
            return mImageCache.get(url) ;
        }
    }

    如图1-1和上述代码所示,小民将ImageLoader一拆为二,ImageLoader只负责图片加载的逻辑,而ImageCache只负责处理图片缓存的逻辑,这样ImageLoader的代码量变少了,职责也清晰了,当与缓存相关的逻辑需要改变时,不需要修改ImageLoader类,而图片加载的逻辑需要修改时也不会影响到缓存处理逻辑。主管在审核了小民的第一次重构之后,对小民的工作给予了表扬,大致意思是结构变得清晰了许多,但是可扩展性还是比较欠缺,虽然没有得到主管的完全肯定,但也是颇有进步,再考虑到自己确实有所收获,小民原本沮丧的心里也略微地好转起来。

    从上述的例子中我们能够体会到,单一职责所表达出的用意就是“单一”二字。正如上文所说,如何划分一个类、一个函数的职责,每个人都有自己的看法,这需要根据个人经验、具体的业务逻辑而定。但是,它也有一些基本的指导原则,例如,两个完全不一样的功能就不应该放在一个类中。一个类中应该是一组相关性很高的函数、数据的封装。工程师可以不断地审视自己的代码,根据具体的业务、功能对类进行相应的拆分,我想这会是你优化代码迈出的第一步。

    2、让程序更稳定、更灵活——开闭原则

    开闭原则的英文全称是Open Close Principle,简称OCP,它是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。当然,在现实开发中,只通过继承的方式来升级、维护原有系统只是一个理想化的愿景,因此,在实际的开发过程中,修改原有代码、扩展代码往往是同时存在的。

    软件开发过程中,最不会变化的就是变化本身。产品需要不断地升级、维护,没有一个产品从第一版本开发完就再没有变化了,除非在下个版本诞生之前它已经被终止。而产品需要升级,修改原来的代码就可能会引发其他的问题。那么如何确保原有软件模块的正确性,以及尽量少地影响原有模块,答案就是尽量遵守本章要讲述的开闭原则。

    勃兰特·梅耶在1988年出版的《面向对象软件构造》一书中提出这一原则。这一想法认为,一旦完成,一个类的实现只应该因错误而被修改,新的或者改变的特性应该通过新建不同的类实现。新建的类可以通过继承的方式来重用原类的代码。显然,梅耶的定义提倡实现继承,已存在的实现对于修改是封闭的,但是新的实现类可以通过覆写父类的接口应对变化。
    说了这么多,想必大家还是半懂不懂,还是让我们以一个简单示例说明一下吧。

    在对ImageLoader进行了一次重构之后,小民的这个开源库获得了一些用户。小民第一次感受到自己发明“轮子”的快感,对开源的热情也越发高涨起来!通过动手实现一些开源库来深入学习相关技术,不仅能够提升自我,也能更好地将这些技术运用到工作中,从而开发出更稳定、优秀的应用,这就是小民的真实想法。

    小民第一轮重构之后的ImageLoader职责单一、结构清晰,不仅获得了主管的一点肯定,还得到了用户的夸奖,算是个不错的开始。随着用户的增多,有些问题也暴露出来了,小民的缓存系统就是大家“吐槽”最多的地方。通过内存缓存解决了每次从网络加载图片的问题,但是,Android应用的内存很有限,且具有易失性,即当应用重新启动之后,原来已经加载过的图片将会丢失,这样重启之后就需要重新下载!这又会导致加载缓慢、耗费用户流量的问题。小民考虑引入SD卡缓存,这样下载过的图片就会缓存到本地,即使重启应用也不需要重新下载了!小民在和主管讨论了该问题之后就投入了编程中,下面就是小民的代码。
    DiskCache.java类,将图片缓存到SD卡中:

    public class DiskCache {
        // 为了简单起见临时写个路径,在开发中请避免这种写法 !
        static String cacheDir = "sdcard/cache/";
         // 从缓存中获取图片
        public Bitmap get(String url) {
            return BitmapFactory.decodeFile(cacheDir + url);
        }
    
        // 将图片缓存到内存中
        public  void  put(String url, Bitmap bmp) {
           FileOutputStream fileOutputStream = null;
            try {
                fileOutputStream = new 
                     FileOutputStream(cacheDir + url);
                bmp.compress(CompressFormat.PNG, 
                     100, fileOutputStream);
          } catch (FileNotFoundException e) {
                e.printStackTrace();
          } final ly {
                if (fileOutputStream != null) {
                    try {
                        fileOutputStream.close();
                  } catch (IOException e) {
                        e.printStackTrace();
                 }
              }
          }
        }
    }

    因为需要将图片缓存到SD卡中,所以,ImageLoader代码有所更新,具体代码如下:

    public class ImageLoader {
        // 内存缓存
        ImageCache mImageCache = new ImageCache();
        // SD卡缓存
        DiskCache mDiskCache = new DiskCache();
        // 是否使用SD卡缓存
        boolean isUseDiskCache = false;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
    
        public  void displayImage(final String url, final ImageView imageView) {
            // 判断使用哪种缓存
           Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) 
                    : mImageCache.get (url);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
           }
            // 没有缓存,则提交给线程池进行下载
        }
    
        public void useDiskCache(boolean useDiskCache) {
            isUseDiskCache = useDiskCache ;
        }
    }

    从上述的代码中可以看到,仅仅新增了一个DiskCache类和往ImageLoader类中加入了少量代码就添加了SD卡缓存的功能,用户可以通过useDiskCache方法来对使用哪种缓存进行设置,例如:

    ImageLoader imageLoader = new ImageLoader() ;
     // 使用SD卡缓存
    imageLoader.useDiskCache(true);
    // 使用内存缓存
    imageLoader.useDiskCache(false);

    通过useDiskCache方法可以让用户设置不同的缓存,非常方便啊!小民对此很满意,于是提交给主管做代码审核。“小民,你思路是对的,但是有些明显的问题,就是使用内存缓存时用户就不能使用SD卡缓存,类似的,使用SD卡缓存时用户就不能使用内存缓存。用户需要这两种策略的综合,首先缓存优先使用内存缓存,如果内存缓存没有图片再使用SD卡缓存,如果SD卡中也没有图片最后才从网络上获取,这才是最好的缓存策略。”主管真是一针见血,小民这时才如梦初醒,刚才还得意洋洋的脸上突然有些泛红……
    于是小民按照主管的指点新建了一个双缓存类DoudleCache,具体代码如下:

    /**
     * 双缓存。获取图片时先从内存缓存中获取,如果内存中没有缓存该图片,再从SD卡中获取。
     *  缓存图片也是在内存和SD卡中都缓存一份
     */
    public class DoubleCache {
        ImageCache mMemoryCache = new ImageCache();
        DiskCache mDiskCache = new DiskCache();
    
        // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
        public   Bitmap get(String url) {
           Bitmap bitmap = mMemoryCache.get(url);
            if (bitmap == null) {
                bitmap = mDiskCache.get(url);
            }
            return  bitmap;
        }
    
        // 将图片缓存到内存和SD卡中
        public void put(String url, Bitmap bmp) {
            mMemoryCache.put(url, bmp);
            mDiskCache.put(url, bmp);
       }
    }

    我们再看看最新的ImageLoader类吧,代码更新也不多:

    public class ImageLoader {
        // 内存缓存
        ImageCache mImageCache = new ImageCache();
        // SD卡缓存
        DiskCache mDiskCache = new DiskCache();
        // 双缓存
        DoubleCache mDoubleCache = new DoubleCache() ;
        // 使用SD卡缓存
        boolean isUseDiskCache = false;
        // 使用双缓存
        boolean isUseDoubleCache = false;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
    
        public void displayImage(final String url, final ImageView imageView) {
            Bitmap bmp = null;
             if (isUseDoubleCache) {
                bmp = mDoubleCache.get(url);
            } else if (isUseDiskCache) {
                bmp = mDiskCache.get(url);
            } else {
                bmp = mImageCache.get(url);
            }
    
             if ( bmp != null ) {
                imageView.setImageBitmap(bmp);
            }
            // 没有缓存,则提交给线程池进行异步下载图片
        }
    
        public void useDiskCache(boolean useDiskCache) {
            isUseDiskCache = useDiskCache ;
        }
    
        public void useDoubleCache(boolean useDoubleCache) {
            isUseDoubleCache = useDoubleCache ;
        }
    }

    通过增加短短几句代码和几处修改就完成了如此重要的功能。小民已越发觉得自己Android开发已经到了的得心应手的境地,不仅感觉一阵春风袭来,他那飘逸的头发一下从他的眼前拂过,小民感觉今天天空比往常敞亮许多。

    “小民,你每次加新的缓存方法时都要修改原来的代码,这样很可能会引入Bug,而且会使原来的代码逻辑变得越来越复杂,按照你这样的方法实现,用户也不能自定义缓存实现呀!”到底是主管水平高,一语道出了小民这缓存设计上的问题。

    我们还是来分析一下小民的程序,小民每次在程序中加入新的缓存实现时都需要修改ImageLoader类,然后通过一个布尔变量来让用户使用哪种缓存,因此,就使得在ImageLoader中存在各种if-else判断,通过这些判断来确定使用哪种缓存。随着这些逻辑的引入,代码变得越来越复杂、脆弱,如果小民一不小心写错了某个if条件(条件太多,这是很容易出现的),那就需要更多的时间来排除。整个ImageLoader类也会变得越来越臃肿。最重要的是用户不能自己实现缓存注入到ImageLoader中,可扩展性可是框架的最重要特性之一。

    “软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的,这就是开放-关闭原则。也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。”小民的主管补充到,小民听得云里雾里的。主管看小民这等反应,于是亲自“操刀”,为他画下了如图1-2的UML图。


    图1-2

    小民看到图1-2似乎明白些什么,但是又不是太明确如何修改程序。主管看到小民这般模样只好亲自上阵,带着小民把ImageLoader程序按照图1-2进行了一次重构。具体代码如下:

    public class ImageLoader {
        // 图片缓存
        ImageCache mImageCache = new MemoryCache();
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
        // 注入缓存实现
        public void setImageCache(ImageCache cache) {
            mImageCache = cache;
        }
    
        public void displayImage(String imageUrl, ImageView imageView) {
            Bitmap bitmap = mImageCache.get(imageUrl);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }
            // 图片没缓存,提交到线程池中下载图片
            submitLoadRequest(imageUrl, imageView);
        }
    
        private void submitLoadRequest(final String imageUrl,
                 final ImageView imageView) {
            imageView.setTag(imageUrl);
            mExecutorService.submit(new Runnable() {
    
                @Override
                public  void run() {
                  Bitmap bitmap = downloadImage(imageUrl);
                    if (bitmap == null) {
                        return;
                 }
                   if (imageView.getTag().equals(imageUrl)) {
                        imageView.setImageBitmap(bitmap);
                 }
                    mImageCache.put(imageUrl, bitmap);
             }
          });
        }
    
        public  Bitmap downloadImage(String imageUrl) {
           Bitmap bitmap = null;
            try {
               URL url = new URL(imageUrl);
                final HttpURLConnection conn = (HttpURLConnection) 
                            url.openConnection();
                bitmap = BitmapFactory.decodeStream(conn.getInputStream());
                conn.disconnect();
            } catch (Exception e) {
                  e.printStackTrace();
            }
    
            return bitmap;
        }
    }

    经过这次重构,没有了那么多的if-else语句,没有了各种各样的缓存实现对象、布尔变量,代码确实清晰、简单了很多,小民对主管的崇敬之情又“泛滥”了起来。需要注意的是,这里的ImageCache类并不是小民原来的那个ImageCache,这次程序重构主管把它提取成一个图片缓存的接口,用来抽象图片缓存的功能。我们看看该接口的声明:

    public interface ImageCache {
        public Bitmap get(String url);
        public void put(String url, Bitmap bmp);
    }

    ImageCache接口简单定义了获取、缓存图片两个函数,缓存的key是图片的url,值是图片本身。内存缓存、SD卡缓存、双缓存都实现了该接口,我们看看这几个缓存实现:

    // 内存缓存MemoryCache类
    public class MemoryCache implements ImageCache {
        private LruCache<String, Bitmap> mMemeryCache;
    
        public MemoryCache() {
            // 初始化LRU缓存
        }
    
         @Override
        public Bitmap get(String url) {
            return mMemeryCache.get(url);
        }
    
        @Override
        public void put(String url, Bitmap bmp) {
            mMemeryCache.put(url, bmp);
        }
    }
    
    // SD卡缓存DiskCache类
    public  class  DiskCache implements ImageCache {
        @Override
        public Bitmap get(String url) {
            return null/* 从本地文件中获取该图片 */;
        }
    
        @Override
        public void put(String url, Bitmap bmp) {
            // 将Bitmap写入文件中
        }
    }
    
    // 双缓存DoubleCache类
    public class DoubleCache implements ImageCache{
        ImageCache mMemoryCache = new MemoryCache();
        ImageCache mDiskCache = new DiskCache();
    
        // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
        public Bitmap get(String url) {
           Bitmap bitmap = mMemoryCache.get(url);
            if (bitmap == null) {
                bitmap = mDiskCache.get(url);
           }
            return bitmap;
         }
    
        // 将图片缓存到内存和SD卡中
        public void put(String url, Bitmap bmp) {
            mMemoryCache.put(url, bmp);
            mDiskCache.put(url, bmp);
        }
    }

    细心的朋友可能注意到了,ImageLoader类中增加了一个setImageCache(ImageCache cache)函数,用户可以通过该函数设置缓存实现,也就是通常说的依赖注入。下面就看看用户是如何设置缓存实现的:

    ImageLoader imageLoader = new ImageLoader() ;
            // 使用内存缓存
    imageLoader.setImageCache(new MemoryCache());
            // 使用SD卡缓存
    imageLoader.setImageCache(new DiskCache());
            // 使用双缓存
    imageLoader.setImageCache(new DoubleCache());
            // 使用自定义的图片缓存实现
    imageLoader.setImageCache(new ImageCache() {
    
                @Override
            public void put(String url, Bitmap bmp) {
                // 缓存图片
           }
    
                @Override
            public Bitmap get(String url) {
                return null/*从缓存中获取图片*/;
           }
        });

    在上述代码中,通过setImageCache(ImageCache cache)方法注入不同的缓存实现,这样不仅能够使ImageLoader更简单、健壮,也使得ImageLoader的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache缓存图片的具体实现完全不一样,但是,它们的一个特点是都实现了ImageCache接口。当用户需要自定义实现缓存策略时,只需要新建一个实现ImageCache接口的类,然后构造该类的对象,并且通过setImageCache(ImageCache cache)注入到ImageLoader中,这样ImageLoader就实现了变化万千的缓存策略,而扩展这些缓存策略并不会导致ImageLoader类的修改。经过这次重构,小民的ImageLoader已经基本算合格了。咦!这不就是主管说的开闭原则么!“软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。而遵循开闭原则的重要手段应该是通过抽象……”小民细声细语的念叨中,陷入了思索中……

    开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。这里的“应该尽量”4个字说明OCP原则并不是说绝对不可以修改原始类的,当我们嗅到原来的代码“腐化气味”时,应该尽早地重构,以使得代码恢复到正常的“进化”轨道,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。我们的开发过程中也没有那么理想化的状况,完全地不用修改原来的代码,因此,在开发过程中需要自己结合具体情况进行考量,是通过修改旧代码还是通过继承使得软件系统更稳定、更灵活,在保证去除“代码腐化”的同时,也保证原有模块的正确性。

    3、构建扩展性更好的系统——里氏替换原则

    里氏替换原则英文全称是Liskov Substitution Principle,简称LSP。它的第一种定义是:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。上面这种描述确实不太好理解,理论家有时候容易把问题抽象化,本来挺容易理解的事让他们一概括就弄得拗口了。我们再看看另一个直截了当的定义。里氏替换原则第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。

    我们知道,面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是,所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。说了那么多,其实最终总结就两个字:抽象。
    小民为了深入地了解Android中的Window与View的关系特意写了一个简单示例,为了便于理解,我们先看如图1-3所示。

    ▲图1-3

    我们看看具体的代码:

    // 窗口类
    public class Window {
        public void show(View child){
            child.draw();
        }
    }
    
    // 建立视图抽象,测量视图的宽高为公用代码,绘制交给具体的子类
    public abstract class  View {
        public abstract void  draw() ;
        public void  measure(int width, int height){
            // 测量视图大小
        }
    }
    
    // 按钮类的具体实现
    public class Button extends View {
        public void draw(){
            // 绘制按钮
        }
    }
    // TextView的具体实现
    public class TextView extends View {
        public void draw(){
            // 绘制文本
        }
    }

    上述示例中,Window依赖于View,而View定义了一个视图抽象,measure是各个子类共享的方法,子类通过覆写View的draw方法实现具有各自特色的功能,在这里,这个功能就是绘制自身的内容。任何继承自View类的子类都可以设置给show方法,也就我们所说的里氏替换。通过里氏替换,就可以自定义各式各样、千变万化的View,然后传递给Window,Window负责组织View,并且将View显示到屏幕上。
    里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性,在OOP当中,继承的优缺点都相当明显。
    优点如下:

    • (1)代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性;
    • (2)子类与父类基本相似,但又与父类有所区别;
    • (3)提高代码的可扩展性。

    继承的缺点:

    • (1)继承是侵入性的,只要继承就必须拥有父类的所有属性和方法;
    • (2)可能造成子类代码冗余、灵活性降低,因为子类必须拥有父类的属性和方法。

    事物总是具有两面性,如何权衡利与弊都是需要根据具体场景来做出选择并加以处理。里氏替换原则指导我们构建扩展性更好的软件系统,我们还是接着上面的ImageLoader来做说明。
    上文的图1-2也很好地反应了里氏替换原则,即MemoryCache、DiskCache、DoubleCache都可以替换ImageCache的工作,并且能够保证行为的正确性。ImageCache建立了获取缓存图片、保存缓存图片的接口规范,MemoryCache等根据接口规范实现了相应的功能,用户只需要在使用时指定具体的缓存对象就可以动态地替换ImageLoader中的缓存策略。这就使得ImageLoader的缓存系统具有了无线的可能性,也就是保证了可扩展性。

    想象一个场景,当ImageLoader中的setImageCache(ImageCache cache)中的cache对象不能够被子类所替换,那么用户如何设置不同的缓存对象以及用户如何自定义自己的缓存实现,通过1.3节中的useDiskCache方法吗?显然不是的,里氏替换原则就为这类问题提供了指导原则,也就是建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的高扩展性、灵活性。开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换来达到对扩展开放,对修改关闭的效果。然而,这两个原则都同时强调了一个OOP的重要特性——抽象,因此,在开发过程中运用抽象是走向代码优化的重要一步。

    4、 让项目拥有变化的能力——依赖倒置原则

    依赖倒置原则英文全称是Dependence Inversion Principle,简称DIP。依赖反转原则指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。这个概念有点不好理解,这到底是什么意思呢?
    依赖倒置原则的几个关键点:

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

    在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是,可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。高层模块就是调用端,低层模块就是具体实现类。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。这又是一个将理论抽象化的实例,其实一句话就可以概括:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。面向接口编程是面向对象精髓之一,也就是上面两节强调的抽象。

    如果在类与类直接依赖于细节,那么它们之间就有直接的耦合,当具体实现需要变化时,意味着在这要同时修改依赖者的代码,并且限制了系统的可扩展性。我们看1.3节的图1-3中,ImageLoader直接依赖于MemoryCache,这个MemoryCache是一个具体实现,而不是一个抽象类或者接口。这导致了ImageLoader直接依赖了具体细节,当MemoryCache不能满足ImageLoader而需要被其他缓存实现替换时,此时就必须修改ImageLoader的代码,例如:

    public class ImageLoader {
        // 内存缓存 ( 直接依赖于细节 )
        MemoryCache mMemoryCache = new MemoryCache();
         // 加载图片到ImageView中
        public void displayImage(String url, ImageView imageView) {
           Bitmap bmp = mMemoryCache.get(url);
            if (bmp == null) {
                downloadImage(url, imageView);
            } else {
                imageView.setImageBitmap(bmp);
            }
        }
    
        public void setImageCache(MemoryCache cache) {
            mCache = cache ;
        }
        // 代码省略
    }

    随着产品的升级,用户发现MemoryCache已经不能满足需求,用户需要小民的ImageLoader可以将图片同时缓存到内存和SD卡中,或者可以让用户自定义实现缓存。此时,我们的MemoryCache这个类名不仅不能够表达内存缓存和SD卡缓存的意义,也不能够满足功能。另外,用户需要自定义缓存实现时还必须继承自MemoryCache,而用户的缓存实现可不一定与内存缓存有关,这在命名上的限制也让用户体验不好。重构的时候到了!小民的第一种方案是将MemoryCache修改为DoubleCache,然后在DoubleCache中实现具体的缓存功能。我们需要将ImageLoader修改如下:

    public class ImageLoader {
        // 双缓存 ( 直接依赖于细节 )
        DoubleCache mCache = new DoubleCache();
        // 加载图片到ImageView中
        public void displayImage(String url, ImageView imageView) {
           Bitmap bmp = mCache.get(url);
            if (bmp == null) {
              // 异步下载图片
                downloadImageAsync(url, imageView);
           } else {
                imageView.setImageBitmap(bmp);
           }
        }
    
        public void setImageCache(DoubleCache cache) {
             mCache = cache ;
        }
        // 代码省略
    }

    我们将MemoryCache修改成DoubleCache,然后修改了ImageLoader中缓存类的具体实现,轻轻松松就满足了用户需求。等等!这不还是依赖于具体的实现类(DoubleCache)吗?当用户的需求再次变化时,我们又要通过修改缓存实现类和ImageLoader代码来实现?修改原有代码不是违反了1.3节中的开闭原则吗?小民突然醒悟了过来,低下头思索着如何才能让缓存系统更灵活、拥抱变化……

    当然,这些都是在主管给出图1-2(1.3节)以及相应的代码之前,小民体验的煎熬过程。既然是这样,那显然主管给出的解决方案就能够让缓存系统更加灵活。一句话概括起来就是:依赖抽象,而不依赖具体实现。针对于图片缓存,主管建立的ImageCache抽象,该抽象中增加了get和put方法用以实现图片的存取。每种缓存实现都必须实现这个接口,并且实现自己的存取方法。当用户需要使用不同的缓存实现时,直接通过依赖注入即可,保证了系统的灵活性。我们再来简单回顾一下相关代码:

    ImageCache缓存抽象:

    public interface ImageCache {
        public Bitmap get(String url);
        public void put(String url, Bitmap bmp);
    }

    ImageLoader类:

    public class ImageLoader {
        // 图片缓存类,依赖于抽象,并且有一个默认的实现
        ImageCache mCache = new MemoryCache();
    
        // 加载图片
        public void displayImage(String url, ImageView imageView) {
           Bitmap bmp = mCache.get(url);
            if (bmp == null) {
            // 异步加载图片
                downloadImageAsync(url, imageView);
           } else {
                imageView.setImageBitmap(bmp);
           }
        }
    
        /**
         * 设置缓存策略,依赖于抽象
         */
        public void setImageCache(ImageCache cache) {
            mCache = cache;
        }
        // 代码省略
    }

    在这里,我们建立了ImageCache抽象,并且让ImageLoader依赖于抽象而不是具体细节。当需求发生变更时,小民只需要实现ImageCahce类或者继承其他已有的ImageCache子类完成相应的缓存功能,然后将具体的实现注入到ImageLoader即可实现缓存功能的替换,这就保证了缓存系统的高可扩展性,拥有了拥抱变化的能力,而这一切的基本指导原则就是我们的依赖倒置原则。从上述几节中我们发现,要想让我们的系统更为灵活,抽象似乎成了我们唯一的手段。

    5、系统有更高的灵活性——接口隔离原则

    接口隔离原则英文全称是InterfaceSegregation Principles,简称ISP。它的定义是:客户端不应该依赖它不需要的接口。另一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。

    接口隔离原则说白了就是,让客户端依赖的接口尽可能地小,这样说可能还是有点抽象,我们还是以一个示例来说明一下。在此之前我们来说一个场景,在Java 6以及之前的JDK版本,有一个非常讨厌的问题,那就是在使用了OutputStream或者其他可关闭的对象之后,我们必须保证它们最终被关闭了,我们的SD卡缓存类中就有这样的代码:

    // 将图片缓存到内存中
    public void put(String url, Bitmap bmp) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
              } catch (IOException e) {
                    e.printStackTrace();
              }
           } // end if
        } // end if finally
    }

    我们看到的这段代码可读性非常差,各种try…catch嵌套,都是些简单的代码,但是会严重影响代码的可读性,并且多层级的大括号很容易将代码写到错误的层级中。大家应该对这类代码也非常反感,那我们看看如何解决这类问题。
    我们可能知道Java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法,如图1-4所示。
    我们要讲的FileOutputStream类就实现了这个接口,我们从图1-4中可以看到,还有一百多个类实现了Closeable这个接口,这意味着,在关闭这一百多个类型的对象时,都需要写出像put方法中finally代码段那样的代码。这还了得!你能忍,反正小民是忍不了的!于是小民打算要发挥他的聪明才智解决这个问题,既然都是实现了Closeable接口,那只要我建一个方法统一来关闭这些对象不就可以了么?说干就干,于是小民写下来如下的工具类:

    ▲图1-4

    public final class CloseUtils {
    
        Private CloseUtils() { }
    
        /**
         * 关闭Closeable对象
         * @param closeable
         */
        public static void closeQuietly(Closeable closeable) {
            if (null != closeable) {
                try {
                    closeable.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
           }
        }
    }

    我们再看看把这段代码运用到上述的put方法中的效果如何:

    public void put(String url, Bitmap bmp) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
       } catch (FileNotFoundException e) {
            e.printStackTrace();
       } final ly {
            CloseUtils.closeQuietly(fileOutputStream);
       }
    }

    代码简洁了很多!而且这个closeQuietly方法可以运用到各类可关闭的对象中,保证了代码的重用性。CloseUtils的closeQuietly方法的基本原理就是依赖于Closeable抽象而不是具体实现(这不是1.4节中的依赖倒置原则么),并且建立在最小化依赖原则的基础,它只需要知道这个对象是可关闭,其他的一概不关心,也就是这里的接口隔离原则。

    试想一下,如果在只是需要关闭一个对象时,它却暴露出了其他的接口函数,比如OutputStream的write方法,这就使得更多的细节暴露在客户端代码面前,不仅没有很好地隐藏实现,还增加了接口的使用难度。而通过Closeable接口将可关闭的对象抽象起来,这样只需要客户端依赖于Closeable就可以对客户端隐藏其他的接口信息,客户端代码只需要知道这个对象可关闭(只可调用close方法)即可。小民ImageLoader中的ImageCache就是接口隔离原则的运用,ImageLoader只需要知道该缓存对象有存、取缓存图片的接口即可,其他的一概不管,这就使得缓存功能的具体实现对ImageLoader具体的隐藏。这就是用最小化接口隔离了实现类的细节,也促使我们将庞大的接口拆分到更细粒度的接口当中,这使得我们的系统具有更低的耦合性,更高的灵活性。

    Bob大叔(Robert C Martin)在21世纪早期将单一职责、开闭原则、里氏替换、接口隔离以及依赖倒置(也称为依赖反转)5个原则定义为SOLID原则,指代了面向对象编程的5个基本原则。当这些原则被一起应用时,它们使得一个软件系统更清晰、简单、最大程度地拥抱变化。SOLID被典型地应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发基本原则的重要组成部分。在经过第1.1~1.5节的学习之后,我们发现这几大原则最终就可以化为这几个关键词:抽象、单一职责、最小化。那么在实际开发过程中如何权衡、实践这些原则,是大家需要在实践中多思考与领悟,正所谓”学而不思则罔,思而不学则殆”,只有不断地学习、实践、思考,才能够在积累的过程有一个质的飞越。

    6、更好的可扩展性——迪米特原则

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

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

    光说不练很抽象呐,下面我们就以租房为例来讲讲迪米特原则。
    “北漂”的同学比较了解,在北京租房绝大多数都是通过中介找房。我们设定的情境为:我只要求房间的面积和租金,其他的一概不管,中介将符合我要求的房子提供给我就可以。下面我们看看这个示例:

    /**
     * 房间
     */
    public class Room {
        public float area;
        public float price;
    
        public Room(float  area, float  price) {
            this.area = area;
            this.price = price;
        }
    
        @Override
        public String toString() {
            return "Room [area=" + area + ", price=" + price + "]";
        }
    
    }
    
    /**
     * 中介
     */
    public class Mediator {
        List<Room> mRooms = new ArrayList<Room>();
    
        public Mediator() {
            for (inti = 0; i < 5; i++) {
                mRooms.add(new Room(14 + i, (14 + i) * 150));
           }
       }
    
        public List<Room>getAllRooms() {
            return mRooms;
       }
    }
    
    
    /**
     * 租户
     */
    public class Tenant {
        public float roomArea;
        public float roomPrice;
        public static final float diffPrice = 100.0001f;
        public static final float diffArea = 0.00001f;
    
        public void rentRoom(Mediator mediator) {
            List<Room>rooms = mediator.getAllRooms();
            for (Room room : rooms) {
                if (isSuitable(room)) {
                 System.out.println("租到房间啦! " + room);
                    break;
              }
           }
        }
    
        private boolean isSuitable(Room room) {
            return Math.abs(room.price - roomPrice) < diffPrice
                    &&Math.abs(room.area - roomArea) < diffArea;
       }
    }

    从上面的代码中可以看到,Tenant不仅依赖了Mediator类,还需要频繁地与Room类打交道。租户类的要求只是通过中介找到一间适合自己的房间罢了,如果把这些检测条件都放在Tenant类中,那么中介类的功能就被弱化,而且导致Tenant与Room的耦合较高,因为Tenant必须知道许多关于Room的细节。当Room变化时Tenant也必须跟着变化。Tenant又与Mediator耦合,就导致了纠缠不清的关系。这个时候就需要我们分清谁才是我们真正的“朋友”,在我们所设定的情况下,显然是Mediator(虽然现实生活中不是这样的)。上述代码的结构如图1-5所示。

    ▲图1-5

    既然是耦合太严重,那我们就只能解耦了,首先要明确地是,我们只和我们的朋友通信,这里就是指Mediator对象。必须将Room相关的操作从Tenant中移除,而这些操作案例应该属于Mediator,我们进行如下重构:

    /**
     * 中介
     */
    public class Mediator {
        List<Room> mRooms = new ArrayList<Room>();
    
        public Mediator() {
            for (inti = 0; i < 5; i++) {
                mRooms.add(new Room(14 + i, (14 + i) * 150));
           }
        }
    
        public Room rentOut(float  area, float  price) {
            for (Room room : mRooms) {
                if (isSuitable(area, price, room)) {
                    return  room;
              }
           }
            return null;
        }
    
        private boolean isSuitable(float area, float price, Room room) {
            return Math.abs(room.price - price) < Tenant.diffPrice
                && Math.abs(room.area - area) < Tenant.diffPrice;
        }
    }
    
    /**
     * 租户
     */
    public class Tenant {
    
        public float roomArea;
        public float roomPrice;
        public static final float diffPrice = 100.0001f;
        public static final float diffArea = 0.00001f;
    
        public void rentRoom(Mediator mediator) {
            System.out.println("租到房啦 " + mediator.rentOut(roomArea, roomPrice));
         }
    }

    重构后的结构图如图1-6所示。

    ▲图1-6

    只是将对于Room的判定操作移到了Mediator类中,这本应该是Mediator的职责,他们根据租户设定的条件查找符合要求的房子,并且将结果交给租户就可以了。租户并不需要知道太多关于Room的细节,比如与房东签合同、房东的房产证是不是真的、房内的设施坏了之后我要找谁维修等,当我们通过我们的“朋友”中介租了房之后,所有的事情我们都通过与中介沟通就好了,房东、维修师傅等这些角色并不是我们直接的“朋友”。“只与直接的朋友通信”这简单的几个字就能够将我们从乱七八糟的关系网中抽离出来,使我们的耦合度更低、稳定性更好。
    通过上述示例以及小民的后续思考,迪米特原则这把利剑在小民的手中已经舞得风生水起。就拿sd卡缓存来说吧,ImageCache就是用户的直接朋友,而SD卡缓存内部却是使用了jake wharton的DiskLruCache实现,这个DiskLruCache就不属于用户的直接朋友了,因此,用户完全不需要知道它的存在,用户只需要与ImageCache对象打交道即可。例如将图片存到SD卡中的代码如下。

    public void put(String url, Bitmap value) {
        DiskLruCache.Editor editor = null;
        try {
           // 如果没有找到对应的缓存,则准备从网络上请求数据,并写入缓存
            editor = mDiskLruCache.edit(url);
            if (editor != null) {
                    OutputStream outputStream = editor.newOutputStream(0);
                if (writeBitmapToDisk(value, outputStream)) {
                  // 写入disk缓存
                    editor.commit();
              } else {
                    editor.abort();
              }
                CloseUtils.closeQuietly(outputStream);
           }
        } catch (IOException e) {
             e.printStackTrace();
        }
    }

    用户在使用SD卡缓存时,根本不知晓DiskLruCache的实现,这就很好地对用户隐藏了具体实现。当小民已经“牛”到可以自己完成SD卡的rul实现时,他就可以随心所欲的替换掉jake wharton的DiskLruCache。小民的代码大体如下:

    @Override
    public  void put(String url, Bitmap bmp) {
        // 将Bitmap写入文件中
        FileOutputStream fos = null;
        try {
           // 构建图片的存储路径 ( 省略了对url取md5)
            fos = new FileOutputStream("sdcard/cache/" + imageUrl2MD5(url));
            bmp.compress(CompressFormat.JPEG, 100, fos);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if ( fos != null ) {
                try {
                    fos.close();
              } catch (IOException e) {
                    e.printStackTrace();
              }
           }
        } // end if finally
    }

    SD卡缓存的具体实现虽然被替换了,但用户根本不会感知到。因为用户根本不知道DiskLruCache的存在,他们没有与DiskLruCache进行通信,他们只认识直接“朋友”ImageCache,ImageCache将一切细节隐藏在了直接“朋友”的外衣之下,使得系统具有更低的耦合性和更好的可扩展性。

    7、总结

    在应用开发过程中,最难的不是完成应用的开发工作,而是在后续的升级、维护过程中让应用系统能够拥抱变化。拥抱变化也就意味着在满足需求且不破坏系统稳定性的前提下保持高可扩展性、高内聚、低耦合,在经历了各版本的变更之后依然保持清晰、灵活、稳定的系统架构。当然,这是一个比较理想的情况,但我们必须要朝着这个方向去努力,那么遵循面向对象六大原则就是我们走向灵活软件之路所迈出的第一步。

    展开全文
  • 面向对象设计原则的总结

    千次阅读 2006-10-31 15:46:00
    正如牛顿三大定律在经典力学中的...其他设计原则(里氏代换原则、依赖倒转原则、合成/聚合复用原则、迪米特法则、接口隔离原则)是实现“开-闭”原则的手段和工具。 一、“开-闭”原则(Open-Closed Principle,OCP)
     
    正如牛顿三大定律在经典力学中的位置一样,“开-闭”原则(Open-Closed Principle)是面向对象的可复用设计(Object Oriented Design或OOD)的基石。其他设计原则(里氏代换原则、依赖倒转原则、合成/聚合复用原则、迪米特法则、接口隔离原则)是实现“开-闭”原则的手段和工具。
     
    一、“开- 闭”原则(Open-Closed Principle,OCP
           1.1 “开- 闭”原则的定义及优点
    1)定义:一个软件实体应当对扩展开放,对修改关闭( Software entities should be open for extension,but closed for modification.)。即在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。
    2)满足“开-闭”原则的系统的优点
    a)通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性。
    b)已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性。
    c)这样的系统同时满足了可复用性与可维护性。
    1.2 如何实现“开- 闭”原则
    在面向对象设计中,不允许更改的是系统的抽象层,而允许扩展的是系统的实现层。换言之,定义一个一劳永逸的抽象设计层,允许尽可能多的行为在实现层被实现。
    解决问题关键在于抽象化,抽象化是面向对象设计的第一个核心本质。
    对一个事物抽象化,实质上是在概括归纳总结它的本质。抽象让我们抓住最最重要的东西,从更高一层去思考。这降低了思考的复杂度,我们不用同时考虑那么多的东西。换言之,我们封装了事物的本质,看不到任何细节。
    在面向对象编程中,通过抽象类及接口,规定了具体类的特征作为抽象层,相对稳定,不需更改,从而满足“对修改关闭”;而从抽象类导出的具体类可以改变系统的行为,从而满足“对扩展开放”。
    对实体进行扩展时,不必改动软件的源代码或者二进制代码。关键在于抽象。
    1.3 对可变性的封装原则
    “开-闭”原则也就是“对可变性的封装原则”(Principle of Encapsulation of Variation ,EVP)。即找到一个系统的可变因素,将之封装起来。换言之,在你的设计中什么可能会发生变化,应使之成为抽象层而封装,而不是什么会导致设计改变才封装。
           “对可变性的封装原则”意味着:
    a)一种可变性不应当散落在代码的许多角落,而应当被封装到一个对象里面。同一可变性的不同表象意味着同一个继承等级结构中的具体子类。因此,此处可以期待继承关系的出现。继承是封装变化的方法,而不仅仅是从一般的对象生成特殊的对象。
    b)一种可变性不应当与另一种可变性混合在一起。作者认为类图的继承结构如果超过两层,很可能意味着两种不同的可变性混合在了一起。
    使用“可变性封装原则”来进行设计可以使系统遵守“开-闭”原则。
    即使无法百分之百的做到“开-闭”原则,但朝这个方向努力,可以显著改善一个系统的结构。
    二、里氏代换原则(Liskov Substitution Principle, LSP
     
    2.1 概念
    定义: 如果对每一个类型为 T1 的对象 O1 ,都有类型为 T2 的对象 O2 ,使得以 T1 定义的所有程序 P 在所有的对象 O1 都代换为 O2 时,程序 P 的行为没有变化,那么类型 T2 是类型 T1 的子类型。
    即,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类。而且它觉察不出基类对象和子类对象的区别。也就是说,在软件里面,把基类都替换成它的子类,程序的行为没有变化。
    反过来的代换不成立,如果一个软件实体使用的是一个子类的话,那么它不一定适用于基类。
    任何基类可以出现的地方,子类一定可以出现。
    基于契约的设计、抽象出公共部分作为抽象基类的设计。
    2.2 里氏代换原则与“开- 闭”原则的关系
        实现“开-闭”原则的关键步骤是抽象化。基类与子类之间的继承关系就是抽象化的体现。因此里氏代换原则是对实现抽象化的具体步骤的规范。
        违反里氏代换原则意味着违反了“开-闭”原则,反之未必。
    三、 依赖倒转原则 dependence inversion principle, DIP
           3.1 概念
       依赖倒转原则就是要依赖于抽象,不要依赖于实现。(Abstractions should not depend upon details. Details should depend upon abstractions.)要针对接口编程,不要针对实现编程。(Program to an interface, not an implementation.)
    也就是说应当使用接口和抽象类进行变量类型声明、参数类型声明、方法返还类型说明,以及数据类型的转换等。而不要用具体类进行变量的类型声明、参数类型声明、方法返还类型说明,以及数据类型的转换等。要保证做到这一点,一个具体类应当只实现接口和抽象类中声明过的方法,而不要给出多余的方法。
    传统的过程性系统的设计办法倾向于使高层次的模块依赖于低层次的模块,抽象层次依赖于具体层次。倒转原则就是把这个错误的依赖关系倒转过来。
    面向对象设计的重要原则是创建抽象化,并且从抽象化导出具体化,具体化给出不同的实现。继承关系就是一种从抽象化到具体化的导出。
    抽象层包含的应该是应用系统的商务逻辑和宏观的、对整个系统来说重要的战略性决定,是必然性的体现。具体层次含有的是一些次要的与实现有关的算法和逻辑,以及战术性的决定,带有相当大的偶然性选择。具体层次的代码是经常变动的,不能避免出现错误。
    从复用的角度来说,高层次的模块是应当复用的,而且是复用的重点,因为它含有一个应用系统最重要的宏观商务逻辑,是较为稳定的。而在传统的过程性设计中,复用则侧重于具体层次模块的复用。
    依赖倒转原则则是对传统的过程性设计方法的“倒转”,是高层次模块复用及其可维护性的有效规范。
    特例:对象的创建过程是违背“开—闭”原则以及依赖倒转原则的,但通过工厂模式,能很好地解决对象创建过程中的依赖倒转问题。
    3.2 关系
    “开 -闭”原则与依赖倒转原则是目标和手段的关系。如果说开闭原则是目标,依赖倒转原则是到达"开闭"原则的手段。如果要达到最好的"开闭"原则,就要尽量的遵守依赖倒转原则,依赖倒转原则是对"抽象化"的最好规范。
    里氏代换原则是依赖倒转原则的基础,依赖倒转原则是里氏代换原则的重要补充。
    3.3 耦合(或者依赖)关系的种类:
    零耦合(Nil Coupling)关系:两个类没有耦合关系
    具体耦合(Concrete Coupling)关系:发生在两个具体的(可实例化的)类之间,经由一个类对另一个具体类的直接引用造成。
    抽象耦合(Abstract Coupling)关系:发生在一个具体类和一个抽象类(或接口)之间,使两个必须发生关系的类之间存有最大的灵活性。
    3.3.1如何把握耦合
    我们应该尽可能的避免实现继承,原因如下:
    1 失去灵活性,使用具体类会给底层的修改带来麻烦。
    2 耦合问题,耦合是指两个实体相互依赖于对方的一个量度。程序员每天都在(有意识地或者无意识地)做出影响耦合的决定:类耦合、API耦合、应用程序耦合等等。在一个用扩展的继承实现系统中,派生类是非常紧密的与基类耦合,而且这种紧密的连接可能是被不期望的。如B extends A ,当B不全用A中的所有methods时,这时候,B调用的方法可能会产生错误!
    我们必须客观的评价耦合度,系统之间不可能总是松耦合的,那样肯定什么也做不了。
    3.3.2我们决定耦合的程度的依据何在呢
    简单的说,就是根据需求的稳定性,来决定耦合的程度。对于稳定性高的需求,不容易发生变化的需求,我们完全可以把各类设计成紧耦合的(我们虽然讨论类之间的耦合度,但其实功能块、模块、包之间的耦合度也是一样的),因为这样可以提高效率,而且我们还可以使用一些更好的技术来提高效率或简化代码,例如c# 中的内部类技术。可是,如果需求极有可能变化,我们就需要充分的考虑类之间的耦合问题,我们可以想出各种各样的办法来降低耦合程度,但是归纳起来,不外乎增加抽象的层次来隔离不同的类,这个抽象层次可以是抽象的类、具体的类,也可以是接口,或是一组的类。我们可以用一句话来概括降低耦合度的思想:"针对接口编程,而不是针对实现编程。
    在我们进行编码的时候,都会留下我们的指纹,如public的多少,代码的格式等等。我们可以耦合度量评估重新构建代码的风险。因为重新构建实际上是维护编码的一种形式,维护中遇到的那些麻烦事在重新构建时同样会遇到。我们知道在重新构建之后,最常见的随机bug大部分都是不当耦合造成的 。
    如果不稳定因素越大,它的耦合度也就越大。
    某类的不稳定因素=依赖的类个数/被依赖的类个数
    依赖的类个数= 在编译此类的时被编译的其它类的个数总和
    3.3.3怎样将大系统拆分成小系统
    解决这个问题的一个思路是将许多类集合成一个更高层次的单位,形成一个高内聚、低耦合的类的集合,这是我们设计过程中应该着重考虑的问题!
    耦合的目标是维护依赖的单向性,有时我们也会需要使用坏的耦合。在这种情况下,应当小心记录下原因,以帮助日后该代码的用户了解使用耦合真正的原因。
    3.4 怎样做到依赖倒转?
    以抽象方式耦合是依赖倒转原则的关键。抽象耦合关系总要涉及具体类从抽象类继承,并且需要保证在任何引用到基类的地方都可以改换成其子类,因此,里氏代换原则是依赖倒转原则的基础。
    在抽象层次上的耦合虽然有灵活性,但也带来了额外的复杂性,如果一个具体类发生变化的可能性非常小,那么抽象耦合能发挥的好处便十分有限,这时可以用具体耦合反而会更好。
    层次化:所有结构良好的面向对象构架都具有清晰的层次定义,每个层次通过一个定义良好的、受控的接口向外提供一组内聚的服务。
    依赖于抽象:建议不依赖于具体类,即程序中所有的依赖关系都应该终止于抽象类或者接口。尽量做到:
    1、任何变量都不应该持有一个指向具体类的指针或者引用。
    2、任何类都不应该从具体类派生。
    3、任何方法都不应该覆写它的任何基类中的已经实现的方法。
    3.5 依赖倒转原则的优缺点
    依赖倒转原则虽然很强大,但却最不容易实现。因为依赖倒转的缘故,对象的创建很可能要使用对象工厂,以避免对具体类的直接引用,此原则的使用可能还会导致产生大量的类,对不熟悉面向对象技术的工程师来说,维护这样的系统需要较好地理解面向对象设计。
    依赖倒转原则假定所有的具体类都是会变化的,这也不总是正确。有一些具体类可能是相当稳定,不会变化的,使用这个具体类实例的应用完全可以依赖于这个具体类型,而不必为此创建一个抽象类型。
    四、合成/ 聚合复用原则( Composite/Aggregate Reuse Principle CARP
           4.1 概念
    定义:在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用这些对象的目的。
    应首先使用合成/聚合,合成/聚合则使系统灵活,其次才考虑继承,达到复用的目的。而使用继承时,要严格遵循里氏代换原则。有效地使用继承会有助于对问题的理解,降低复杂度,而滥用继承会增加系统构建、维护时的难度及系统的复杂度。
    如果两个类是“Has-a”关系应使用合成、聚合,如果是“Is-a”关系可使用继承。"Is-A"是严格的分类学意义上定义,意思是一个类是另一个类的"一种"。而"Has-A"则不同,它表示某一个角色具有某一项责任。
    4.2 什么是合成?什么是聚合?
    合成(Composition)和聚合(Aggregation)都是关联(Association)的特殊种类。
    聚合表示整体和部分的关系,表示“拥有”。如奔驰S360汽车,对奔驰S360引擎、奔驰S360轮胎的关系是聚合关系,离开了奔驰S360汽车,引擎、轮胎就失去了存在的意义。在设计中, 聚合不应该频繁出现,这样会增大设计的耦合度。
    合成则是一种更强的“拥有”,部分和整体的生命周期一样。合成的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个合成关系的成分对象是不能与另一个合成关系共享的。
        换句话说,合成是值的聚合(Aggregation by Value),而一般说的聚合是引用的聚合(Aggregation by Reference)。
    明白了合成和聚合关系,再来理解合成/聚合原则应该就清楚了,要避免在系统设计中出现,一个类的继承层次超过3层,则需考虑重构代码,或者重新设计结构。当然最好的办法就是考虑使用合成/聚合原则。
    4.3 通过合成/ 聚合的优缺点
    优点:
    1) 新对象存取成分对象的唯一方法是通过成分对象的接口。
    2) 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
    3) 这种复用支持包装。
    4) 这种复用所需的依赖较少。
    5) 每一个新的类可以将焦点集中在一个任务上。
    6) 这种复用可以在运行时间内动态进行,新对象可以动态的引用与成分对象类型相同的对象。
    7) 作为复用手段可以应用到几乎任何环境中去。
    缺点:就是系统中会有较多的对象需要管理。
    4.4 通过继承来进行复用的优缺点
    优点:
    新的实现较为容易,因为超类的大部分功能可以通过继承的关系自动进入子类。
    修改和扩展继承而来的实现较为容易。
        缺点
    继承复用破坏包装,因为继承将超类的实现细节暴露给子类。由于超类的内部细节常常是对于子类透明的,所以这种复用是透明的复用,又称“白箱”复用。
    如果超类发生改变,那么子类的实现也不得不发生改变。
    从超类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性。
    继承只能在有限的环境中使用。
    五、 迪米特法则( Law of Demeter LoD
           5.1 概述
        定义:一个软件实体应当尽可能少的与其他实体发生相互作用。
        这样,当一个模块修改时,就会尽量少的影响其他的模块。扩展会相对容易。
        这是对软件实体之间通信的限制。它要求限制软件实体之间通信的宽度和深度。
    5.2 迪米特法则的其他表述:
    1)只与你直接的朋友们通信。
    2)不要跟“陌生人”说话。
    3)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
    5.3 狭义的迪米特法则
    如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
    朋友圈的确定
    “朋友”条件:
    1)当前对象本身(this)
    2)以参量形式传入到当前对象方法中的对象
    3)当前对象的实例变量直接引用的对象
    4)当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
    5)当前对象所创建的对象
    任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。
    缺点:会在系统里造出大量的小方法,散落在系统的各个角落。
    与依赖倒转原则互补使用
    5.4 狭义的迪米特法则的缺点:
    在系统里造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的商务逻辑无关。
    遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。
    5.5 迪米特法则与设计模式
    门面(外观)模式和调停者(中介者)模式实际上就是迪米特法则的具体应用。
    5.6 广义的迪米特法则
    迪米特法则的主要用意是控制信息的过载。在将迪米特法则运用到系统设计中时,要注意下面的几点:
    1)在类的划分上,应当创建有弱耦合的类。
    2)在类的结构设计上,每一个类都应当尽量降低成员的访问权限。
    3)在类的设计上,只要有可能,一个类应当设计成不变类。
    4)在对其他类的引用上,一个对象对其对象的引用应当降到最低。
    5.7 广义迪米特法则在类的设计上的体现
    1)优先考虑将一个类设置成不变类
    2)尽量降低一个类的访问权限
    3)谨慎使用Serializable
    4)尽量降低成员的访问权限
    5)取代C Struct
    迪米特法则又叫作最少知识原则(Least Knowledge Principle或简写为LKP),就是说一个对象应当对其他对象有尽可能少的了解。
    5.8 如何实现迪米特法则
    迪米特法则的主要用意是控制信息的过载,在将其运用到系统设计中应注意以下几点:
    1) 在类的划分上,应当创建有弱耦合的类。类之间的耦合越弱,就越有利于复用。
    2) 在类的结构设计上,每一个类都应当尽量降低成员的访问权限。一个类不应当public自己的属性,而应当提供取值和赋值的方法让外界间接访问自己的属性。
    3) 在类的设计上,只要有可能,一个类应当设计成不变类。
    4) 在对其它对象的引用上,一个类对其它对象的引用应该降到最低。
    六、 接口隔离原则( interface separate principle, ISP
          6.1 概念
        接口隔离原则:使用多个专门的接口比使用单一的总接口要好。也就是说,一个类对另外一个类的依赖性应当是建立在最小的接口上。      
    这里的"接口"往往有两种不同的含义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言具体的"接口"定义,有严格的定义和结构。比如c# 语言里面的Interface结构。对于这两种不同的含义,ISP的表达方式以及含义都有所不同。(上面说的一个类型,可以理解成一个类,我们定义了一个类,也就是定义了一种新的类型)      
    当我们把"接口"理解成一个类所提供的所有方法的特征集合的时候,这就是一种逻辑上的概念。接口的划分就直接带来类型的划分。这里,我们可以把接口理解成角色,一个接口就只是代表一个角色,每个角色都有它特定的一个接口,这里的这个原则可以叫做"角色隔离原则"。
    如果把"接口"理解成狭义的特定语言的接口,那么ISP表达的意思是说,对不同的客户端,同一个角色提供宽窄不同的接口,也就是定制服务,个性化服务。就是仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来。
        应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。
      这也是对软件实体之间通信的限制。但它限制的只是通信的宽度,就是说通信要尽可能的窄。
    遵循迪米特法则和接口隔离原则,会使一个软件系统功能扩展时,修改的压力不会传到别的对象那里。
    6.2 如何实现接口隔离原则
    不应该强迫用户依赖于他们不用的方法。
    1、利用委托分离接口。
    2、利用多继承分离接口。
    展开全文
  • 分布式事物介绍

    千次阅读 2019-02-19 14:30:30
    分布式事物本地事务概念分布式理论CAP定理分布式事务概念分布式事务解决方法1.两阶段提交(2PC)XA规范下的2pc事务两阶段提交原理图2PC事务解决方案的利弊2.补偿事务(TCC)base理论/柔性事务 ----- 为了可用性,...
  • 设计模式之SOLID原则再回首

    千次阅读 2014-11-29 20:42:23
    这是一篇关于回顾设计模式SOLID五大原则的文章,我非常喜欢文章中的例子,每个例子都是我精选了描述模式的,通过Modom讲述了单一职责原则、通过加减法计算器讲述了开闭原则、通过企鹅动物讲述了里氏替换原则、通过...
  • SOLID原则

    千次阅读 2014-08-07 15:29:19
    SOLID原则是SRP单一职责原则、OCP开闭原则、LSP里氏代换原则 、ISP接口隔离原则 、DIP依赖倒置原则 四个原则的第一个字母简称,如今还多了一个依赖注入DI。本文介绍了SOLID原则和现在新的流行OO设计思想。 设计...
  • 设计模式七大原则

    千次阅读 热门讨论 2018-05-29 11:24:33
    设计模式七大原则
  • 公共技术点之面向对象六大原则

    千次阅读 多人点赞 2015-02-24 23:14:21
    当然最大的问题就是职责的定义,什么是类的职责,以及怎么划分类的职责。 试想一下,如果你遵守了这个原则,那么你的类就会划分得很细,每个类都有自己的职责。恩,这不就是高内聚、低耦合么! 当然,如何界定...
  • 代码原则

    千次阅读 2015-12-01 20:55:58
    1、优化代码的第一步——单一职责原则 单一职责原则的英文名称是Single Responsibility Principle,简称SRP。它的定义是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该是一组相关性很高的...
  • 朴素贝叶斯分类算法原理

    千次阅读 2018-12-14 09:23:02
    如果一个事物在一些属性条件发生的情况下,事物属于A的概率大于属于B的概率,则判定事物属于A。 朴素贝叶斯分类器  朴素贝叶斯分类器的表示形式:  当特征为为x时,计算所有类别的条件概率,...
  • 本人一直崇尚一个原则,“我思,故我在!”。一直以来忙于编码,但是不甚如人意(写了较多重复性、耦合性太强的代码)。行有行规,面向对象编程的Java语言也不例外,遵循其相关原则,才能高效快速的编写高性能的代码...
  • 深度学习之美——算法的分类

    千次阅读 2018-08-25 18:51:35
    这里的标签,即使就是某个事物分类。 根据已知数据集做训练,未知数据集合做分类 所谓监督学习,就是先用训练数据集合学习得到一个模型,然后再使用这个模型新样本进行预测。 监督学习的基本流程 监督.....
  • 设计模式之6大设计原则

    千次阅读 2017-05-04 20:03:46
    介绍设计模式的六大原则
  • “陶行知”即知行统一原则,“正面”和“守纪律”即正面教育与纪律约束相结合的原则,“疏”即疏导原则,“导”即导向原则,“一连”即教育影响的一致性与连贯性原则,“积极”即依靠积极因素、克服消极因素的原则,...
  • 互联网分类与标签

    千次阅读 2018-06-20 09:40:27
    分类分类是一个将思想或事物进行识别、差异化和理性化的过程。也通常是出于某一目的,进行分门别类(分组)。最初的博客页面自带了分类功能;文章和类别是一一对应的。后来对分类进行了延伸,允许子类的存在,允许一...
  • 界面设计原则

    千次阅读 2010-04-18 16:52:00
    1.设计原则 (1)用户原则。人机界面设计首先要确立用户类型。划分类型可以从不同的角度,视实际情况而定。确定类型后要针对其特点预测他们不同界面的反应。这就要从多方面设计分析。(2)信息最小量原则。人机...
  • 两种定义如出一撤,只是一个事物的两种不同描述。 我们可以把这两个定义概括为一句话:建立单一接口,不要建立臃肿庞大的接口。再通俗的一点讲:接口尽量细化,同时接口中的方法尽量的少。看到这儿大家可能会...
  • 接口与设计原则

    千次阅读 2016-07-28 20:22:17
    11种设计原则原则  1.单一职责原则 - Single Responsibility Principle(SRP) 就一个类而言,应该仅有一个引起它变化的原因。 职责即为“变化的原因”。 2.开放-封闭原则 - Open Close Principle(OCP) ...
  • web交互设计原则

    千次阅读 2017-03-05 10:14:20
    别让我思考这是最重要的原则,如果你只能记住一条可用性原则,那么就记住这条。它意味着,设计者应该尽量做到,当我看一个页面时,它应该是不言而喻,一目了然,自我解释的。 网页上的每项内容都有可能迫使我们停...
  • 1.分类(classification... 分类方法是一种离散型随机变量建模或预测的监督学习算法。使用案例包括邮件过滤、金融欺诈和预测雇员异动等输出为类别的任务。 许多回归算法都有与其相对应的分类算法,分类算法通常...
  • 内容的分类和标签

    千次阅读 2015-05-04 17:45:00
    分类是一个将思想或事物进行识别、差异化和理性化的过程。也通常是出于某一目的,进行分门别类(分组)。 最初的博客页面自带了分类功能;文章和类别是一一对应的。后来对分类进行了延伸,允许子类的存在,允许一篇...
  • 前端 UI 设计原则(基于Ant-design)

    千次阅读 2019-09-17 15:53:41
    前端 UI 设计原则(基于Ant-design) 本文摘录自:AntDesign官网 1. 设计原则 1.1 亲密性 纵向关系:三种间隔 横向关系:栅格布局 1.2 对齐 Law of Continuity 文案对齐:松散时确定统一的视觉起点 表单类对齐:...
  • E-R图:实体与属性的划分原则

    千次阅读 2020-12-20 18:08:43
    事实上,在现实世界中具体的应用环境常常实体和属性已经作了自然的大体划分。 在数据字典中,数据结构、数据流和数据存储都是若干属性有意义的聚合,这就已经体现了这种划分。可以先从这些内容出发定义E-R图,然后...
  • OOD设计基本原则

    千次阅读 2011-03-25 20:36:00
    <br />OOD设计基本原则   Ø  OCP原则 Ø  里氏替换原则 Ø  依赖倒置原则 Ø  接口隔离原则 Ø  聚合与继承原则 Ø  单一职责原则 Ø  Separation ...
  • 数据分类与聚类的区别

    千次阅读 2018-03-13 19:07:45
    一、分类 利用分类技术可以从数据集中提取描述数据类的一个函数或模型(也常称...即数据挖掘的目标就是根据样本数据形成的类的知识并源数据进行分类,进而也可以预测未来数据的归类。 分类技术具有广泛的应用,...
  • Java三大特性、五大原则

    千次阅读 2019-08-21 23:26:55
    也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。 简单的说,一个类就是一个封装了数据...
  • 用户界面设计原则

    千次阅读 2008-12-22 14:46:00
    (一)软件产品界面设计原则1.设计原则 (1)用户原则。人机界面设计首先要确立用户类型。划分类型可以从不同的角度,视实际情况而定。确定类型后要针对其特点预测他们不同界面的反应。这就要从多方面设计分析。...
  • 二八原则将人分为了三六九等

    千次阅读 2019-06-25 14:07:20
    适用于经济环境的各个领域,这个世界上很多事物都遵循着二八原则,例如: 空气中氮气与氧气的比例 人体占比中的水分与其他 20%的人靠脖子上赚钱80%的人靠脖子下赚钱 20%的人想的是...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 26,649
精华内容 10,659
关键字:

对事物进行分类的原则