今天你多态了吗?

今天你多态了吗?

Do You Polymorphism Today? [0]

 

Written by Allen Lee

 

-1. 目录

  • -1. 目录
  • 0. 写在前面的话。
    • 0.0 关键字。
    • 0.1 系统要求?!
    • 0.2 如何阅读本文?
  • 1. 图书馆魔术事件簿。
    • 1.0 图书管理员的烦恼。
    • 1.1 魔术棒是如何工作的?
    • 1.2 魔术般真能起作用吗?
    • 1.3 我们在干什么?
    • 1.4 Poly呢?
  • 2. 多态为何物?
    • 2.0 真实的多态。
    • 2.1 多态的种类。
    • 2.2 多态失效了?
    • 2.3 要睡觉了吗?
  • 3. 多态与重构。
    • 3.0 请别妨碍我们改善公司的组织结构!
    • 3.1 Poly的噩梦——图书馆的倒塌。
  • 4. 完结·新篇。
  • 5. 参考书目。
  • X. 注释。

 

0. 写在前面的话。

0.0 关键字。

  • 中文:多态,面向对象,继承,封装,抽象类,重构,基类,基类继承多态,接口,接口继承多态,抽象方法,虚方法,重载方法,继承体系,插件系统,引用类型,值类型,接口多重继承
  • 英文:Polymorphism, Object-Oriented, Inheritance, Encapsulation, abstract class, base class, Refactoring, interface, abstract method, virtual method, overriden method, override, new, Null Object Pattern, reference type, value type, switch

0.1 系统要求?!

  • 0.1.0 使用面向对象技术(至少有使用的打算)。
  • 0.1.1 对C#有一个基本的了解(我指的是语法以及相关的概念)。
  • 0.1.2 初步了解面向对象的继承以及封装(如果没有了解...就看着办吧!)。
  • 0.1.3 希望了解多态及其相关的内容。
  • 0.1.4 必要的耐性(至少你应该看完“如何阅读本文?”。连“如何阅读本文?”都看不进?关掉浏览器吧!)。
  • 0.1.5 其它......

0.2 如何阅读本文?

switch(you.Purpose)
{
  
case "希望读一个关于多态的故事“
    
if(you.IsPatient)
      
goto "1. 图书馆魔术事件簿";

  
case "因为《readonly vs. const》的Remark的最后一点而来":
    
goto "2.0 真实的多态";

  
case "仅希望了解一下多态的种类":
    
goto “2.1 多态的种类”

  
case "仅希望了解一下不同种类的多态的注意事项":
    
goto “2.2 多态失效了?”

  
case "只想知道我是如何回答'何谓多态?'":
    
goto “2.3 要睡觉了吗?”;

  
case "希望了解多态如何帮助你实现更灵活的条件扩展机制":
    
goto “3.0 请别妨碍我们改善公司的组织结构”;

  
case "希望了解Null Object模式以及Introduce Null Object重构原则如何发挥多态的威力":
    
goto “3.1 Poly的噩梦——图书馆的倒塌”;

  
case "想听听我对多态的闲谈":
    
goto “4. 完结·新篇”;

  
case "只想活动一下因使用鼠标而劳累的手":
    
goto "关闭你的浏览器";

  
default:
    
goto "从头到尾阅读本文";

}

 

1. 图书馆魔术事件簿[1]。

1.0 图书管理员的烦恼。

  • Poly:学校这几年扩招,图书馆也有一大批新书入库,而管理员却还是只有我们两个,每天都忙个不停。
  • Morph:没办法啦,这总比失业好吧?
  • Poly:哎,每天单处理归还的书的工作就叫人喘不了气了,如果这些书能够自动飞回各自所属的位置该多好呀!
  • Morph:来,我赐你一把魔术棒,这样你就不用那么辛苦了。
  • Poly:拜托!不要拿支牙刷来耍我,今天的工作恐怕又干不完了,没空跟你开玩笑呀!
  • Morph:呵呵,谁叫你有这种闲情发白日梦?

1.1 魔术棒是如何工作的呢?

刚刚才被Morph耍完的Poly现在又在发白日梦了,他想着Morph提到的魔术棒,自言自语:“如果真的有这么一支魔术棒,它将如何工作呢?”

