设计原则总提-1

有四个主要的征兆告诉我们该软件设计正在“腐烂”中。它们并不是互相独立的,而是互相关联,它们是过于僵硬、过于脆弱、不可重用性和粘滞性过高。

      1. 过于僵硬Rigidity Rigidity 致使软件难以更改,每一个改动都会造成一连串的互相依靠的模块的改动,项目经理不敢改动,因为他永远也不知道一个改动何时才能完成。

      2. 过于脆弱Fragility Fragility 致使当软件改动时,系统会在许多地方出错。并且错误经常会发生在概念上与改动的地方没有联系的模块中。这样的软件无法维护,每一次维护都使软件变得更加难以维护。(恶性循环)

      3. 不可重用性immobility immobility 致使我们不能重用在其它项目中、或本项目中其它位置中的软件。工程师发现将他想重用的部分分离出来的工作量和风险太大,足以抵消他重用的积极性,因此软件用重写代替了重用。

      4. 粘滞性过高viscosity viscosity有两种形式:设计的viscosity和环境的viscosity.当需要进行改动时,工程师通常发现有不止一个方法可以达到目的。但是这些方法中,一些会保留原有的设计不变,而另外一些则不会(也就是说,这些人是hacks)。一个设计如果使工程师作错比作对容易得多,那么这个设计的viscosity 就会很高。


  
在面向对象设计中,如何通过很小的设计改变就可以应对设计需求的变化,这是令设计者极为关注的问题。为此不少OO先驱提出了很多有关面向对象的设计原则用于指导OO的设计和开发。下面是几条与类设计相关的设计原则。 
1. 开闭原则(the Open Closed Principle OCP)

      一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。因此在进行面向对象设计时要尽量考虑接口封装机制、抽象机制和多态技术。该原则同样适合于非面向对象设计的方法,是软件工程设计方法的重要原则之一。
我们以收音机的例子为例,讲述面向对象的开闭原则。我们收听节目时需要打开收音机电源,对准电台频率和进行音量调节。但是对于不同的收音机,实现这三个步骤的细节往往有所不同。比如自动收缩电台的收音机和按钮式收缩在操作细节上并不相同。因此,我们不太可能针对每种不同类型的收音机通过一个收音机类来实现(通过重载)这些不同的操作方式。但是我们可以定义一个收音机接口,提供开机、关机、增加频率、降低频率、增加音量、降低音量六个抽象方法。不同的收音机继承并实现这六个抽象方法。这样新增收音机类型不会影响其它原有的收音机类型,收音机类型扩展极为方便。此外,已存在的收音机类型在修改其操作方法时也不会影响到其它类型的收音机。 
图1是一个应用OCP生成的收音机类图的例子:



图1 OCP应用(收音机)

2. 替换原则 (the Liskov Substitution Principle LSP)

      子类应当可以替换父类并出现在父类能够出现的任何地方。这个原则是Liskov于1987年提出的设计原则。它同样可以从Bertrand Meyer 的DBC (Design by Contract) 的概念推出。

      我们以学生为例,夜校生为学生的子类,因此在任何学生可以出现的地方,夜校生均可出现。这个例子有些牵强,一个能够反映这个原则的例子时圆和椭圆,圆是椭圆的一个特殊子类。因此任何出现椭圆的地方,圆均可以出现。但反过来就可能行不通。 

      Liskov的相关图示见图2:



图2 Liskov 原则


      运用替换原则时,我们尽量把类B设计为抽象类或者接口,让C类继承类B(接口B)并实现操作A和操作B,运行时,类C实例替换B,这样我们即可进行新类的扩展(继承类B或接口B),同时无须对类A进行修改。
3. 依赖原则 (the Dependency Inversion Principle DIP)

      在进行业务设计时,与特定业务有关的依赖关系应该尽量依赖接口和抽象类,而不是依赖于具体类。具体类只负责相关业务的实现,修改具体类不影响与特定业务有关的依赖关系。

      在结构化设计中,我们可以看到底层的模块是对高层抽象模块的实现(高层抽象模块通过调用底层模块),这说明,抽象的模块要依赖具体实现相关的模块,底层模块的具体实现发生变动时将会严重影响高层抽象的模块,显然这是结构化方法的一个"硬伤"。

      面向对象方法的依赖关系刚好相反,具体实现类依赖于抽象类和接口(见图-3)。

      为此,我们在进行业务设计时,应尽量在接口或抽象类中定义业务方法的原型,并通过具体的实现类(子类)来实现该业务方法,业务方法内容的修改将不会影响到运行时业务方法的调用。 




