程序架构师设计师必须掌握的面向对象的程序设计原则

程序架构师设计师必须掌握的面向对象的程序设计原则

参见:http://blog.sina.com.cn/s/blog_4012c6370100yfwg.html

 
  一个优秀的软件项目,除去优秀的界面设计和合理恰当的功能设计以外,其程序的架构设计也一定是优秀的,本文专门探讨面向对象的程序架构设计问题。这也是每一个想成为软件架构师的人必须面对和做好的。
 
  我们说一般优秀的程序结构具有如下一些特征:
    1、  高稳定性:包括代码逻辑清晰、严密,运行无错
    2、  高性能:运行效率高,相应时间短,支持高并发
    3、  高可移植性:支持多种不同的运行环境,避免使用特定版本的运行和开发环境提供的方法
    4、  高可阅读性:编码符合常用规范,代码结构清晰,条理清楚,备注明确,使阅读者能很快
    5、  高可扩展性:易于新功能、新模块的增加
    6、  高可复用性:避免重复编码
    7、  代码简洁:实现同样的功能,当然代码行越少越好
    8、  等等
 
  要使我们编出的代码具有上述的这些特征,我们除了必须养成良好的编码习惯外,还要总结和掌握一些基本的程序架构设计原则,只有架构在这些原则之上的代码才是具备优秀代码的特征。
下面将目前在程序架构设计中常见的一些原则进行分析总结。这些原则的实质其实就是如何分类组织我们编写的那么多行代码,就像一个好的书架的图书分类,让各种类目的书籍都能分类摆放,便于浏览和查找。
在说具体的原则之前,首先提一下每个程序员一定都要有一个中心思想:拒绝复制代码。要让这个思想深入自己的编程生涯,这样,你才会更好的理解下面这些原则。
 
  1. 可变代码分离原则
  在任何程序中,总有一些相对稳定而不需要经常修改的代码,同时也总有一些不稳定而需要经常修改的代码。通常,那些决定了程序结构的框架代码就具有较强的稳定性,而那些和具体业务相关的代码则稳定性较差。为了表达方便,人们常把后者称为可变性代码。所以为了提高程序的可维护性,程序设计的第一个原则就是尽可能把可变性代码分离出来单独编写,这就是所谓的可变性代码隔离原则。
在我们现在开发环境中,很多框架如Hibernate实际上就已经把一些相对稳定的代码进行封装隔离了。
 
  2."开-闭"原则(拥抱扩展拒绝修改)
  围绕着可变性代码隔离原则的实现,以及使软件产品具有良好的可维护性和可复用性,人们进行了大量研究,其成果之一就是又提出了一个"开-闭"原则。其中的"开"指的是软件模块要对功能扩展开放,而"闭"则指的是软件模块要对修改进行关闭。
  显然,"开-闭"原则意味着程序中的各个代码模块应具有边界,也就是说得先有"闭"。长期以来人们为了使代码具有边界而煞费脑筋,从而也就出现了函数、类、对象等概念。即在设计一个程序时,根据业务逻辑的紧密程度把代码封装成一个个具有某种形式的功能模块,并设法使这些模块只能通过有限的公有通道来通信,从而实现"高内聚,低耦合",即实现了有效的"闭"。
  那么如何不用修改模块代码而使模块可以被扩展(即实现有效的"开")呢?人们想到了日常生活中使用的电器插口和插头,它们各自的背后都连接着相应的电器,这些插口和插头的作用就是把不同的设备连接起来以扩展它们的功能,而不需要对各个电器进行任何改动。当然,这需要一个前提:插口和插头必须符合某种稳定(不变)的标准或合约。如果把程序模块之间的公有通信通道看做插口和插头,那么就意味着这些公有通信通道必须符合某种稳定的标准或合约,这样就可以实现程序模块的"开"。