他联想到自己平时工作的情况,认为要把图书归类好,必须满足以下两个条件:

  • a) 每本图书都应该具有一些信息,这些信息描述了该书的所属类别以及存放地点等;
  • b) 有一头任劳任怨的牛根据这些信息把书拿到存放地点放好。

想着想着,Poly奏起眉头了:“这么说来,我不就是那头牛?不行,我得让魔术棒帮帮我!”于是,Poly又陷入沉思了:如果这些书懂得自己飞回它所属的存放地点的话......对了,得让魔术棒帮我这个忙!

1.2 魔术棒真能起作用吗?

1.2.0 现实中的Poly是如何工作的?

class Poly
{
  
static void Main()
  
{
    
// 图书馆早上准时开门;
    Library library = new Library();

    
// 当然,Poly他们没有理由只开图书馆的大门,他们还要开放图书馆的各层楼。
    
// 一楼的门就是大门了,要开两次大门吗?
    Floor floor2 = library.Floor2;
    Floor floor3 
= library.Floor3;
    Floor floor4 
= library.Floor4;
    
    
// 同学们一大早就归还很多图书。
    List<Book> books = new List<Book>();
    
// 省略填充books列表的代码
    
// 

    
// 我们认为Poly会先把书分类好属于哪一层楼,然后再一次过搬到对应的楼层,
    
// 而不是傻乎乎的每拿到一本书就跑到对应的楼层并把它放入对应的书架!
    List<Book> floor2Car = new List<Book>();
    List
<Book> floor3Car = new List<Book>();
    List
<Book> floor4Car = new List<Book>();
    
    
// Poly需要根据每本书上的信息进行处理
    foreach(Book book in books)
    
{
      
// 首先看看该书属于图书馆那一层楼的。
      switch(book.FloorNumberString)
      
{
        
// 一楼是图书馆办公室和借书柜台,不用于存放图书。
        case "2nd":
          
// 把书放进往二楼的推车里。
          floor2Car.Add(book);
          
break;
        
case "3rd":
          
// 把书放进往三楼的推车里。
          floor3Car.Add(book);
          
break;
        
case "4th":
          
// 把书放进往四楼的推车里。
          floor4Car.Add(book);
          
break;

        
// default:
        
//  // 把书放进哪里?地下室?还是天台?
        
//  break;
      }

    }


    
// 接着Poly把往二楼的车推到二楼。
    
// 我们认为Poly会先把书按照所属类别分类好,放在装书篮里,
    
// 而不是傻乎乎的每拿到一本书就往对应的书架跑。
    List<Book> mathBasket = new List<Book>();
    List
<Book> physicsBasket = new List<Book>();
    List
<Book> chemistryBasket = new List<Book>();

    
foreach(Book book in floor2Car)
    
{
      
switch(book.Catalog)
      
{
        
case "Math":
          mathBasket.Add(book);
          
break;
        
case "Physics":
          physicsBasket.Add(book);
          
break;
        
case "Chemistry":
          chemistryBasket.Add(book);
          
break;
      }

    }


    
// 然后Poly提着篮子跑到对应的书架
    foreach(Book book in mathBasket)
      floor2.MathShelves.Add(book)

    
foreach(Book book in physicsBasket)
      floor2.PhysicsShelves.Add(book);

    
foreach(Book book in chemistryBasket)
      floor2.ChemistryShelves.Add(book);

    
// 接着Poly把往三楼的车推到三楼。
    
// 我们同样认为Poly会先把书按照所属类别分类好,放在装书篮里,
    
// 而不是傻乎乎的每拿到一本书就往对应的书架跑。
    List<Book> economicsBasket = new List<Book>();
    List
<Book> marketingBasket = new List<Book>();
    List
<Book> managementBasket = new List<Book>();

    
foreach(Book book in floor3Car)
    
{
      
switch(book.Catalog)
      
{
        
case "Economics":
          economicsBasket.Add(book);
          
break;
        
case "Marketing":
          marketingBasket.Add(book);
          
break;
        
case "Management":
          managementBasket.Add(book);
          
break;
      }

    }


    
// 然后Poly提着篮子跑到对应的书架
    foreach(Book book in economicsBasket)
      floor3.EconomicsShelves.Add(book)

    
foreach(Book book in marketingBasket)
      floor3.MarketingShelves.Add(book);

    
foreach(Book book in managementBasket)
      floor3.ManagementShelves.Add(book);

    
// 接着Poly把往四楼的车推到四楼。
    
// 如果Poly傻乎乎的每拿到一本书就往对应的书架跑,那我们只好让他跑了。
    List<Book> programmingBasket = new List<Book>();
    List
<Book> seBasket = new List<Book>();
    List
<Book> databaseBasket = new List<Book>();
    List
<Book> networkBasket = new List<Book>();

    
foreach(Book book in floor4Car)
    
{
      
switch(book.Catalog)
      
{
        
case "Programming":
          programmingBasket.Add(book);
          
break;
        
case "Software Engineering":
          seBasket.Add(book);
          
break;
        
case "Database":
          databaseBasket.Add(book);
          
break;
        
case "Network":
          networkBasket.Add(book);
          
break;
      }

    }


    
// 然后Poly提着篮子跑到对应的书架
    foreach(Book book in programmingBasket)
      floor4.ProgrammingShelves.Add(book)

    
foreach(Book book in seBasket)
      floor4.SEShelves.Add(book)

    
foreach(Book book in physicsBasket)
      floor4.DatabaseShelves.Add(book);

    
foreach(Book book in chemistryBasket)
      floor4.NetworkShelves.Add(book);

    
// 最后,Poly整个人躺在地上

  }

}

