# 多态类型——静中之动

# 作用

对于静态类型语言来说,继承是多态的基础,多态是继承的目的。
静态类型语言中的多态是动静结合的产物,将静态类型的安全性和动态类型的灵活性融为一体。

# 实现

  • 一般有两种实现方式:
一种利用GP(泛型编程)中的参数多态(Parametric Polymorphism)
一种利用OOP中的包含多态(Inclusion Polymorphism)或称子类型多态(Subtyping Polymorphism)
  • 从实现机制上看,不同之处在于何时将一个变量与其实际类型所定义的行为挂钩
前者在编译期,属于早绑定(Early Binding)或静态绑定(Static Binding);
后者在运行期,属于迟绑定(late Binding)或动态绑定(dynamic Binding)。
  • 从形式上看
前者是发散式的,让相同的代码应用于不同的场合;
后者是收敛式的,让不同的代码应用于相同的场合。
  • 从思维方式上看
前者是泛型编程风格,看重的是算法的普适性;
后者是对象式编程风格,看重接口与实现的分离。
二者目的都一样:
在保证必要的类型安全的前提下,突破编译期间过于严苛的类型限制。

# 示例

对于既是静态类型语言又是静态语言、既支持OOP又支持GP的C++、Java和C#而言,
多态机制是保证代码灵活性、可维护性和可重用性的终极武器。
另外一种设计方案:
// 键值对的存取接口
interface KeyValueKeeper
{
    public void store(String key, String value);
    public String retrieve(String key);
}

// 加密接口
interface Encrypter
{
    public String encrypt(String plainText);
}

class Authenticator
{
    private KeyValueKeeper keeper;
    private Encrypter encrypter;

    public Authenticator(KeyValueKeeper keeper, Encrypter encrypter)
    {
        this.keeper = keeper;
        this.encrypter = encrypter;
    }

    public void save(String user, String password)
    {
        if (password == null)
            password = "";
        keeper.store(user, encrypter.encrypt(password));
    }

    public boolean authenticate(String user, String password)
    {
        String storedPassword = keeper.retrieve(user);
        if (storedPassword == null) return false;

        if (password == null)
            password = "";
        return storedPassword.equals(encrypter.encrypt(password));
    }
}
// 如果仔细比较两种设计,就会发现它们很相似。后者只不过把前者对子类开放的接口合成为自己的两个成员。
// 再看接口的实现类——
class MemoryKeeper implements KeyValueKeeper
{
     private Map<String, String> keyValue = new HashMap<String, String>();

    @Override public void store(String key, String value)
    {
        keyValue.put(key, value);  
    }

    @Override public String retrieve(String key)
    {
        return keyValue.get(key);  
    }
}

class PlainEncrypter implements Encrypter
{
    @Override public String encrypt(String plainText)
    {
        return plainText;
    }
}

class Sha1Encrypter implements Encrypter
{
    private static final String ALGORITHM = "SHA-1";
    private static final String CHARSET = "UTF-8"; 

    @Override public String encrypt(String plainText)
    {
        try
        {
            MessageDigest md = MessageDigest.getInstance(ALGORITHM);
            md.update(plainText.getBytes(CHARSET));
            byte digest[] = md.digest();
            return (new sun.misc.BASE64Encoder()).encode(digest); 
        }
        catch (java.security.NoSuchAlgorithmException e)
        {
            throw new InternalError(e.getMessage());
        }
        catch (java.io.UnsupportedEncodingException e)
        {
            throw new InternalError(e.getMessage());
        }
    }
}
//  MemoryKeeper与SimpleAuthenticator、Sha1Encrypter与Sha1Authenticator除了超类型和方法访问修饰符外,其他毫无二致。
public class TestAuthenticator
{
    public static void main(String[] args)
    {
        test(new Authenticator(new MemoryKeeper(), new PlainEncrypter()));
        test(new Authenticator(new MemoryKeeper(), new Sha1Encrypter()));
    }    

    private static void test(Authenticator authenticator) // 隐含子类型多态
    { /* 同上,略 */}
}
// 测试代码区别也不大,只是Authenticator的多态性更加隐蔽
  • 设计对比:
1. 后者的实现是以小弊换大利,首先,后者用的是合成与接口继承,比前者的实现继承更值得推荐。

2. 其次,假设共有M种数据存取方式,包括内存、文件、数据库等等;共有N种加密方式,
  包括明文、SHA-1、SHA-256、MD5等等。按第一种设计,需要(M×N)个实现类;按第二种设计,只要(M+N)个实现类。
		这还只是两种变化因素,假如需要考虑更多的因素,二者差距将更大。
			比如增加编码方式:加密后的数据可以选择费空间省时间的十六进制编码、
        费时间省空间的BASE64编码、省时间省空间却包含非打印字符的原始形式等;
			比如增加安全强度:引入salt、nonce或IV等[3];
			比如增加密码状态:已生效密码、未生效密码、已过期密码等等。
  • 两种设计的UML比较(P292)
