当前位置:   article > 正文

面向对象设计六大原则_面向对象的设计原则

面向对象的设计原则

    面向对象设计的原则是面向对象思想的提炼,它比面向对象思想的核心要素更具可操作性,但与设计模式相比,却又更加的抽象,是设计精神要义的抽象概括。形象地将,面向对象思想像法理的精神,设计原则则相对于基本宪法,而设计模式就好比各式各样的具体法律条文了。

    面向对象设计原则有 6 个:开放封闭原则,单一职责原则,依赖倒置原则,Liskov 替换原则,迪米特法则和接口隔离原则或合成/聚合复用原则(不同资料略有不同,这里对7都做了整理)。 


1 单一职责原则(Single Responsibility Principle SRP)

    There should never be more than one reason for a class to change. 什么意思呢?

    所谓单一职责原则就是一个类只负责一个职责,只有一个引起变化的原因。

    如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化会削弱或抑制这个类完成其他职责的能力,这个耦合会导致脆弱的设计。

       软件设计真正要做的许多内容,就是发现职责并把这些职责相互分离;如果能够想到多于一个动机去改变一个类,那么这个类就具有多于一个职责,就应该考虑类的分离。
       以调制解调器为例如下图:


      从上述类图里面我们发现有四个方法 Dial(拨通电话),Hangup(挂电话),Receive(收到信 息),Send(发送信息),经过分析不难判断出,实际上 Dial(拨通电话)和 Hangup(挂电话)是属 于连接的范畴,而 Receive(收到信息)和 Send(发送信息)是属于数据传送的范畴。这里类包括 两个职责,显然违反了 SRP。
       这样做有潜在的隐患,如果要改变连接的方式,势必要修改 Modem,而修改 Modem 类的结果导致凡事依赖 Modem 类可能都需要修改,这样就需要重新编译和部署,不管数据 传输这部分是否需要修改。
       因此要重构 Modem 类,从中抽象出两个接口,一个专门负责连接,另一个专门负责数 据传送。依赖 Modem 类的元素要做相应的细化,根据职责的不同分别依赖不同的接口。如 下图:


      这样以来,无论单独修改连接部分还是单独修改数据传送部分,都彼此互不影响。
      总结单一职责优点:
        降低类的复杂性,
        提高可维护性
        提高可读性。 

        降低需求变化带来的风险。需求变化是不可避免的,如果单一职责做的好,一个接口修改只对相应的实现类有影响,对其它的接口无影响,这对系统的扩展性和维护性都有很大的 帮助。