1.2.1 Poly,我终于理解你的呐喊了!

说实话,直道我完成这个代码,我才真正明白为何Poly一直在烦闷,我想,这个代码应该可以令校方领导下决心改善图书馆的工作环境,至少也得请多几个人来分担一下工作。否则,某天学校突然决定要为图书馆加楼层、增加新的图书类别或者调整图书现有类别时,Poly会毅然决定逃去参军(失业大军)!不信,你试着把第二书店的电脑书部分分类加入到图书馆的4楼,合并3楼现有的类别,并增加文学、宗教等系列的新类别,然后......怎么样?有什么感觉?牵一发而动全身!

1.2.2 Poly决定挥动手上的魔术棒:

Poly手上的魔术棒叫什么名字呢?既然是Morph“赐”的,就叫他Morphism吧!好了,Poly挥动他手上的Morphism魔术棒...

class Polymorphism
{
  
static void Main()
  
{
    
// 图书馆早上准时开门;
    Library library = new Library();

    
// 怎么每天只需开大门其他楼层的门就自动打开了吗?
    
// Floor floor1

    
// 同学们一大早就归还很多图书。
    List<Book> books = new List<Book>();
    
// 省略填充books列表的代码
    
// 

    
// 果然每本书自己飞回各自所属的原位哟。
    foreach(Book book in books)
    
{
      book.Return(library);
    }

  }

}


abstract class Book
{
  
public abstract void Return(Library library);
}


abstract class Floor2Book : Book { }

abstract class Floor3Book : Book { }

abstract class Floor4Book : Book { }

class MathBook : Floor2Book
{
  
public override void Return(Library library)
  
{
    library.Floor2.MathShelves.Add(
this);
  }

}


class PhysicsBook : Floor2Book
{
  
// Code here
}


class ChemistryBook : Floor2Book
{
  
// Code here
}


class ManagementBook : Floor3Book
{
  
public override void Return(Library library)
  
{
    library.Floor3.ManagementShelves.Add(
this);
  }

}


class EconomicsBook : Floor3Book
{
  
// Code here
}


class MarketingBook : Floor3Book
{
  
// Code here
}


class ProgrammingBook : Floor4Book
{
  
public override void Return(Library library)
  
{
    library.Floor4.ProgrammingShelves.Add(
this);
  }

}


class SoftwareEngineeringBook : Floor4Book
{
  
// Code here
}


class DatabaseBook : Floor4Book
{
  
// Code here
}


class NetworkBook : Floor4Book
{
  
// Code here
}

下面是魔术棒所产生的副产品[2]

1.2.3 检验魔术棒的效果。

