登录  
 加关注
查看详情
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

秒大刀 博客

好好学习 天天向上

 
 
 

日志

 
 
 
 

读《重构与模式》  

2009-09-23 22:11:19|  分类: 读书 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

读《重构与模式》 - 秒大刀 - 秒大刀的城堡

    算《重构-改善既有代码的设计》一书的姊妹篇吧,主要讲的是设计模式相关的重构。模式不是设计出来的,是重构出来的。好的设计也不是设计出来的,是重构出来的。不要怕改变,只要改变得法,变就不再是灾难,而是进步的良机。

    重构吧!


译者序
  设计模式代表了传统的软件开发思想:好的设计会产生好的软件,因此在实际开发之前,值得花时间去做一个全面而细致的设计。重构代表了敏捷软件开发的浪潮:软件并不是在一开始就可以设计得完美无缺的,因此可以先进行实际开发,然后通过对代码不断的进行小幅度的修改来改善其设计。二者从不同角度阐述了设计的重要性。
  重构是实现设计模式的一种手段,设计模式往往也是重构的目的。

Martin Fowler序
  重构其实就是循序渐进的进行较大的修改。

前言
  重构:改善既有代码设计的过程
  模式:针对反复出现的问题的经典解决方案
  建议使用模式来改善既有的设计,要优于在新的设计早期使用模式。建议通过一系列低层次的设计转换,也就是重构,来应用模式,改进设计。

第1章 本书的写作缘由
  应该通过重构实现模式、趋向模式和去除模式(refactoring to, towards, and away from pattern),而不是在预先设计中使用模式,也不再过早的在代码中加入模式。这技能避免过度设计,又不至于设计不足。

  过度设计:代码的灵活性和复杂性超出所需。
  项目中每人负责一块会使每个人都在自己的小天地工作而不关注别人是否已经完成了自己所需,最终导致大量重复代码。

  设计不足产生的原因:
    程序员没有时间,没有抽出时间,或者时间不允许进行重构
    程序员在何为好的软件设计方面知识不足
    程序员被要求在既有系统中快速的添加新功能
    程序员被迫同时进行太多项目
  长期的设计不足,会使软件开发节奏变成“快,慢,更慢”,可能的后果是:
    系统的1.0版本很快就交付了,但是代码质量很差
    系统的2.0版本也交付了,但质量低劣的代码使我们慢下来
    在企图交付未来版本时,随着劣质代码的倍增,开发速度也越来越慢,最后人们对系统、程序员乃至使大家陷入这种境地的整个过程都失去了信心
    到了4.0版本时或者之后,我们意识到这样肯定不行,开始考虑推倒重来
  测试驱动开发和持续重构提供了一种精益、迭代和训练有素的编程风格,能够最大程度的有张有弛,提高生产率。“迅速而又从容不迫”
  使用测试驱动开发和持续重构有助于:
    保持较低的缺陷数量
    大胆的进行重构
    得到更加简单、更加优秀的代码
    编程时没有压力
 
  模式和重构之间存在着天然联系,模式是你想达到的目的地,而重构则是从其他地方抵达这个目的地的条条道路。
 
第2章 重构
  重构:保持行为的转换。重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。
  重构过程:去除重复、简化复杂逻辑和澄清模糊的代码。
  重构最好持续而不是分阶段的进行,只要看到代码需要改善,就改善它。
  重构实践必须和谐的适应业务上的轻重缓急。
  重构的动机:
    使新代码的增加更容易
    改善既有代码的设计
    对代码理解更透彻
    提高编程的趣味性
 
  所有优秀的设计都是不断修改而成的,修改意味着重新审视
  任何傻瓜都会编写计算机能理解的代码,好的程序员能够编写人能够理解的代码。
  要保持代码清晰,必须持续的去除重复,简化和澄清代码。决不能容忍代码中的脏乱,决不能倒退到坏习惯中去。清晰的代码能产生更好的设计,而更好的设计将使开发过程更加迅速,从而使客户和程序员皆大欢喜。请保持代码的清晰吧!
  绿条就像一个陀螺仪,能够使我们不偏离航线,如果红条显示的时间太长——超过几分钟,我就知道自己采取的步骤不够小。这时应该返工,重新开始。采取更小、更安全的步骤比采取更大的步骤更能快速达到目标。
  使用重构的技术语言和大多数管理人员交流,效果都不好,经理很难批准你花一些时间进行所谓的重构或改善设计。应该采用更好的术语“设计欠账”包括“欠账的滞纳金”术语和管理层沟通会取得更佳的效果。
  进展太快对所有人都没有好处
  不能将框架组合应用组隔离开,应该成立一个组,并用实际的需求驱动框架的持续进化。这无论对应用还是对框架进化都是很有好处的。
  一般而言,设计模式的代码生成工具提供的是饮鸩止渴之道,很容易使代码过度设计。
 