2 里氏替换原则(Liskov Substitution Principle LSP)

       里氏替换原则是面向对象设计的基本原则之一。任何基类可以出现的地方,子类一定可 以出现。LSP 是继承复用的基石,只有当子类可以替换基类,软件单位的功能不受影响时, 基类才能真正的被复用,而子类也可以在基类的基础上增加新的行为。
       Liskov 提出了关于继承的原则:Inheritance should ensure that any property proved about supertype objects also holds for subtype objects.‐‐‐‐继承必须确保超类中所拥有的性质在子类 中仍然成立。2002 年,软件工程大师 Robert C. Martin 出版了一本《Agile Software DevelopmentPrinciples Patterns and Practices》,在文中他把里氏代换原则最终简化为一 句话:“Subtypes must be substitutable for their base types”也就是说子类必须能够替 换成他们的基类。
       事实上,当一个类继承了另外一个类,那么子类就拥有了父类中可以继承下来的属性和操作。理论上来讲,此时使用子类型去替换掉父类型,应该不会出现问题。
       但是,很不幸的是,在某些情况下是会出现问题的。比如,如果子类型覆盖了父类型的某些方法,或者是子类型修改了父类型某些属性的值,那么原来使用父类型的程序可能会出现错误,因为在运行期间,从表面上看,它调用的是父类型的方法,需要的是父类型方法实现的功能,但是在实际运行调用的却是子类型覆盖的实现的方法,而该方法和父类型的方法并不一样,于是导致错误的产生。
       里氏替换原则讲的是基类和子类的关系,只有这种关系存在的时候里氏替换原则才能成立。 里氏替换原则是实现开放封闭原则的具体规范。开放原则要求对扩展开放,扩展的一个实现手段就是使用继承;而里氏替换原则是保证子类型能够正确替换父类型,只有能正确替换,才能实现扩展,否则扩展了也会出现错误。
       我们大家都打过 CS 的游戏,用枪射击杀人,如下类图:


       枪的主要职责是射击,如何射击在各个具体的子类中定义。注意在类中调用其他类时务 必调用父类或接口,如果不能掉话父类或接口,说明类的射击已经违反了 LSP 原则。
       如果我们有一个玩具手 枪,该如何定义呢?我们先在类图 2-1 上增加一个类 ToyGun, 然后继承于 AbstractGun 类,修改后的类图如下:


      玩具枪是不能用来射击的,杀不死人的,这个不应该写 shoot 方法,在这种情况下业 务的调用类就会出现问题。为了解决这个问题,ToyGun 可以脱离继承,建立一个独立的父 类,为了做到代码可以服用,可以与 AbstractGun 建立关联委托关系,如下图:


        因此,如果子类不能完整地实现父类的方法,那么建议断开父子继承关系,采用依赖,聚合, 组合等关系代替继承。
       子类可以有自己的属性或方法。
       覆盖或实现父类的方法时输入的参数可以放大。 

       覆盖或实现父类的方法时输出结果可以被缩小。这是什么意思呢,父类的方法返回值是一个类型 T,子类相同的方法(覆写)的返回值为类型 S,那么根据里氏替换原则就要求 S 必须小于 等于 T,也就是说要么 S 和 T 是同一个类型,要么 S 是 T 的子类型。
       采用里氏替换原则的目的就是增加程序的健壮性,需求变更时也可以保持良好的兼容性和稳 定性,即使增加子类,原有的子类可以继续运行。在实际项目中,每个子类对应不同的业务含义, 使用父类作为参数,传递不同的子类完成不同业务逻辑。


3 依赖倒置原则(Dependence Inversion Principle DIP )

       所谓依赖倒置原则就是要依赖于抽象,不要依赖于具体。简单的说就是对抽象进行编