第一种设计中的九个Authenticator的子类只能作为验证类来重用,
第二种设计中六个实现类不仅可以合作完成验证类的功能,还能分别单独提供键值存储和加密字符串的功能。

这就是职责分离的好处。存储与加密本是两样不相干的工作,必要时可以合作,但平时最好分开管理,符合‘低耦合、高内聚’的原则。

(GoF设计模式是OOP大树上结出的硕果,在你心中培养的OOP成熟之前,匆忙缔结的果实多半是青涩弱小的。)

# 策略模式(strategy pattern或policy pattern)的基本思想

把一个模块所依赖的某类算法委交其他模块实现。比如Java中的Comparable和Comparator、C#中的IComparer就是比较算法的接口,

当一个类的某个方法接收了此种类型的参数,实质上就采用了策略模式。
	
模板方法模式与策略模式非常神似,都是把一个类的可变部分移交给其他类处理。
模板方法模式突出的是稳定坚固的骨架,策略模式突出的是灵活多变的手腕。
不妨拿国家政策作比:
	一个强调对内要稳,老一辈制订了大政方针,下一代必须在坚持原则的前提下进行完善;
	一个强调对外要活,不能或不便自行开发的技术不妨从国外引进。

正如模板方法模式可看作控制反转的特例,策略模式与依赖注射(Dependency Injection)也异曲同工。
第二个Authenticator所依赖的两个功能KeyValueKeeper和Encrypter,就是是通过构造方法‘注射’进来的。
(constructor injection,另外两种常用的注射方法是setter injection和interface injection。)
策略只是一种特殊的依赖,是自内而外的——将算法抽出来外包;
	依赖注射的机制更复杂、涵盖面更广,是自外而内的——从外部嵌入定制功能。
	后者被广泛地用于框架应用之中,尤以Spring Framework和Google Guice为代表。
  • 示例
#include <string>
#include <map>

using namespace std;

template <typename KeyValueKeeper, typename Encrypter>
class Authenticator
{
    private:
        KeyValueKeeper keeper;
        Encrypter encrypter;
    public:
        void save(const string& user, const string& password)
        {
            keeper.store(user, encrypter.encrypt(password));
        }

        bool authenticate(const string& user, const string& password) const
        {
            string storedPassword;
            if (!keeper.retrieve(user, storedPassword)) return false;

            return storedPassword == encrypter.encrypt(password);
        }
};

class MemoryKeeper
{
    private:
        map<string, string> keyValue;
    public:
        void store(const string& key, const string& value)
        {
            keyValue[key] = value;  
        }

        bool retrieve(const string& key, string& value) const
        {
            map<string, string>::const_iterator itr = keyValue.find(key);
            if (itr == keyValue.end()) return false;

            value = itr->second;  
            return true;
        }
};

class PlainEncrypter
{
    public:
        string encrypt(const string& plainText) const { return plainText; }
};

class Sha1Encrypter
{
    public:
        string encrypt(const string& plainText) const { /* 省略代码  */ }
};

namespace
{
    template <typename K, typename E>
    void test(Authenticator<K, E> authenticator) // 参数多态
    { /* 省略代码  */ }
}

int main()
{ 
    test(Authenticator<MemoryKeeper, PlainEncrypter>());
    test(Authenticator<MemoryKeeper, Sha1Encrypter>());
    return 0;
}

以上代码与Java版的策略模式代码很相似,主要的区别是把KeyValueKeeper和Encrypter两个接口换成了模板参数。
由于模板是在编译期间实例化的,因此没有动态绑定的运行开销,但缺点是不能动态改变策略。
通过一个验证类的三种解法,分别展示了三种形式的多态:
	基于类继承的多态、基于接口继承的多态和基于模板的多态。
	C中的函数指针比Java中的接口更加灵活高效,当然对程序员的要求也更高。
  • 重载不也是一种多态吗?
刚才所说的多态都属于通用多态(universal polymorphism)。

此外,还有一类特别多态(ad-hoc polymorphism),常见有两种形式。
一种是强制多态(coercion polymorphism),
	即一种类型的变量在作为参数传递时隐式转换成另一种类型,
	比如一个整型变量可以匹配浮点型变量的函数参数。

另一种就是重载多态(overloading polymorphism),
	它允许不同的函数或方法拥有相同的名字。
	特别多态浅显易懂,其重要性与通用多态也不可同日而语,故不在我们关注之列。

只是要注意一点,正如子类型应遵守超类型的规范,同名的函数或方法也应遵守相同的规范。
如果为贪图取名方便而滥用重载,早晚因小失大。