第3章 模式
  在预先设计对赢得合同非常关键时,仍然很值得采用,其他情况下请审慎。
 
第4章 代码坏味
  常见的设计问题都出重复、不清晰、复杂的代码
  重复代码
    类层次中不同子类里存在明显或微妙的重→形成Template Method
    子类中的方法除了对象创建之外其他实现方法都类似→用Factory Method引入多态创建→使用Template Method去除更多重复
    构造函数包含重复代码→链构造函数
    单独的代码处理一个对象或者一组对象→Composite替换
    类层次多个子类都实现了自己的Composite,而且实现可能完全相同→提取Composite
    对象处理方法的区别仅在于接口不同→通过Adapter统一接口
    条件逻辑处理空对象,而且相同的空逻辑在整个系统中都是重复的→引入Null Object
  方法过长
    短方法更容易逻辑共享,更容易理解,更容易扩展维护
    方法应该在10行以内,大多数方法应该在1~5行。如果系统的大多数方法都比较小,可以有少数的方法稍大一些,只要它们容易理解并且不包含重复代码
    不用担心短方法造成的性能问题:优秀的设计人员都不会对代码进行不成熟的优化;性能剖析工具证明将许多小方法串起来性能开销微乎其微;即使剖桥遇到性能问题,可以通过重构来改进性能,无需放弃小方法原则
 
    方法过长→用组合方法重构将其分解为Composed Method→若正在转换的Composed Method要将某个信息累加到一个公共变量中?→将聚集操作搬移到Collecting Parameter
    大的switch分派→Command
    采用switch从接口不同的许多类收集数据→将聚集操作搬移到Vistor
    大方法包含了算法的许多版本和运行时用来选择版本的条件逻辑→Strategy
  条件逻辑过复杂
    条件逻辑控制的是一种计算的几个变形→Strategy
    条件逻辑控制执行类的核心行为之外某个特殊行为→将装饰功能搬移到Decorator
    控制对象状态转换→State
    处理空操作,并且系统中有重复相同的空条件逻辑→引入Null Object
  基本类型迷恋
    非类型安全的基本类型(客户可以赋予它不安全或不正确的值)控制类中的逻辑→用类替换基本类型,得到类型安全并且能够扩展新行为的代码
    控制对象状态转换使用了复杂条件逻辑的基本类型→State,用许多类表示每个状态和简化的状态转换逻辑
    控制算法运行的条件逻辑非常复杂→Strategy
    隐式创建了基本类型的树结构→Composite
    如果有许多方法支持多个基本类型值的组合,那么就可能存在隐式语言→Interpreter
    基本值类型只是为了提供类核心职责的装饰→Decorator
    过于原始,对客户代码的用处不大的Composite→用Builder封装Composite
  不恰当的暴露
    让客户看到不太重要或有间接重要性的代码会增加复杂度
    Factory可以很好的处理公开构造函数的暴露
  解决方案蔓延
    ——许多类中都有用来完成某些职责的代码或数据,通常由快速的功能堆砌造成
    对象创建职责的蔓延可以通过Factory解决
  异曲同工的类
    ——两个相似的类却有不同的接口
    若对代码有可控权→重构为共享一个公共接口
    若无代码控制权(如3Party代码)→Adapter
  冗赘类rǒngzhuì
    类如果功能有限,缺乏存在价值,就应该删除
    内联Singleton是快速而优雅的Singleton实现
  类过大
    如果类中存在过多的字段,往往说明类的职责太多
 
    switch或if条件调度→Command
    状态改变条件语句→State
    隐式语言(模仿某种语言的代码)→Interpreter
  分支语句
    switch或if条件调度→Command
    聚集操作→Visitor
  组合爆炸
    Interpreter
  怪异解决方案
    ——在同一系统中用不同的方式解决同一问题,称之为怪异或者不一致的解决方案
 