程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
       面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变化时,上层也要跟着 变化,这就会导致模块的复用性降低而且大大提高了开发的成本。
       面向对象的开发很好的解决了这个问题,一般的情况下抽象的变化概率很小,让用户程 序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变化,只要抽象不变,客户程 序就不需要变化。这大大降低了客户程序域实现细节的耦合度。
       比如一个合资汽车公司现在要求开发一个自动驾驶系统,只要汽车上安装上这个系统,就可以实现无人驾驶,该系统可以在福特车系列和本田车系列上使用。面向过程的结构图:

       实现代码如下:
  1. public class HondaCar {
  2. public void Run() {
  3. Console.WriteLine("本田车启动了!");
  4. }
  5. public void Turn() {
  6. Console.WriteLine("本田车拐弯了!");
  7. }
  8. public void Stop() {
  9. Console.WriteLine("本田车停止了!");
  10. }
  11. }
  12. public class FordCar {
  13. public void Run() {
  14. Console.WriteLine("福特车启动了!");
  15. }
  16. public void Turn() {
  17. Console.WriteLine("福特车拐弯了!");
  18. }
  19. public void Stop() {
  20. Console.WriteLine("福特车停止了!");
  21. }
  22. }
  23. public class AutoSystem {
  24. public enum CarType {
  25. Ford, Fonda
  26. }
  27. private HondaCar hondcar = new HondaCar();
  28. private FordCar fordcar = new FordCar();
  29. private CarType type;
  30. public AutoSystem(CarType carType) {
  31. this.type = carType;
  32. }
  33. public void RunCar() {
  34. if (this.type == CarType.Fonda) {
  35. hondcar.Run();
  36. } else if (this.type == CarType.Ford) {
  37. fordcar.Run();
  38. }
  39. }
  40. public void StopCar() {
  41. if (this.type == CarType.Fonda) {
  42. hondcar.Stop();
  43. } else if (this.type == CarType.Ford) {
  44. fordcar.Stop();
  45. }
  46. }
  47. public void TurnCar() {
  48. if (this.type == CarType.Fonda) {
  49. hondcar.Turn();
  50. } else if (this.type == CarType.Ford) {
  51. fordcar.Turn();
  52. }
  53. }
  54. }
          显然这个实现代码也可满足现在的需求。
       但是如何现在公司业务规模扩大了,该自动驾驶系统还要把吉普车也兼容了。这些就需 要修改 AutoSystem 类如下:

  1. public class AutoSystem {
  2. public enum CarType {
  3. Ford, Fonda, Jeep
  4. }
  5. private HondaCar hondcar = new HondaCar();
  6. private FordCar fordcar = new FordCar();
  7. private Jeep jeep = new Jeep();
  8. private CarType type;
  9. public AutoSystem(CarType carType) {
  10. this.type = carType;
  11. }
  12. public void RunCar() {
  13. if (this.type == CarType.Fonda) {
  14. hondcar.Run();
  15. } else if (this.type == CarType.Ford) {
  16. fordcar.Run();
  17. } else if (this.type == CarType.Jeep) {
  18. jeep.Run();
  19. }
  20. }
  21. public void StopCar() {
  22. if (this.type == CarType.Fonda) {
  23. hondcar.Stop();
  24. } else if (this.type == CarType.Ford) {
  25. fordcar.Stop();
  26. } else if (this.type == CarType.Jeep) {
  27. jeep.Stop();
  28. }
  29. }
  30. public void TurnCar() {
  31. if (this.type == CarType.Fonda) {
  32. hondcar.Turn();
  33. } else if (this.type == CarType.Ford) {
  34. fordcar.Turn();
  35. } else if (this.type == CarType.Jeep) {
  36. jeep.Turn();
  37. }
  38. }
  39. }
         通过代码分析得知,上述代码也确实满足了需求,但是软件是不断变化的,软件的需求也是 变化的,如果将来业务又扩大了,该自动驾驶系统还有能实现通用、三菱、大众汽车,这 样我们不得不又要修改AutoSystem类了。这样会导致系统越来越臃肿,越来越大, 而且依赖越来越多低层模块,只有低层模块变动,AutoSystem类就不得不跟着变 动,导致系统设计变得非常脆弱和僵硬。
       导致上面所述问题一个原因是,含有高层策略的模块,如AutoSystem模块,依赖于 它所控制的低层的具体细节的模块(如FordCar和HondaCar)。如果能使AutoSystem 模块独立于它所控制的具体细节,而是依赖抽象,那么我们就可以服用它了。这就是 面向对象中的“依赖倒置”机制。如下类图:
        实现代码如下:
  1. public interface ICar {
  2. void Run();
  3. void Stop();
  4. void Turn();
  5. }
  6. public class HondaCar:ICar {
  7. public void Run() {
  8. Console.WriteLine("本田车启动了!");
  9. }
  10. public void Turn() {
  11. Console.WriteLine("本田车拐弯了!");
  12. }
  13. public void Stop() {
  14. Console.WriteLine("本田车停止了!");
  15. }
  16. }
  17. public class FordCar :ICar {
  18. public void Run() {
  19. Console.WriteLine("福特车启动了!");
  20. }
  21. public void Turn() {
  22. Console.WriteLine("福特车拐弯了!");
  23. }
  24. public void Stop() {
  25. Console.WriteLine("福特车停止了!");
  26. }
  27. }
  28. public class Jeep:ICar {
  29. public void Run() {
  30. Console.WriteLine("福特车启动了!");
  31. }
  32. public void Turn() {
  33. Console.WriteLine("福特车拐弯了!");
  34. }
  35. public void Stop() {
  36. Console.WriteLine("福特车停止了!");
  37. }
  38. }
  39. public class AutoSystem {
  40. private ICar car;
  41. public AutoSystem(ICar car) {
  42. this.car = car;
  43. }
  44. public void RunCar() {
  45. this.car.Run();
  46. }
  47. public void StopCar() {
  48. this.car.Stop();
  49. }
  50. public void TurnCar() {
  51. this.car.Turn();
  52. }
  53. }
       现在Autosystem系统依赖于ICar这个抽象,而与具体的实现细节HondaCar:和FordCar无关,所以实现细节的变化不会影响AutoSystem.对于实现细节只要实现ICar即可。 即实现细节依赖于ICar抽象。
       综上所述:一个应用中的重要策略决定及业务 正是在这些高层的模块中。也 正是这些模块包含这应用的特性。但是,当这些模块依赖于低层模块时,低层模 块的修改比较将直接影响到他们,迫使它们也改变。这种情况是荒谬的。(很多人觉得,层次化调用的时候,应该是高层调用“底层所拥有的接口”,这是一个典型的误解。事实上,一般的高层模块包含对业务功能的处理和业务决策选择,应该被重用,是高层模块去影响底层的具体实现
       应该是处于高层的模块去迫使那些低层的模块发生改变。处于高层的模块应 优先于低层的模块。无论如何高层模块也不应该依赖于低层模块。而且我们想能 够复用的是高层的模块,只有高层模块独立于低层模块时,复用才有可能。(因此,这个底层的接口应该是高层提出,然后由底层去实现的。也就是说底层的接口的所有权在高层模块,因是一种所有权的倒置。)
       总之,高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象。抽象不应该依赖于具体,具体应该依赖于抽象。
       倒置接口所有权,这就是著名的hollywood(好莱坞)原则;不要找我们,我们会联系你。

4 迪米特法则

       迪米特法则(Law of Demeter)又叫最少知识原则(Least Knowledge Principle LKP),就 是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。(只和你的朋友谈话。)
       对面向对象来说,一个软件实体应当尽可能的少的与其他实体发生相互作用。每一个软 件单位对其他的单位都只有最少的知识,而其局限于那些与本单位密切相关的软件单位。(这个原则用来指导我们在设计系统时,因该尽量减少对象之间的交互,对象只能和自己的朋友谈话,也就是只和自己的朋友交互,从而松散类之间的耦合。通过松散类之间的耦合来降低来降低类之间的相互依赖,这样在修改系统的某一个部分的时候,就不会影响其他其他部分,从而使得系统具有更好的可维护性。)
       迪米特法则的目的在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此, 很容易使得系统的功能模块相互独立,相互之间不存在依赖关系。应用迪米特法则有可能造 成的一个后果就是,系统中存在的大量的中介类,这些类只所以存在完全是为了传递类之间 的相互调用关系‐‐‐这在一定程度上增加系统的复杂度。
       设计模式中的门面模式(Facade)和中介模式(Mediator)都是迪米特法则的应用的例 子。
       狭义的迪米特法则的缺点:
       在系统里面造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的商业逻辑无 关。
       遵循类之间的迪米特法则会使一个系统的局部设计简化,因为每一个局部都不会和远距 离的对象有之间的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系 统的不同模块之间不容易协调。
       广义的迪米特法则在类的设计上的体现: 
          优先考虑将一个类设置成不变类. 
          尽量降低一个类的访问权限。 
          尽量降低成员的访问权限。
        那么究竟哪些对象才能被当作朋友呢?最小指导原则提供了一些指导。
          当前对象本身。
          通过方法的参数传递进来的对象。
          当前对象创建的对象。
          当前对象的实例变量所引用的对象。
          方法内所创建或实例化的对象。
        总之,最小指导原则要求我们的方法调用必须保持在一定界限范围之内,尽量减小对象的依赖的关系。

       下面的代码在方法体内部依赖了其他类,这严重违反迪米特法则

  1. public class Teacher {
  2. public void commond(GroupLeader groupLeader) {
  3. List<Girl> listGirls = new ArrayList<Girl>();
  4. for (int i = 0; i < 20; i++) {
  5. listGirls.add(new Girl());
  6. }
  7. groupLeader.countGirls(listGirls);
  8. }
  9. }

       方法是类的一个行为,类竟然不知道自己的行为与其他类产生了依赖关系,这是 不允许的。正确的做法是:
  1. public class Teacher {
  2. public void commond(GroupLeader groupLeader) {
  3. groupLeader.countGirls();
  4. }
  5. }
  1. public class GroupLeader {
  2. private List<Girl> listGirls;
  3. public GroupLeader(List<Girl> _listGirls) {
  4. this.listGirls = _listGirls;
  5. }
  6. public void countGirls() {
  7. System.out.println("女生数量是:" + listGirls.size());
  8. }
  9. }


5 开放封闭原则(Open­Closed Principle OCP)

       Software entities(classes,modules,functions etc) should open for extension ,but close for modification.
什么意思呢?
所谓开放封闭原则就是软件实体应该对扩展开发,而对修改封闭。开放封闭原则是所有 面向对象原则的核心。软件设计本身所追求的目标就是封装变化,降低耦合,而开放封闭原 则正是对这一目标的最直接体现。
       开放封闭原则主要体现在两个方面: 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。 
       对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改。

为什么要用到开放封闭原则呢? 
       软件需求总是变化的,世界上没有一个软件的是不变的,因此对软件设计人员来说,必须在不需要对原有系统进行修改的情况下,实现灵活的系统扩展。

如何做到对扩展开放,对修改封闭呢? 
       实现开放封闭的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以对修改就是封闭的;而通过面向对象的继承和多态机制,可以实现 对抽象体的继承,通过覆写其方法来改变固有行为,实现新的扩展方法,所以对于扩展就是 开放的。
       对于违反这一原则的类,必须通过重构来进行改善。常用于实现的设计模式主要有 Template Method 模式和 Strategy 模式。而封装变化,是实现这一原则的重要手段,将经常 变化的状态封装为一个类。
       以银行业务员为例 
       没有实现 OCP 的设计:

  1. import com.sun.security.ntlm.Client;
  2. public class BankProcess {
  3. // 存款
  4. public void Deposite() {
  5. }
  6. // 取款
  7. public void Withdraw() {
  8. }
  9. // 转账
  10. public void Transfer() {
  11. }
  12. }
  13. public class BankStaff {
  14. private BankProcess bankpro = new BankProcess();
  15. public void BankHandle(Client client) {
  16. switch (client.Type) {
  17. // 存款
  18. case "deposite":
  19. bankpro.Deposite();
  20. break;
  21. // 取款
  22. case "withdraw":
  23. bankpro.Withdraw();
  24. break;
  25. // 转账
  26. case "transfer":
  27. bankpro.Transfer();
  28. break;
  29. }
  30. }
  31. }
       这种设计显然是存在问题的,目前设计中就只有存款,取款和转账三个功能,将来如果 业务增加了,比如增加申购基金功能,理财功能等,就必须要修改 BankProcess 业务类。我 们分析上述设计就不能发现把不能业务封装在一个类里面,违反单一职责原则,而有新的需 求发生,必须修改现有代码则违反了开放封闭原则。
       从开放封闭的角度来分析,在银行系统中最可能扩展的就是业务功能的增加或变更。对 业务流程应该作为扩展的部分来实现。当有新的功能时,不需要再对现有业务进行重新梳理, 然后再对系统做大的修改。
       如何才能实现耦合度和灵活性兼得呢?
       那就是抽象,将业务功能抽象为接口,当业务员依赖于固定的抽象时,对修改就是封闭 的,而通过继承和多态继承,从抽象体中扩展出新的实现,就是对扩展的开放。
       以下是符合 OCP 的设计:

      首先声明一个业务处理接口
  1. public interface IBankProcess {
  2. void Process();
  3. }
  4. public class DepositProcess : IBankProcess {
  5. public void Process() {
  6. //办理存款业务
  7. Console.WriteLine("Process Deposit");
  8. }
  9. }
  10. public class WithDrawProcess : IBankProcess{
  11. public void Process() {
  12. //办理取款业务
  13. Console.WriteLine("Process WithDraw");
  14. }
  15. }
  16. public class TransferProcess : IBankProcess {
  17. public void Process() {
  18. //办理转账业务
  19. Console.WriteLine("Process Transfer");
  20. }
  21. }
  22. public class BankStaff {
  23. private IBankProcess bankpro = null;
  24. public void BankHandle(Client client) {
  25. switch (client.Type) {
  26. //存款
  27. case "Deposit":
  28. bankpro = new DepositUser(); break;
  29. //转账
  30. case "Transfer":
  31. bankpro = new TransferUser(); break;
  32. //取款
  33. case "WithDraw":
  34. bankpro = new WithDrawUser(); break;
  35. }
  36. bankpro.Process(); }
  37. }
  38. }
       这样当业务变更时,只需要修改对应的业务实现类就可以,其他不相干的业务就不必修 改。当业务增加,只需要增加业务的实现就可以了。

       设计建议:
       开放封闭原则,是最为重要的设计原则,Liskov 替换原则和合成/聚合复用原则为开放 封闭原则提供保证。
       可以通过 Template Method 模式和 Strategy 模式进行重构,实现对修改封闭,对扩展开 放的设计思路。
       封装变化,是实现开放封闭原则的重要手段,对于经常发生变化的状态,一般将其封装 为一个抽象,例如银行业务中 IBankProcess 接口。
       拒绝滥用抽象,只将经常变化的部分进行抽象。