1.2.3.0 加入新电脑书籍分类。

class WebDevelopmentBook : Floor4Book
{
  
public override void Return(Library library)
  
{
    library.Floor4.WebDevelopmentShelves.Add(
this);
  }

}

当然,你需要在4楼腾出地方来放一个(或多个)新的书架来安放Web Development类的书籍。

1.2.3.1 合并3楼现有的分类。

class BusinessBook : Floor3Book
{
  
public override void Return(Library library)
  
{
    library.Floor3.BusinessShelves.Add(
this);
  }

}

当然,你需要把Economics、Marketing、Management三类书的书架放在一起(或者你有更好的是这些书放在一起的方法),把原本书架的这三个标签撕掉,重新贴上Business标签。

1.2.3.2 学校要加盖第五层楼?

abstract class Floor5Book : Book { }

当然,你需要为5楼添加设施和设定图书分类,并放入一些书。

1.3 我们在干什么?

实质上我们在重构(Refactoring)!只是前后两种代码哪种来的更清晰以及更可读而已。当然,在这个重构的过程中,我们很难避免改动图书馆(Library)以及各层楼的公共设施(公共成员),所以,必须小心行事!

说实话,这并不是一个好的例子,因为书本的行为与图书馆结构和设施的细节有太多的藕断丝连了。所以,如果我们能够为他们找一个中间人(第三者?)来处理这些复杂关系(别说我坏心肠棒打鸳鸯呀!),避免过分的纠缠就好了。不过,作为一个开场白,我想这应该够了吧!?

1.4 Poly呢?

  • Poly:“哎呀!好疼哟!谁用书敲我的头?”
  • Morph:“你真过分,居然偷懒在这里大睡?”
  • Poly:“魔术棒...”
  • Morph:“什么魔术棒?你拿着这支牙刷干什么吗?”
  • Poly:“......”

 

2. 多态为何物?

2.0 真实的多态。

我曾经在《readonly vs. const》一文提到下面这句话:

然而,当这种结合使用枚举和条件判断的代码阻碍了你进行更灵活的扩展,并有可能导致日后的维护成本增加,你可以代之以多态,使用Replace Conditional with Polymorphism来对代码进行重构

下面我来试一下用多态实现会员分级制,首先我们来看看UML类图:

接着看看对应的代码:

class Order
{
  
public Order(Customer customer)
  
{
    ID 
= Guid.NewGuid().ToString();
    m_Customer 
= customer;
    m_Products 
= new List<Product>();
  }


  
public readonly string ID;

  
private Customer m_Customer;

  
private List<Product> m_Products;
  
public void AddProduct(Product product)
  
{
    m_Products.Add(product);
  }


  
public double TotalAmount()
  
{
    
double totalAmount = 0.0;

    
foreach(Product product in m_Products)
      totalAmount 
+= product.Price;

    
return totalAmount * m_Customer.Discount;
  }


  
public override string ToString()
  
{
    
return "Dear " + m_Customer.Name ",\n" +
           
"\tTotal amount of order " + ID +
           
" is " + TotalAmount().ToString();
  }

}


abstract class Customer
{
  
public abstract string Name get; }

  
public abstract double Discount get; }
}


class SuperVipCustomer : Customer
{
  
public SuperVipCustomer(string name)
  
{
    m_Name 
= name;
  }


  
private string m_Name;
  
public override string Name
  
{
    
get return m_Name; }
  }


  
private const double c_Discount = 0.65;
  
public override double Discount
  
{
    
get return c_Discount; }
  }


  
public override string ToString()
  
{
    
return "Name: " + m_Name + "[SuperVip]";
  }

}


class VipCustomer : Customer
{
  
// Code here
}


class NormalCustomer : Customer
{
  
// Code here
}

这样,如果日后我们发现业务需要,要添加一个InactiveCustomer身份来表示那些很久没有上来购物的人,我们就可以:

class InactiveCustomer : Customer
{
  
// Code here

  
private const double c_Discount = 1.0;
  
public override double Discount
  
{
    
get return c_Discount; }
  }


  
public override string ToString()
  
{
    
return "Name: " + m_Name + "[Inactive]";
  }

}