第5章 模式导向的重构目录
 
第6章 创建
6.1 用Creating Method替换构造函数
  当类中有多个构造函数,因此很难决定在开发期间用哪一个时,可以用能够说明意图的返回对象实例的Creation Method替换构造函数
 
动机:
  Creation Method——类中的一个静态或者非静态的负责实例化类的新实例方法。因Creating Method命名没有限制,所以可以取最能表达所创建对象的名字。
  类中有太多构造函数→提炼类或者提炼子类 或者 用Creation Method替换构造函数来澄清构造函数的意图
优缺点:
  + 比构造函数能够更好的表达所创建的实例种类
  + 避免了构造函数的局限,比如两个构造函数的参数数目和类型不能相同
  + 更容易发现无用的创建代码
  - 创建方式是非标准的,有的用new实例化,而有的用Creation Method实例化
变体:
  不需要为每个对象的配置都设立一个Creation Method,非必要情况下可以添加参数来减少Creation Method的数量
  当Creation Method过多的分散了类的主要职责是,应该考虑将相关的Creation Method重构为一个Factory
6.2 将创建知识搬移到Factory
  当用来实例化一个类的数据和代码在多个类中到处都是时,可以讲有关创建的知识搬移到一个Factory中
 
动机:
  创建蔓延——将创建的职责放在了不应该承担对象创建任务的类中,是解决方案蔓延中的一种,一般是之前的设计问题导致。
  使用一个Factory类封装创建逻辑和客户代码的实例化选项,客户可以告诉Factory实例如何实例化一个对象,然后用同一个Factory实例在运行时执行实例化。
  Factory不需要用具体类专门实现,可以使用一个接口定义Factory,然后让现有的类实现这个接口。
  如果Factory中创建逻辑过于复杂,应将其重构为Abstract Factory,客户代码可以配置系统使用某个ConcreteFactory(AbstractFactory的一个具体实现)或者默认的ConcreteFactory。
  只有确实改进了代码设计,或者无法直接进行实例化时才有足够的理由进行Factory重构
优缺点:
  + 合并创建逻辑和实例化选项
  + 将客户代码与创建逻辑解耦
  - 如果可以直接实例化,会使设计复杂化
6.3 用Factory封装类
  当直接实例化处在同一包结构中、实现统一接口的多个类。可以把类的构造函数声明为非公共的,并通过Factory来创建它们的实例
 
动机:
  可以通过Factory将一组客户并不需关心的子类屏蔽到包内部。
  如果类共享一个通用的公共接口、共享相同的超类、并且处在同一包结构中,该重构可能有用。
优缺点:
  + 通过意图导向的Creation Method简化了不同种类实例的创建
  + 通过隐藏不需要公开的类减少了包的“概念重量”
  + 帮助严格执行“面向接口编程,而不是面向实现”这一格言
  - 当需要创建新种类的实例时,必须更新Creation Method
  - 当客户只能获得Factory的二进制代码而无法获得源码时,对Factory的定制将受到限制
6.4 用Factory Method引入多态创建
  当一个层次中的类都相似的实现一个方法,只是对象创建的步骤不同时,可以创建调用Factory Method来处理实例化方法的唯一超类版本
 
动机:
  Factory Method是OOP中最常见的模式,因其提供了多台创建对象的方法
  使用Factory Method后的代码往往比在类中赋值方法来创建自定义对象要简单
  使用Factory Method的主要情况:
    当兄弟子类实现了除对象创建步骤外都很相似的方法时
    当超类和子类实现了除对象创建步骤外都很相似的方法时
