zoukankan      html  css  js  c++  java
  • 三谈多态——善用virtual

     

    三谈多态——善用virtual

    作者:Nicrosoft(nicrosoft@sunistudio.com) —— 2002.4.30

    出处:东日软件开发网络(SSDN) http://www.ssdn.net

     
      多态性,是一种能给程序带来灵活性的东西。看过《设计模式》的程序员应该都知道,相当多的模式(几乎所有)都是依靠多态来实现的,以此给程序提供可扩展、可重用性。在《再谈多态——向上映射及VMT/DMT》一文中,提到了多态性是依赖于虚函数/虚方法(即动态绑定)来实现的,也介绍了虚函数/虚方法(virtual)的实现方法。那么本文就来谈一下,如何使用virtual、善用virtual来获取多态性给我们带来的灵活性。
      
      实例是最好的教材,因此本文还是假设一个需求,写一个实例来讲解。不过,我想没有必要给出所有源码,因此在本文中有些实现的代码会粗略带过。另外,本文所有代码均为Object Pascal语言编写,实现环境为Delphi。
      
      另外,由于“方法(Method)”一词已经成为Object Pascal的术语,因此,以下称成员函数都为“方法”。也许C++程序员会不太适应这样的称呼(呵呵,我自己也不太适应),见谅吧。
      
      假设我们要编写一个纯文本内的编辑器,也就是记事本(呵呵,别嫌例子老套,记事本程序在相当多方面都是很好的教材),编辑控件我们一般会用TMemo或TRichEdit,但是它们的功能都不甚强大,也许我们目前没有,但日后会找到一个更好的第三方文本编辑控件(比如,支持语法着色的)。因此,我们必须为未来的改进留下方便之门,否则到时候再重写全部程序真是太傻了。
      
      界面层(菜单响应、状态显示等)对文本编辑器控件的控制的代码对于所有编辑器来说都是类似的,应该可以被重用。那么就必须将界面层的代码与编辑器控件的控制代码隔离开来。
      
      如何隔离?我们可以为所有的编辑器控件指定一个公共的接口(抽象类),界面层只看得到这个接口,只使用接口提供的功能,那么,我们更换任何编辑器控件时,都不必更改界面层的代码了。
      
      首先,抽象出编辑器的基本操作,如:Load(打开文本)、Save(保存到文件)、Copy(复制到剪贴板)等等,将这些操作作为public方法。其次,考虑这些操作中有哪些会涉及到具体相关控件的,对于这些操作,你有两种选择:1、如果完全依赖控件本身的,可以选择将其定义为虚方法或抽象虚方法(即C++中的纯虚函数);2、如果只是有部分代码依赖控件本身的,将这部分操作抽象到一个protected的抽象虚方法中,而将相同的部分代码写在基类中,并由基类的方法中调用protected的抽象虚方法。
      
      如果还没有完全明白,请看代码:
       
        TEditor = class // 抽象基类
      private
          m_FileName : String;
      protected
          function DoLoad(FileName : String) : Boolean; virtual; abstract;
        public
            function Load(FileName : String) : Boolean;
            function Save() : Boolean;
            function SaveAs(FileName : String) : Boolean; virtual; abstract;
          // ... 其他需要的操作,由需求而定
        end;
      
      好,我们来详细说明一下TEditor为什么是这个样子的。其有一个私有成员,保存编辑器所对应的文件名。然后看public部分,它至少提供了三个操作:Load——从某文件中读取文本到编辑器;Save——将文本保存到文件,文件名为m_FileName所保存的;SaveAs——以指定的一个文件名保存文本。

      三个操作中,SaveAs为抽象虚方法,因为它的实际动作与每个编辑控件相关,而基类本身并不知道该如何保存文件。

      Save和Load都是非虚方法,其中Save的任务很简单,就是将文本以m_FileName所保存的文件名保存,其实现可以是调用抽象的SaveAs,只需要将文件名传给SaveAs即可。基类完全可以确定如何完成Save(因为它可以保证派生类实现了SaveAs),因此其为非虚方法。

      而Load呢?Load操作的步骤是:1、将文本从文件中读取到编辑器控件中;2、将m_FileName的值设置为所读取的文件名。其中第一个步骤与具体编辑器控件相关,应该是由派生类去实现它,第二个步骤派生类无法实现,因为派生类看不到私有的m_FileName成员。但即使把m_FileName移到protected节中,以使派生类可以访问它,也并非好办法,因为这样的话,在实现每个派生类时,都要记住设置m_FileName的值,这不仅造成代码重复(每个派生类都要这样做),而且这不应该是派生类的义务。因为m_FileName应该是基类的内部实现,对外不可见。因此,第二个步骤应该由基类来完成。如何达成这个目的呢?

      我们可以注意到,protected节中有一个DoLoad方法,它就被用来完成第一个步骤——每个编辑器控件去将文本读入编辑器。然后,DoLoad由Load方法中被选择在适当的时机调用。
      
        基类的实现如下:

        function TEditor.Load(FileName : String) : Boolean;
      begin
          Result := DoLoad(FileName);
          if Result then
              m_FileName := FileName;
        end;
      
        function TEditor.Save() : Boolean;
      begin
          SaveAs(m_FileName); // 调用抽象的 SaveAs
        end;
      
      接着,我们使用TMemo来实现一个编辑器类:
      
      TMemoEditor = class(TEditor)
      private
          m_Editor : TMemo;
      protected
          function DoLoad(FileName : String) : Boolean; override;
        public
          constructor Create();
          destrcutor Destroy(); override;
      
          function SaveAs(FileName : String) : Boolean; override;
          // ...其它需要的操作
      end;
      
      在该派生类中,有一个私有成员,即TMemo控件的实例。然后覆盖(override)了基类的两个抽象虚方法:DoLoad和Save。
      
      其实现如下:
      function TMemoEditor.Create();
      begin
          // 创建TMemo实例
            m_Editor := TMemo.Create(nil);
         
            // 接着完成将TMemo实例置于界面上显示出来等操作,省略
        end;
      
      function TMemoEditor.Destroy();
      begin
          // 其他清理工作
      
          m_Editor.Free();
          m_Editor := nil;
      end;
      
      function TMemoEditor.DoLoad(FileName : String) : Boolean;
      begin
            Result := false;
            try
              m_Editor.LoadFromFile(FileName);
           except end;
          Result := true;
      end;
      
      function TMemoEditor.SaveAs(FileName : String) : Boolean;
      begin
          Result := false;
          try
              m_Editor.SaveToFile(FileName);
          except end;
          Result := true;
        end;
      
      很好,这样的实现已经可以使个部分运作正常了。如果,今后找到更好的编辑器控件,只需要从TEditor派生,再实现一个TXXXEditor类即可,其他部分的代码不用作任何改动。而且,具体实现的TXXXEditor类中的代码,只和具体控件本身特性相关(如:读取、保存文件的方法),而公共逻辑也已经在TEditor类中实现了。
      
      virtual的使用方法,基于笔者个人认识与经验:
      
      1、如果基类不知道如何实现某方法(只有派生类知道),而基类的其他方法又必须使用该方法,则把该方法声明为抽象虚方法—— virtual; abstract;(即C++的纯虚函数)。
      
      2、如果基类能够为某方法提供一种默认实现,但派生类可能完全重写这个实现,则将该方法声明为虚方法—— virtual;并实现默认算法。

      3、如果基类能够且必须提供某方法的部分的实现,而派生类必须提供另一部份的实现,则将该方法声明为非虚方法,并在基类中为其配套提供一个虚方法或抽象虚方法,以允许由基类本身调用和被派生类覆盖。犹如上例中的Load与DoLoad。
      
      善用virtual,善用多态,你的代码将更具灵活性!

  • 相关阅读:
    React生命周期, 兄弟组件之间通信
    React组件式编程Demo-用户的增删改查
    React之this.refs, 实现数据双向绑定
    CCF CSP 201812-4 数据中心
    CCF CSP 201812-4 数据中心
    PAT 顶级 1020 Delete At Most Two Characters (35 分)
    PAT 顶级 1020 Delete At Most Two Characters (35 分)
    Codeforces 1245C Constanze's Machine
    Codeforces 1245C Constanze's Machine
    CCF CSP 201712-4 行车路线
  • 原文地址:https://www.cnblogs.com/liangqihui/p/132207.html
Copyright © 2011-2022 走看看