6 接口隔离原则(ISP)

       接口隔离原则 认为:"使用多个专门的接口比使用单一的总接口要好"。因为接口如果能够保持粒度够小, 就能保证它足够稳定,正如单一职责原则所标榜的那样。多个专门的接口就好比采用活字制版,可以随时拼版拆版,既利于修改,又利于文字的重用。而单一的总接口就是雕版印刷,显得笨重,实现殊为不易;
       一旦发现错字别字,就很难修改,往往需要整块雕版重新雕刻。
      所谓接口隔离原则,指的是,不应该强迫客户依赖于他们不用的方法。
      这个原则用来处理那些比较“庞大”的接口,这种接口通常会有较多的方法操作声明,设计到很多的职责。客户在使用这样的接口的时候,通常会有很多他不需要的方法,这些方法对于客户来讲,就是一种接口污染,相当于强迫用户在一大堆“垃圾方法”中去寻找他需要的方法。
      因此,这样的接口应该被分离,应该按照不同的客户需要来分离成为针对客户的接口。这样的接口中,只包含客户需要的操作声明,这样既方便了客户的使用,也可以避免因误用接口而导致的错误。
      分离接口的方式,除了直接进行代码的分离之外,还可以使用委托来分离接口,在能够支持多重继承的语言中,还可以采用多重继承的方式进行分离。