优缺点:
  + 减少因创建自定义对象而产生的重复代码
  + 有效的表达了对象创建发生的位置,以及如何重写对象的创建
  + 强制Factory Method使用的类必须实现统一的类型
  - 可能会向Factory Method的一些实现者传递不必要的参数
6.5 用Builder封装Composite
  当构造Composite是重复的、复杂的且容易出错的工作时,通过使用Builder处理构造细节来简化构造过程。
 
动机:
  构造Composite是重复的、复杂的、容易出错的工作,通过使用Builder处理构造细节来简化构造过程
  Builder模式很擅长处理繁重的、复杂的构造步骤。
  Builder模式的意图:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
优缺点:
  + 简化了构造Composite的客户代码
  + 减少了创建Composite的重复和易出错的本性
  + 在客户代码和Composite之间实现了松耦合
  + 允许对已封装的Composite或复杂对象创建不同的表示
  - 接口可能不会很清楚的表达其意图
6.6 内联Singleton
  当代码需要访问一个对象,但是不需要对象的全局入口时,可以把Singleton的功能搬移到一个保存并提供对象访问入口的类中。删除Singleton。
 
动机:
  Singleton意图:确保一个类仅有一个实例,并提供一个访问它的全局访问点
  保持暴露对象和保护对象之间的平衡对维护系统的灵活性是至关重要的
  任何全局数据在被证明是无害之前都是有害的
  如果遇到本不该实现为Singleton的Singleton,不要犹豫,内联它!
优缺点:
  + 使对象的协作变得更明显和明确
  + 保护了单一的实例,且不需要特殊的代码
  - 当在许多层次间传递对象实例比较困难的时候,会使设计变得复杂
第7章 简化
  我们所编写的绝大部分代码都不会从一开始就很简单。
  算法经常会因为支持多种变化而变得复杂。
  控制状态转换的转换的逻辑往往会变得越来越复杂。
7.1 组合方法
  当你无法迅速的理解一个方法的逻辑时,把方法的逻辑转换成几个同一层面上的、能够说明意图的步骤。
 
动机:
  Composed Method由对其他方法的调用组成,好的Composed Method的代码都在细节的同一层面上。
  Composed Method一般不会引入性能问题
优缺点:
  + 清晰的描述了一个方法所实现的功能以及如何实现
  + 把方法分解成命名良好的、处在细节的同一层面上的行为模块,以此来简化方法
  - 可能会产生过多的小方法
  - 可能会使调试变得困难,因为程序的逻辑分散在许多小方法中
Composed Method指导原则:
  Composed Method都很小。一般在5行左右,很少超过10行
  删除重复代码和死代码。除去明显的和微妙的代码重复,除去没有被使用的代码,以减少方法的代码量
  表达意图。清楚的命名程序中的变量、方法和参数,使它们明确表达意图。
  简化。转换代码,使它尽可能简单。
  使用细节的统一层面。当把一个方法分解成一组行为时,要保证这些行为在细节的相似层面上。
7.2 用Strategy替换条件逻辑
  当方法中条件逻辑控制着应该执行计算的哪个变体时,为每个变体创建一个Strategy并使方法把计算委托到Strategy实例。
 
动机:
  ——为算法的各个变体生成一系列的类,并用Strategy的一个实例装配主类,主类在运行时委托到该Strategy实例
  复杂的条件逻辑是最常导致复杂度上升的地点之一
优缺点:
  + 通过减少或去除条件逻辑使算法变得清晰易懂
  + 通过把算法的变体搬移到类层次中简化了类
  + 允许在运行时用一种算法替换另一种算法
  - 当应用基于继承的解决方案或“简化条件表达式”中的重构更简单时,会增加设计的复杂度
  - 增加了算法如何获取或接收上下文类数据的复杂度
7.3 将装饰功能搬移到Decorator
  当代码向类和核心职责提供装饰功能时,可以考虑将装饰代码搬移到Decorator
 
  无论多么喜欢一个模式,不要在不必要的时候使用它
优缺点:
  + 把装饰功能从类中移除,从而简化类
  + 有效的把类的核心职责和装饰功能区分开来
  + 可以去除几个相关类中重复的装饰逻辑
  - 改变了被装饰对象的类型
  - 会使代码变得更难理解和调试
  - 当Decorator组合产生负面影响的时候,会增加设计的复杂度