然而,Order类却没有受到任何的影响,这才是最重要的!当然,如果因为某些原因,我们决定去掉SuperVip这个身份,也将看到不会对Order产生任何影响!这,就是多态的威力,刚才发生在图书馆的一切...忘了吧!

2.1 多态的种类。

2.1.0 基类继承多态(Base Class Polymorphism)

现在让我们回到网上商店,我们首先创建了一个Customer的abstract class,然后,在Order里面使用这个Customer的“实例”(在Order的构造函数进行赋值初始化)。然而,我们知道一个abstract class是不可能被实例化的,那么我们究竟在引用着什么呢?答案是Customer的派生类的实例(这个派生类就不能够再是abstract的了)。由于每一个Customer的派生类跟之均是一种is-a的关系,这些派生类必定能够执行Customer约定的功能,从而只要Order知道Customer提供什么功能,就能够应付这些派生类里同样的功能了[3]。当然,如果我们在派生类里面添加了一些新的功能,那么这些新的功能将不会被Order识别而已,因为Order不能透过Customer的约定来获知这些新的功能的存在。

使用基类继承多态的关键就是有一个继承体系(如果没有,就建立一个)[4],而客户端只需要持有一个类型为这个体系的顶层基类[5]的变量,用于保存其派生类的实例,并调用预先约定的抽象方法或者虚方法就行了,剩下的事多态将会为你好好的安排!

通常我们在面对一组相关的对象时,我们就会考虑使用多态。请留意Order.TotalAmount()的代码片断:

foreach(Product product in m_Products)
  totalAmount 
+= product.Price;

这里的Product可能是一个基类(抽象或者非抽象),它的派生类可能有:Book、Toy、Movie、CD、CPU等等,然而,我们需要一种统一的方法来把一组产品的价格加总。使用多态,就可以避免询问变量的类型在判断并读取所需的数据。

2.1.1 接口继承多态(Interface Polymorphism)

除了基类继承多态,我们还有一种接口继承多态。顾名思义,这种多态是通过继承(更确切的说是“实现”)接口而产生继承体系的。所以,使用接口继承多态的关键也是拥有一个继承体系。一般情况下我们会尽量使用基类继承多态[6],其原因可能是由于关系表述的准确性(is-acan-do之间概念上的区别[7]),或者版本控制的问题。然而,接口继承多态仍然有它独特的用处,当一个对象需要拥有不同的身份时,接口继承就给了你一种实现的方式。例如String的声明如下:

public sealed class String : IComparable, ICloneable, IConvertible, IEnumerable, IComparable<string>

这样,String就可以以多种不同的身份出席不同的场合了,例如

System.Collection.SortedList.Add(object key, object value);

要求传递给该方法的key参数必须实现IComparable接口,以便能够进行排序比较。

换句话说,C#不支持基类多重继承,却支持接口多重继承。

2.1.2 混合继承多态?

由于一个类可以同时继承一个基类(base class)、实现多个接口(interface),我们不难想象一下声明:

public sealed class DerivedClass : BaseClass, IInterface 

然而,真的有混合继承多态吗?其实是没有的,因为在强类型语言里面,变量在给定的某个时刻只能够以一个身份出现,即使它同时具备了多个身份。你不能够使得一个变量同时是多种类型吧?

2.2 多态失效了?

使用基类继承多态,有一点特别需要注意的就是:基类(抽象或者非抽象)中需要获得多态效果的成员必须有abstractvirtual修饰。例如:

class BaseClass
{
  
// Code here

  
public void PrintMe()
  
{
    Console.WriteLine(
"Printed in BaseClass.");
  }

}


class DerivedClass : BaseClass
{
  
// Code here

  
public void PrintMe()
  
{
    Console.WriteLine(
"Printed in DerivedClass.");
  }

}


class Program
{
  
static void Main()
  
{
    BaseClass bc 
= new BaseClass();
    BaseClass dc 
= new DerivedClass();
    bc.PrintMe();
    dc.PrintMe();
  }

}

输出结果:

Printed in BaseClass.
Printed in BaseClass.

但是,编译器将给出以下警告:

The keyword new is required on 'DerivedClass.PrintMe()' because it hides inherited member 'BaseClass.PrintMe()'

当我们在BaseClass的PrintMe()前加上virtual时,

public virtual void PrintMe() 

将得到如下警告:

'DerivedClass.PrintMe()' hides inherited member 'BaseClass.PrintMe()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.

我们不应该忽略编译器的警告,因为别人看你的代码时,可能会疑惑你的意图,他(或她)可能猜测你是否漏掉了overridenew关键字,又或者在猜测你如果不想继承基类的成员方法,那为什么要为它起同一个名字呢(尤其是你使用new的时候)?所以你不希望继承基类的成员方法,那么最好为方法另起一个名字。你可能有一万个理由表明使用同一个名字的必要,我也没有绝对不可以这样做的意思,只是我们应该尽最大的努力使的代码的维护者一眼就能看出代码的意图而不是做来回的做揣测。

说得太多了,其实这个是继承方面的内容[8],说这些内容主要是希望大家注意程序的输出结果。从结果可以看出,这里并没有发生多态效应!所以我有必要为你重复这一点:

基类(抽象或者非抽象)中需要获得多态效果的成员必须有abstract或virtual修饰。

至于使用哪个修饰符就要看你具体的情况了。

然而,使用接口继承多态就不需要注意这些了,因为所有的接口均为抽象的,你如果要实现(impletement)一个接口,你就必须实现其所有的成员(无论是显式还是隐式)[9],否则编译器将会拒绝编译而不是仅仅给出警告!

2.3 要睡觉了吗?

好了,故事讲完了,但你总不能先个小孩一样,听完故事就上床睡觉吧?我们总得有个事后思考!或许你已经发现,从头到尾我都没有为多态下一个明确的定义。是的,我没有,也不打算这样做!一个完整精确的定义对我来说难度太大了,所以我选择用一些例子和相关的解释来为你们描述。不过我还是愿意简单的说一下何谓多态:

多态就是使得你能够用一种统一的方式来处理一组各具个性却同属一族的不同个体的机制。[10]

关于这多态,有一点很关键的,那就是多态是以继承体系为基础的,所以上面这句中的“同属一族”所指的就是“属于同一继承体系的”。觉得烦了吗?还是那一句:忘了它!

 

3. 多态与重构。

3.0 请别妨碍我们改善公司的组织结构!

多态的威力虽然强大,但并不是所有的代码一开始就在该用多态的地方使用多态。面对既有的代码,我们如何重新引爆多态的力量呢?答曰:Replace Conditional with Polymorphism![11]

我借用了《重构》的一段代码[12]

class Employee
{
  
// Code here

  
int PayAmount()
  
{
    
switch(GetKind())
    
{
      
case EmployeeType.ENGINEER:
        
return _monthlySalary;
      
case EmployeeType.SALESMAN:
        
return _monthlySalary + _commission;
      
case EmployeeType.MANAGER:
        
return _monthlySalary + _bonus;
      
default:
        
throw new InvalidOperationException("Incorrect Employee");
    }

  }

}

我们希望公司的薪金系统不会妨碍公司组织结构的改变,我们可能有上千个职位,每个都有着不同的薪资算法,这些算法又有可能随时发生变化,而且我们还随时有可能新增一个职位或者去掉一个职位,有或者暂增一个临时职位。我们希望公司组织结构的改动与薪金系统的改动都同样灵活!于是,我们要借助多态的力量了[13]

3.1 Poly的噩梦——图书馆的倒塌。

某天,Poly发了一个梦,梦中Poly和Morph坐在图书馆一楼大厅里很休闲的听着音乐。原来,魔术榜的效果不但是还书“自动化”,借书也“自动化”了。要借书的话,只需要走进一楼大厅,然后大声叫出书名,书就会飞到你的手上!如果学校领导知道这件事的话,肯定嚷着要裁掉他们两个!此时,一PLMM来到大厅,大叫一声:今天你多态了吗?突然,Poly和Morph还有那PLMM感觉到图书馆在震动,而且越来越厉害,最后,图书馆倒塌了!好在他们三个跑得快,否则后果不堪设想。究竟出了什么事?突然那PLMM尖叫:那墙上刻有“未处理异常:NullReferenctException”!