例一:
       参考下图的设计,在这个设计里,取款、存款、转帐都使用一个通用界面接口, 也就是说,每一个类都被强迫依赖了另两个类的接口方法,那么每个类有可能因 为另外两个类的方法(跟自己无关)而被影响。拿取款来说,它根本不关心“存款 操作”和“转帐操作”,可是它却要受到这两个方法的变化的影响。

       那么我们该如何解决这个问题呢?参考下图的设计,为每个类都单独设计专门的 操作接口,使得它们只依赖于它们关系的方法,这样就不会互相影了!


例二:
       使用多个专门的接口还能够体现对象的层次,因为我们可以通过接口的继承,实现对总接口的定义。例 如,.NET 框架中 IList 接口的定义。
  1. public interface IEnumerable{
  2. IEnumerator GetEnumerator();
  3. }
  4. public interface ICollection : IEnumerable{
  5. void CopyTo(Array array, int index);
  6. // 其余成员略
  7. }
  8. public interface IList : ICollection, IEnumerable{
  9. int Add(object value);
  10. void Clear();
  11. bool Contains(object value);
  12. int IndexOf(object value);
  13. void Insert(int index, object value);
  14. void Remove(object value);
  15. void RemoveAt(int index);
  16. // 其余成员略
  17. }
       如果不采用这样的接口继承方式,而是定义一个总的接口包含上述成员,就无法实现 IEnumerable 接口、 ICollection 接口与 IList 接口成员之间的隔离。假如这个总接口名为 IGeneralList,它抹平了 IEnumerable 接口、ICollection 接口与 IList 接口之间的差别,包含了它们的所有方法。现在,如果我们需要定义一 个 Hashtable 类。根据数据结构的特性,它将无法实现 IGeneralList 接口。因为 Hashtable 包含的 Add() 方法,需要提供键与值,而之前针对 ArrayList 的 Add()方法,则只需要值即可。这意味着两者的接口存 在差异。我们需要专门为 Hashtable 定义一个接口,例如 IDictionary,但它却与 IGeneralList 接口不存 在任何关系。正是因为一个总接口的引入,使得我们在可枚举与集合层面上丢失了共同的抽象意义。虽然 Hashtable 与 ArrayList 都是可枚举的,也都具备集合特征,它们却不可互换。
      如果遵循接口隔离原则,将各自的集合操作功能分解为不同的接口,那么站在 ICollection 以及 IEnumerable 的抽象层面上,可以认为 ArrayList 和 Hashtable 是相同的对象。在这一抽象层面上,二者 是可替换的,如图 2-9 所示。这样的设计保证了一定程度的重用性与可扩展性。从某种程度来讲,接口隔 离原则可以看做是接口层的单一职责原则。

       倘若一个类实现了所有的专门接口,从实现上看,它与实现一个总接口的方式并无区别;但站在调用者的 角度,不同的接口代表了不同的关注点、不同的职责,甚至是不同的角色。因此,面对需求不同的调用者, 这样的类就可以提供一个对应的细粒度接口去匹配。此外,一个庞大的接口不利于我们对其进行测试,因 为在为该接口实现 Mock 或 Fake 对象 时,需要实现太多的方法。
       概括地讲,面向对象设计原则仍然是面向对象思想的体现。例如,单一职责原则与接口隔离原则体现了封 装的思想,开放封闭原则体现了对象的封装与多态,而 Liskov 替换原则是对对象继承的规范,至于依赖倒 置原则,则是多态与抽象思想的体现。在充分理解面向对象思想的基础上,掌握基本的设计原则,并能够 在项目设计中灵活运用这些原则,就能够改善我们的设计,尤其能够保证可重用性、可维护性与可扩展性 等系统的质量属性。这些核心要素与设计原则,就是我们设计的对象法则,它们是理解和掌握设计模式的 必备知识。