7.4 用State替换状态改变条件语句
  当控制一个对象状态转换的条件表达式过于复杂时,可以考虑用处理特殊状态转换的State类替换条件语句
 
优缺点:
  + 减少或去除状态改变条件逻辑
  + 简化了复杂的状态改变逻辑
  + 提供了观察状态改变逻辑的很好的鸟瞰图
  - 当状态转换逻辑已经易于理解的时候,会增加设计的复杂度
7.5 用Composite替换隐含树
  当用原生表示法隐含的形成了树结构时,可以考虑用Composite替换这个原生表示法
 
优缺点:
  + 封装重复的指令,如格式化、添加或删除结点
  + 提供了处理相似逻辑增长的一般性方法
  + 简化了客户代码的构造职责
  - 当构造隐式树更简单的时候,会增加设计的复杂度
7.6 用Command替换条件调度程序
  当条件逻辑用来调度请求和执行操作时,为每个动作创建一个Command。把这些Command存储在一个集合中,并用获取及执行Command的代码替换条件逻辑。
 
  为每个动作创建一个Command,把这些Command存储在一个集合中,并用获取及执行Command的代码替换条件逻辑
优缺点:
  + 提供了用统一方法执行不同行为的简单机制
  + 允许在运行时改变所处理的请求,以及如何处理请求
  + 仅仅需要很少的代码实现
  - 当条件调度程序已经足够的时候,会增加设计的复杂度
第8章 泛化
  泛化是把特殊代码转换成通用目的代码的过程。泛化代码的产生往往的重构的结果。
8.1 形成Template Method
  当子类中的两个方法以相同的顺序执行相似的步骤,但是步骤并不完全相同。通过把这些步骤提取成具有相同签名的方法来泛化这两个方法,然后上移这些泛化方法,形成Template Method。
 
优缺点:
  + 通过把不变行为搬移到超类,去除子类中的重复代码
  + 简化并有效的表达了一个通用算法的步骤
  + 允许子类很容易的定制一个算法
  - 当为了生成算法,子类必须实现很多方法的时候,会增加设计的复杂度
8.2 提取Composite
  当一个类层次结构中的多个子类实现了同一个Composite时,可以提取一个实现该Composite的超类
 
优缺点:
  + 去除重复的类存储逻辑和类处理逻辑
  + 能够有效的表达类处理逻辑的可继承性
8.3 用Composite替换一/多之分
  当类使用不同的代码处理单一对象与多个对象时,用Composite能够产生既可以处理单一对象又可以处理多个对象的代码
 
优缺点:
  + 去除与处理一个或多个对象相关联的重复代码
  + 提供处理一个或多个对象的统一方法
  + 支持处理多个对象的更丰富的方法
  - 可能会在Composite的构造过程中要求类型安全的运行时检查
8.4 用Observer替换硬编码的通知
  当子类通过硬编码来通知另一个类的实例时可以去除这些子类,并使其超类能够通知一个或多个实现了Observer接口的类
 
优缺点:
  + 使主题及其观察者访问松散耦合
  + 支持一个或多个观察者
  - 当硬编码的通知已经足够的时候,会增加设计的复杂度
  - 当出现串联通知的时候,会增加代码的复杂度
  - 当观察者没有从它们的主题中被删除的时候,可能会造成资源泄漏
8.5 通过Adapter统一接口
  当客户代码与两个类交互,其中的一个类具有首选接口,可以用一个Adapter统一接口
 
动机:
  当下面条件都为真时,重构Adapter就是有用的:
    两个类所做的事情相同或相似,但是具有不同的接口
    如果类共享同一个接口,客户代码会更简单、更直接、更紧凑
    无法轻易改变其中一个类的接口,因为它是第三方库中的一部分,或者它是一个已经被其他客户代码广泛使用的框架的一部分,或者无法获得源码
优缺点:
  + 使客户代码可以通过相同的接口与不同的类交互,从而去除或减少重复代码
  + 使客户代码可以通过公共的接口与多个对象交互,从而简化了客户代码
  + 统一了客户代码与不同类的交互方式
  - 当类的接口可以改变的时候,会增加设计的复杂度