简言之,符合"开-闭"原则的程序模块边界应该是一种遵守了某种稳定合约的语言元素,它对内可以把可变性代码封装起来,对外则提供了标准接口。
按上述的原则,所谓“拥抱扩展”,即指作为程序员不要对软件功能的改变或者增加有任何抵制情绪,相反,我们欢迎对软件任何有用的完善,那么实际做法就是设计合理的接口并使接口具备扩展性。
所谓“拒绝修改”是指针对软件功能的完善和增加,程序员不能上来就修改原来的代码,因为你并不知道这些代码(方法)的修改会对软件的现有功能有何种的影响,而是尽量使用类的继承和组合来实现新的功能。当然,对原有程序的错误修改不再此例。
 
  3.面向抽象(接口)编程原则(非面向实现编程)
  从上面的分析中可以得出一个很有用的结论:程序模块边界应该是一个遵守了某种稳定合约的语言元素,其中的关键词是"稳定"。
  那么世界上什么是稳定的?抽象!抽象总是相对稳定的。例如,家长们经常教育孩子:“要多看书”,但如果是“××出版社出的××书”就不是稳定的了。所以,为实现面向抽象编程,Java提供了两种语言要素:抽象类和接口。
  除了抽象类,Java还有一个更高层次的抽象模型--接口。它是对行为的抽象,因为程序使用对象的目的常常是要使用它的某种行为。例如,在要求别人为我们办一件事情时,我们关心的是这件事情是否能够办到,至于由谁来办则无所谓。又如,我们需要足够的照明,那么凡是能照明的东西就都符合我们的要求。于是在这种情况下客户和服务方的关系就可以按图4-6所示的样子来组织。
在面向对象的程序系统中,如果一个类不能确定它最后的类型,就是说不知道它以后要被实现成什么样,就可以采用面向接口的编程。所有需要这个类的地方都设成一个接口,而让这个类继承这个接口。后期要更改的时候只用继承这个接口就可以了。
  在一个面向对象的系统中,系统的各种功能是由许许多多的不同对象协作完成的。在这种情况下,各个对象内部是如何实现自己的对系统设计人员来讲就不那么重要了;而各个对象之间的协作关系则成为系统设计的关键。小到不同类之间的通信,大到各模块之间的交互,在系统设计之初都是要着重考虑的,这也是系统设计的主要工作内容。面向接口编程我想就是指按照这种思想来编程吧!实际上,在日常工作中,你已经按照接口编程了,只不过如果你没有这方面的意识,那么你只是在被动的实现这一思想;表现在频繁的抱怨别人改的代码影响了你(接口没有设计到),表现在某个模块的改动引起其他模块的大规模调整(模块接口没有很好的设计)等等
 
  4.接口分离原则(粒度最小、单一职责)
  在面向接口编程时,遵循按功能分类的原则,宜于使用小接口。也就是说,当一个类的功能很多时,要组织成多个接口,从而使每个接口中的一组方法一定是完成同一功能的方法。因为只有这样才便于实现功能的隔离,从而避免可变性代码的耦合。其实,Java从语言级别上就对这种原则提供了良好的支持,即一个类可以实现多个接口,而用户方则只能使用其中之一。
  这里附带说一句,有人说Java的接口机制是为了实现多继承而设置的,本书作者并不这样认为。因为从本质上来讲接口就是对方法的规范,是客户和服务方的服务合约,它并没有继承的用意。而抽象类才真正是用来继承的,但也不能多继承,因为多继承违背了分类学原理,它会在逻辑上给程序造成混乱,从而影响了程序的可读性,进而也就影响了程序的可维护性。
  这也是常说的低耦合,降耦解耦等,程序开发中坚决避免牵一发而动全身的代码。
 
  5.弱关联优先原则(组合聚合代替继承)
       先做一下名词解释:
          1、组合:contains-a的关系,表示组成部分,如一个人有手,有眼。
          2、聚合:has-a的关系,表示拥有,如一个人可以有房,有车。
          3、继承:is-a的关系,表示属于某一类,如男人是人类的一个子类。
    合成/聚合复用的优点:
         新对象存取成分对象的唯一方法是通过成分对象的接口。
         这种复用是黑箱复用。因为成分对象的细节是新对象看不见的。
         这种复用支持包装。
         这种复用所需依赖较少。
         每一个新类可以将焦点聚集在一个任务上。
         这种复用可以在运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。
    合成/聚合复用的缺点:
         使用这种复用建造的系统会有较多的对象需要管理。
    继承复用的优点:
         新的实现较为容易,因为超类的大部分功能可以通过继承关系自动进入子类。
         修改或扩展继承而来的实现较为容易。
    继承复用的缺点:
         继承复用破坏包装,将超类的实现细节暴露给子类。
         如果超类的实现发生变化,那么子类的实现也不得不发生变化。
         从超类结成而来的实现是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。  
  人们在多年面向对象程序设计实践中发现,有相当一部分程序设计人员误用或者误解了类的继承机制的作用,他们使用继承的唯一理由就是继承可以实现代码重用和功能扩充。这种做法显然违背了人们设置继承机制的初衷,从而使得程序晦涩难懂,并难以维护。其实,人们在程序设计语言中引入类及继承概念,是为了对事物进行抽象并将其分类管理,从而使描述事物的方法更为科学且更接近人类的习惯,其根本目的还是为了抽象。
  那么当某个类需要进行功能扩充时该怎么办呢?如果功能扩充之后,从分类学的角度讲这个类还属于原来的类,那么可以考虑继承。如果不是这样,那么就应该使用关联(组合、聚合)。其实,在程序设计实践中大多数属于后者,所以在进行类的扩充时首先应该考虑关联。例如,如果想在客厅中放置一套影音系统使其成为家庭影院,则不应该使用继承,而应该使用聚合关系。
  另外,由于继承是一种强关联,它在编译时期就已经确定,从而无法在运行时期动态改变,所以从实现"开-闭"原则的角度来看,它不如组合、聚合和依赖等可以动态确定和改变的弱关联更有利。总之就是一句话:扩充功能时弱关联优先,慎用继承。
 