图3依赖原则图示

4. 接口分离原则(the Interface Segregation Principle ISP)

      采用多个与特定客户类有关的接口比采用一个通用的涵盖多个业务方法的接口要好。

      ISP原则是另外一个支持诸如COM等组件化的使能技术。缺少ISP,组件、类的可用性和移植性将大打折扣。

      这个原则的本质相当简单。如果你拥有一个针对多个客户的类,为每一个客户创建特定业务接口,然后使该客户类继承多个特定业务接口将比直接加载客户所需所有方法有效。

      图4展示了一个拥有多个客户的类。它通过一个巨大的接口来服务所有的客户。只要针对客户A的方法发生改变,客户B和客户C就会受到影响。因此可能需要进行重新编译和发布。这是一种不幸的做法。



图4 带有集成接口的服务类

      我们再看图-5中所展示的技术。每个特定客户所需的方法被置于特定的接口中,这些接口被Service类所继承并实现。




图5 使用接口分离的服务类设计

      如果针对客户A的方法发生改变,客户B和客户C并不会受到任何影响,也不需要进行再次编译和重新发布。

      以上四个原则是面向对象中常常用到的原则。此外,除上述四原则外,还有一些常用的经验诸如类结构层次以三到四层为宜、类的职责明确化(一个类对应一个具体职责)等可供我们在进行面向对象设计参考。但就上面的几个原则看来,我们看到这些类在几何分布上呈现树型拓扑的关系,这是一种良好、开放式的线性关系、具有较低的设计复杂度。一般说来,在软件设计中我们应当尽量避免出现带有闭包、循环的设计关系,它们反映的是较大的耦合度和设计复杂化。

Class Design Principles
Summary
" Open-Closed Principle (OCP)
– Extend functionality with new code
" Liskov Substitution Principle (LSP)
– Derived classes fully substitute their base classes
" Dependency Inversion Principle (DIP)
– Depend on abstractions, not details
" Interface Segregation Principle (ISP)
– Split interfaces to control dependencies  
  
我这篇文章的主旨是介绍一部分类和接口的高质量设计的准则。这些准则不但应该保证设计并且实现的类或者接口本身有高质量代码,而且更重要的是在工业领域应该尽可能的使代码的更新和维护不影响客户的活动,主要也就是保持二进制代码兼容(binary compatibility)和源代码兼容(source compatibility)。我希望这些准则能帮助刚从学校进入工业领域的朋友尽快适应更高标准的编程要求,尽快提升自己的设计能力。 
文中以C++类的设计为讨论范围。
      总提
      面向对象编程对于产出高质量,易维护的代码是非常有帮助的。面向对象编程的概念构建于三个基本特征之上:封装,继续,多态。在C++中,class是面向对象编程概念的核心和具体形式。class通过私有成员体现“封装”,通过直接继续或者组合体现“继续”,通过虚函数和动态绑定(dynamic binding)体现“多态”。class的设计质量直接决定了整个系统的质量。
      从整体功能层面谈class设计,有这么三条原则:
      ·单一功能原则(Single Responsibility Principle)
      一个class就其整体应该只提供单一的服务。假如一个class提供多样的服务,那么就应该把它拆分,反之,假如一个在概念上单一的功能却由几个class负责,这几个class应该合并。
      ·开放/封闭原则(Open/Close Principle)
      一个设计并实现好的class,应该对扩充的动作开放,而对修改的动作封闭。也就是说,这个class应该是答应扩充的,但不答应修改。假如需要功能上的扩充,一般来说应该通过添加新类实现,而不是修改原类的代码。添加新类不单可以通过直接继续,也可以通过组合。
      ·最小惊奇原理(Least Surprise Principle)
      在重载函数,或者子类实现父类虚函数时,应该基本维持函数原来所期望的功能。比如:
    
      class Pet {
      public:
      virtual Talk() = 0;
      };
      class Cat : public Pet {
      public:
      void Talk() { cout << "miao"; }
      };
      class Dog : public Pet {
      public:
      void Talk() { BiteOwner(); }
      };
      class Dog 在实现虚函数Talk的时候,没有像我们期望的那样输出狗吠声,而是咬起主人来了。这是应该避免的。
      接口和实现
      在系统中,观察一个class有两个角度,从外部或者用户角度我们看到的是接口,从内部我们看到的是实现。因为系统肯定要不断修改,因此实现免不了不停的变化,但是接口又被要求尽量保持稳定。这两者的矛盾必须通过良好的设计尽量避免,基本原则就是将实现细节与接口隔离。下面列出几条比较具体点的:
      ·接口的设计保持最小而完整
      精简接口函数个数,使每一个函数有代表性,函数功能恰好覆盖class的职能。一个最小的接口可以使维护简单,增加潜在的代码重用性,减少客户的迷惑,并且也可以缩小头文件长度和编译时间。当改进函数时,应该用类似函数名实现改进而保留原函数,代码注释里应该有相应的说明。可以增加新函数,但不能删除旧函数。
      ·成员变量应该都为私有

       显而易见,public变量破坏封装性以及接口和实现的分离;protected变量也可能使客户编写继续类而依靠于父类的实现细节。
      ·避免函数返回成员变量的指针或引用