8.6 提取Adapter
  当一个类适配了多个版本的组件、类库、API或其他实体时,可以为组件、类库、API或其他实体的每个版本提取一个Adapter
 
  Adapter用来适配对象,Facade用来适配整个系统,Facade通常用来与遗留系统进行交互
优缺点:
  + 隔离了不同版本的组件、类库或API之间的不同之处
  + 使类只负责适配代码的一个版本
  + 避免频繁的修改代码
  - 如果某个重要行为在Adapter中不可用的话,那么客户代码将无法执行这一重要行为
8.7 用Interpreter替换隐式语言
  当类中的许多方法组合成了一种隐式语言的元素,可以为隐式语言的元素定义类,这样就可以通过类实例组合,形成易于理解的表达式
 
优缺点:
  + 比隐式语言更好的支持语言元素的组合
  + 不需要解析新的代码来支持语言元素的新组合
  + 允许行为的运行时配置
  - 会产生定义语言和修改客户代码的开销
  - 如果语言很复杂,则需要很多的编程工作
  - 如果语言本身就很简单,则会增加设计的复杂度
第9章 保护
9.1 用类替换类型代码
  字段的类型无法保护它免受不正确的复制和非法的等同性比较,可以把字段的类型声明为类,从而限制复制和等同性比较
 
优缺点:
  + 更好的避免非法赋值和比较
  - 比使用不安全类型要求更多的代码
9.2 用Singleton限制实例化
  代码创建了一个对象的多个实例,并导致内存使用过多和系统性能下降时,可以用Singleton替换多个实例
 
  不要做不成熟的代码优化,经过不成熟优化的代码比未优化的代码更难于重构。在代码优化之前,你会发现更多可以改进的地方
优缺点:
  + 改进性能
  - 在任何地方都可以很容易的访问。在很多情况下,这可能是设计的缺点
  - 当对象含有不能共享的状态时,本重构无效
9.3 引入Null Object
  当代码中到处都是处理null字段或变量的重复逻辑时,将null逻辑替换为一个Null Object,一个提供正确null行为的对象
 
优缺点:
  + 不需要重复的null逻辑就可以避免null错误
  + 通过最小化null测试简化了代码
  - 当系统不太需要null测试的时候,会增加设计的复杂度
  - 如果程序员不知道Null Object的存在,就会产生多余的null测试
  - 使维护变得复杂,拥有超类的Null Object必须重写所有新继承到的公共方法
第10章 聚集操作
10.1 将聚集操作搬移到Collecting Parameter
  有一个很大的方法将信息聚集到一个局部变量中时,可以把结果聚集到一个Collecting Parameter中,并将它传入被提炼出的方法中
 
优缺点:
  + 帮助我们把很大的方法转换成更小的,更简单的多个方法
  - 使结果代码运行得更快
10.2 将聚集操作搬移到Visitor
  有一个方法从不同的类中聚集信息,可以把聚集工作搬移到一个能够访问每个类以便采集信息的Visitor中。
 
优缺点:
  + 调节多个算法,使其适用于不同的对象结构
  + 访问相同或不同继承结构中的类
  + 调用不同类上的类型特定方法,无需类型转换
  - 当可以使用通用接口把互不相同的类变成相似类的时候,会增加代码的复杂度
  - 新的可访问类需要新的接收方法,每个Visitor中需要新的访问方法
  - 可能会破坏访问类的封装性
第11章 使用重构
11.1 链构造函数
  有很多包含重复代码的构造函数时,可以把构造函数链接起来,从而获得最少的代码重复。
11.2 统一接口
  当需要一个与其子类具有相同接口的超类或接口时,可以找到所有子类含有而超类没有的公共方法,把这些方法复制到超类中,并修改每个方法,使其执行空行为。
11.3 提取参数
  当一个方法或构造函数将一个字段赋值为一个局部实例化的值时,可以把赋值声明的右侧提取到一个参数中,并通过客户代码提供的参数对字段进行赋值。
  评论这张
 
阅读(4177)| 评论(0)

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018