6.依赖注入DI(Dependency Injection)原则(控制反转IoC(Inversion of Control))
  通常我们在一个类里用New方法实例化类,如AInterface a = new AInterfaceImp(); 由于以上的代码被写入了调用者程序中,同时象AinterfaceImp这样的接口的实现类是在编译时被装载的,如果以后想加入新的接口实现类则必须修改调用者的代码。
  IoC模式与以上情况不同,接口的实现类是在运行时被装载的,这样即使以后新添加了接口实现类是也不需修改调用者的代码(可以通过特定的方式来定位新增的实现类,如配置文件指定)。IoC英文为 Inversion of Control,即反转模式,意为:你待着别动,到时我会找你。
  我们都知道依赖注入也是spring框架中一种解耦策略,主要有set方式(提供set和get方法)和constractor(构造方法)方式,它使得类与类之间以配置文件的形式组织在一起,而不是硬编码的方式,例如classA 中用到了classB如果写代码的话是new 一个classB,而用依赖注入的方式则是在applicationContext.xml里面进行配置。
 
   7.里氏替换原则(LSP Liskov Substitution Principle)
  任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
  根据该原则,子类必须能够替换掉它们的基类,也就是说使用基类的方法或函数能够顺利地引用子类对象。
LSP原则与单一职责原则和接口分离原则密切相关,如果一个类比子类具备更多功能,很有可能某些功能会失效,这就违反了LSP原则。为了遵循该设计原则,派生类或子类必须增强功能。
  如果两个具体的类A,B之间的关系违反了LSP的设计,(假设是从B到A的继承关系)那么根据具体的情况可以在下面的两种重构方案中选择一种。
  -----创建一个新的抽象类C,作为两个具体类的超类,将A,B的共同行为移动到C中来解决问题。
  -----从B到A的继承关系改为委派关系。
  在进行设计的时候,我们尽量从抽象类继承,而不是从具体类继承。如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口。当然这个只是一个一般性的指导原则,使用的时候还要具体情况具体分析。
 
总结:
 
  其实上面的这些原则并不是独立的,而是相通的,或者是逐步深入的,但是不管哪种原则,实际上我们可以看出来主要都是围绕在“保证程序质量的前提下,如何提高编码效率”这个目的的,如果仔细想一想,我们平时特别重视的可扩展、可复用不都是为了这个目的吗!如果一个程序员脑子里始终充满着“减少编码工作量、提高效率”这个念头并在一切编码工作中去实行的话,他一定是一个优秀的程序员和架构师。

你可能感兴趣的:(Program)