这么做也会使客户代码依靠于实现细节。
      ·考虑是否禁用编译器缺省产生的函数
      这些函数包括:复制构造函数,赋值操作符(operator =)。假如我们不打算定义自己的版本而不禁用默认版本的话,可能使客户代码在不注重的情况下调用这些函数。当实现发生改动时就可能引起问题,比如class多了一个heap memory指针。假如我们答应对象拷贝,比较稳妥的方法是禁用它们,而定义一个专门的clone()函数。
      兼容性(compatibility)
      不用说,兼容性是非常重要的。Intel和Microsoft之所以如此成功,其中一个重要方面就是他们的产品,不管是硬件还是软件,都做到了很好的兼容老产品。代码的兼容也是如此。难以想象,假如客户依靠于你的library产品,而要因为你的产品的更新而不断的重写他的代码,他还会继续用你的产品。
      代码兼容可以简单分为二进制兼容和源代码兼容。二进制兼容也就是说,客户的已编译代码可以在不用重新编译的情况下,直接使用你的不同版本的已编译代码。源代码兼容就是,假如你的代码更新了,客户的代码不需要修改,只需要重新编译就可正常运行。在C++中,接口一般是由头文件和library二进制代码提供,因此,任何可能造成library代码和旧的头文件不一致的情况都可能破坏二进制兼容,因为客户代码必须和新的头文件重新编译一次。
      因此,遵循几条准则可以使你更轻松地解决兼容性问题:
      ·不改变类的大小或者改变成员变量的顺序
      包括几个方面:不增加或减少成员变量;不修改成员变量类型;不改变成员变量的声明顺序;不改变虚函数的有无。显而易见,增加或减少成员变量会改变类的大小,并且需要更新头文件,从而可能造成与客户代码不兼容。类型的变化也可能引起类的大小的变化。成员变量的访问一般是由编译器按偏移量确定,顺序假如改变,偏移量也就会改变,破坏了二进制兼容。至于虚函数的有无,决定是否存在虚函数表指针,也就影响了类的大小和成员变量的顺序。
      ·不使用inline函数
      inline函数声明于头文件中,并且被编译于客户代码中,假如inline函数访问了private成员,该成员又改变了顺序,那么inline函数虚要被重新编译,破坏了二进制兼容。
      ·接口函数不使用虚函数
      虚函数的访问和成员变量类似,是通过虚函数表中的偏移。虚函数顺序的改变会影响偏移。因此,在条件答应时,应该避免使用public虚函数。比如:
     
     
      class Picture {
      public:
      virtual void Draw();
      };
      应该改为
    
      class Picture {
      public:
      void Draw();
      private:
      virtual void DoDraw();
      };
      void Picture::Draw()
      {
      DoDraw();
      }
      ·不改变接口函数的顺序
      在很多嵌入式系统中,链接库通过输出函数表(eXPorted function table)暴露接口以节省空间。此时,对接口函数的访问也是通过索引值进行,因此改变顺序也会破坏兼容性。
      ·避免使用函数缺省参数
      给函数形参设定缺省值可以方便客户,但是可能破坏兼容。缺省值随头文件给出,缺省值的改变也就会引起兼容问题。
      以上就是我能想到的了,希望能对大家有帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值