真实一个可怕的噩梦,但引用空对象在现实中确实家常便饭之事,你必须额外编写代码来处理它[14]

Employee e = paymentSystem.GetEmployee("Allen");
if(e != null && e.IsTimeToPay(today))
  e.Pay();

或者

Employee e = paymentSystem.GetEmployee("Allen");
try
{
  
if(e.IsTimeToPay(today))
    e.Pay();
}

catch(NullReferenceException)
{
  
// Exception handler code
}

然而,这些代码看起来都不太漂亮,有没有更统一直观的处理方式呢?答曰:Introduce Null Object[15]

class Employee
{
  
// Code here

  
public virtual bool IsTimeToPay(DateTime dt)
  
{
    
// 
  }


  
public virtual void Pay()
  
{
    
// 
  }

}


class NullEmployee : Employee
{
  
// Code here

  
public override bool IsTimeToPay(DateTime dt)
  
{
    
// 空方法体!
  }


  
public override void Pay()
  
{
    
// 空方法体!
  }

}


class Program
{
  
static void Main()
  
{
    Employee e 
= paymentSystem.GetEmployee("Allen");
    
// 这样,如果GetEmployee("Allen");返回一个空引用,下面的代码将什么也不做!
    if(e.IsTimeToPay(DateTime.Today)
      e.Pay();
  }

}

实际的操作中,我们为了避免客户端对NullEmployee的了解,可以把该类作为内嵌类(Nested class)加入到Employee,并提供工厂方法(Factory Method)来生成NullEmployee的实例。并且,我们会将Null Object模式与Singleton模式结合一起使用,因为Null Object对于每一个调用方来说都应该是同质的,也就是一种常量性质的东西,它的成分不应该发生改变。

class NullEmployee : Employee
{
  
private static NullEmployee m_Instance = null;

  
private NullEmployee() { }

  
public static NullEmployee CreateInstance()
  
{
    
if (m_Instance == null)
      m_Instance 
= new NullEmployee();

    
return m_Instance;
  }


  
// Code here
}

有时候,我们需要对对象进行类似IsNull()的判断并读取里面的值,那么你可以自己声明一个INullable接口,再让NullEmployee实现它。然而,你也可以直接实现.NET(ver. 2.0)内置的System.INullableValue接口:

class Employee : INullableValue
{
  
// Code here

  INullableValue.HasValue()
  
{
    
return true;
  }


  INullableValue.Value()
  
{
    
return this;
  }

}


class NullEmployee : Employee
{
  
// Code here

  INullableValue.HasValue()
  
{
    
return false;
  }


  INullableValue.Value()
  
{
    
return null;
  }

}

然而,Introduce Null Object只能用来处理引用类型(Reference Type),因为值类型(Value Type)都是密封(sealed)的。

 

4. 完结·新篇[16]。

不知不觉到了结论部分,我也不知道该说些什么,但总得留下一些东西。好了,回想一下我对“何谓多态”的回答:

多态就是使得你能够用一种统一的方式来处理一组各具个性却同属一族的不同个体的机制。

再回想一下我们的这些例子,不难发现,我们一直都在缝缝补补的,我们是修补工吗?是,也不是。说它是,那是因为我们不能轻易放弃既有的代码,要为这些代码做进一步完善;说它不是,那就是如果你一开始就把多态考虑进你的设计,那么它就不是一项修补工了,当然不排除日后你又需要使用Replace ... with Polymorphism!然而,没有人能够一开始就想出一个完美的设计,所以我们需要重构,而多态,则为我们提供如何去完善我们的设计的基本理念。

那么,如果我们一开始就把多态考虑进我们的设计又会是怎样一种情况呢?插件系统,如果需要有一点规模的话,这是我能想到的一个答案。那些支持热插拔组件的系统(插件系统)均支持向系统增加外部功能扩展模块(插件组件)而无需重新编译。然而,系统必须能够识别这些插件组件的功能并执行之才有意义。于是,我们会预先约定一组插件系统能够识别功能接口,并让插件组件实现这些接口。这样插件系统便能识别并以统一的方式来调用它们。这,不就是多态吗?

当然,插件系统的设计与实现是另一个庞大的课题,里面涉及的不仅仅是多态,还有其他很多很多的技术,但多态肯定是其核心技术,而且,它还必须遵守开放——封闭原则(The Open-Closed Principle,简称OCP),这样,系统才能够以更灵活的方式去应对未来的变化。作为一个开始,我为你们介绍了多态,剩下的路你们就要拿出自己的探索精神了。

 

5. 参考资料。