7 组合/聚集复用原则

       组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP).组合和聚合都是对象 建模中关联(Association)关系的一种.聚合表示整体与部分的关系,表示“含有”,整体由 部分组合而成,部分可以脱离整体作为一个独立的个体存在。组合则是一种更强的聚合,部 分组成整体,而且不可分割,部分不能脱离整体而单独存在。在合成关系中,部分和整体的 生命周期一样,组合的新的对象完全支配其组成部分,包括他们的创建和销毁。一个合成关 系中成分对象是不能与另外一个合成关系共享。
       组合/聚合和继承是实现复用的两个基本途径。合成复用原则是指尽量使用合成/聚合, 而不是使用继承。
       只有当以下的条件全部被满足时,才应当使用继承关系。
       1 子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分“Has‐A”和“Is‐A”. 只有“Is‐A”关系才符合继承关系,“Has‐A”关系应当使用聚合来描述。
       2 永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变 成另外一个子类的话,就不要使用继承。
       3 子类具有扩展超类的责任,而不是具有置换掉或注销掉超类的责任。如果一个子类需 要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。
       错误的使用继承而不是合成/聚合的一个常见原因是错误地把“Has‐A”当成了 “Is‐A”.”Is‐A”代表一个类是另外一个类的一种;而“Has‐A”代表一个类是另外一个类的一 个角色,而不是另外一个类的特殊种类。
       我们需要办理一张银行卡,如果银行卡默认都拥有了存款、取款和透支的功能,那么我 们办理的卡都将具有这个功能,此时使用了继承关系:

      为了灵活地拥有各种功能,此时可以分别设立储蓄卡和信用卡两种,并有银行卡来对它 们进行聚合使用。此时采用了合成复用原则

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/404001
推荐阅读
相关标签
  

闽ICP备14008679号