  • Martin Fowler; Refactoring: Improving the Design of Existing Code; Addison Wesley Longman, Inc., 1999
  • Robert C. Martin; Agile Software Development: Principles, Patterns, and Practices; Pearson Education, Inc., 2003
  • Alan Shalloway, James R. Trott; Design Patterns Explained: A New Perspective on Object-Oriented Design; Addison Wesley, 2002
  • Jesse Liberty; Programming C#, 3rd Edition; O'Reilly & Associates, Inc., 2003
  • Jeffrey Richter; Applied Microsoft .NET Framework Programming; Microsoft Press, 2002
  • Don Box, Chris Sells; Essential .NET, Volume 1: The Common Language Runtime; Addison Wesley, 2002
  • Eric Gunnerson; A Programmer's Introduction to C#; Apress, 2000
  • Microsoft Corporation; .NET Framework SDK Documentation; Microsoft Corporation

 


X. 注释。

  1. 本文的题目形式借用了当年Yahoo! 的标语“Do You Yahoo?”。
  2. 本故事纯属虚构,如有雷同,纯属巧合。
  3. 本UML类图是使用Microsoft Visio绘制的。
  4. 这里的“同样的功能”并不是指有着一样的执行效果的方法(method),而仅仅是签名(signature)相同(除abstract、virtual、override这三个关键字)的方法。
  5. 继承是另一个面向对象的重要概念,关于继承及如何建立继承体系的内容可以写一片完整的文章了,所以请另行查阅相关资料。
  6. 该基类不一定都是抽象类(abstract class),只要不为密封类(sealed class)就行了。
  7. 有关使用基类还是接口来建立继承体系的更详细内容,可以另行查阅相关资料。
  8. 基类继承是is-a关系,接口继承是can-do关系。
  9. 如果想更深入了解相关的内容,可以查阅有关继承体系中成员的版本控制方面的资料。
  10. 有关接口的使用,可以查阅.NET文档或者MSDN在线文档。
  11. 注意,请别将这当作多态的定义,它仅仅是我对“何谓多态”的简单回答。
  12. 由于本文的重点并不是重构,有关该代码的重构过程、结果以及相关注意事项,请参考《重构——改善既有代码的设计》的9.6 Replace Conditional with Polymorphism。
  13. 代码原样请参见《重构——改善既有代码的设计》P.257,此处代码稍有改动。
  14. 其实要应对这些变化,单纯的多态是不够的,你还要把存在变化风险的部分(例如可能随时发生改变的薪资算法)封装好,让调用方不能直接接触它,那么如果它发生了改变就不会波及到调用方,因为调用方从头到尾都不知道它是如何工作的!当然,这里的一个关键就是处理好公共接口。关于封装,它也是面向对象的一个重要概念,并且其内容也很庞大,详细了解请另行查阅相关资料。
  15. 该代码改编自《敏捷软件开发 原则、模式与实践》的第17章Null Object模式的示例。
  16. 关于Introduce Null Object重构原则详细步骤以及注意事项,请参见《重构——改善既有代码的设计》的9.7 Introduce Null Object。
  17. 由于本小节并不能算是真正意义上的结论总结,只是一个本文阅读完成与读者实践开始的交界点,所以我使用这样一个标题。当然,一开始我只知道用“Endings and Beginnings”能确切表达这种意思,却找不到一个对应的合适的中文标题,本标题是我与Teddy Tam共同讨论的结果。当然Teddy的功劳最大,他绞尽脑汁的想出一个又一个的标题,然后我只是在一旁“感觉”一下这些标题是否合适。所以在这里要再次感谢Teddy的支持。

 

posted @ 2004-11-02 14:35  Allen Lee  阅读(19096)  评论(51编辑  收藏  举报