冒号课堂§10.1:多态类型
冒号课堂
第十课 多态机制(1)课前导读
本课通过实例编程和对抽象类型的解读,显示了OOP中多态机制和抽象类型的重要性,有助于培养和加深读者的OOP语感。
本课共分两节——
1.多态类型——静中之动
2.抽象类型——实中之虚
动静屈伸,唯变所适
-
继承是多态的基础,多态是继承的目的
-
多态是动静结合的产物,将静态类型的安全性和动态类型的灵活性融为一体
-
前者(参数多态)是发散式的,让相同的实现代码应用于不同的场合
-
后者(包含多态)是收敛式的,让不同的实现代码应用于相同的场合
-
模板方法模式突出的是稳定坚固的骨架,策略模式突出的是灵活多变的手腕
当冒号迈着不变的步伐出现在教室时,手上有了一点变化:左手仍拎着笔记本包,右手却多了一样东西。大家定睛一看,原来是个电脑主板,不由得暗自纳闷:难道软件课改成了硬件课?
冒号照例直入主题:“上节课我们对继承的利弊作了详细的分析,其中最重要的观点是:继承的主要用途不是代码重用,而是代码被重用。这依赖于两个前提,一个是在语义上遵循里氏代换原则,另一个是在语法上支持多态(polymorphism)机制。因此不妨说,对于静态类型语言来说,继承是多态的基础,多态是继承的目的。”
问号忍不住问:“为什么要强调静态类型呢?”
“还记得鸭子类型[1]吗?那就是一种不依赖于继承的多态类型,也是动态类型语言一大优劣参半的特色。”冒号提醒道,“静态类型语言中的多态是动静结合的产物,将静态类型的安全性和动态类型的灵活性融为一体。它一般有两种实现方式:一种利用GP(泛型编程)中的参数多态(parametric polymorphism),一种利用OOP中的包含多态(inclusion polymorphism)或称子类型多态(subtyping polymorphism)。从实现机制上看,二者的不同之处在于何时将一个变量与其实际类型所定义的行为挂钩。前者在编译期,属于早绑定 (early binding)或静态绑定(static binding)[2];后者在运行期,属于迟绑定 (late binding)或动态绑定(dynamic binding)。从应用形式上看,前者是发散式的,让相同的实现代码应用于不同的场合;后者是收敛式的,让不同的实现代码应用于相同的场合。从思维方式上看,前者是泛型式编程风格,看重的是算法的普适性;后者是对象式编程风格,看重的是接口与实现的分离度。尽管二者从范式到语法、语义都大相径庭,但都是为着同一个目的:在保证必要的类型安全的前提下,突破编译期间过于严苛的类型限制。对于既是静态类型语言又是静态语言、既支持OOP又支持GP的C++、Java和C#而言,多态机制是保证代码的灵活性、可维护性和可重用性的终极武器。为了说明问题,我们看一个简单而实用的例子:编写一个类,让它能储存用户名和密码,以作今后验证之用。”
叹号一愣:“这题是不是太简单了?还有别的要求吗?”
冒号摇摇头。
引号却认为:“要求太少反而不好做。比如是把数据放在内存、还是文件或者数据库?密码以明文还是密文的形式存储?”
句号提出:“无论是数据的存放方式还是密码的加密方式,都不应该硬编码。”
“循此思路,我们就来编写一个可重用的抽象类。”冒号投放了一段Java代码——
abstract class Authenticator
{
/** 保存用户名和密码 */
final public void save(String user, String password)
{
if (password == null)
password = "";
store(user, encrypt(password));
}
/** 验证用户名和密码 */
final public boolean authenticate(String user, String password)
{
String storedPassword = retrieve(user);
if (storedPassword == null) return false; // 无此用户
if (password == null)
password = "";
return storedPassword.equals(encrypt(password));
}
/** 保存用户名和加密过的密码 */
protected abstract void store(String user, String encryptedPassword);
/** 从用户名获取相应的加密过的密码 */
protected abstract String retrieve(String user);
/** 给明文单向(one-way)加密,默认不加密 */
protected String encrypt(String text) { return text; }
}
冒号解说道:“该抽象类有两个public接口,一个用来保存,一个用来验证。它们用final修饰符来禁止子类覆盖,因为真正的扩展点是三个protected方法。其中store和retrieve是抽象的,encrypt有一个平凡实现。以此为基础,再根据实际需要来编写子类,具体实现这三个方法。”
幻灯片转到下一页——
import java.util.HashMap;
/** 一个简单的验证类,数据放在内存,密码保持明文 */
class SimpleAuthenticator extends Authenticator
{
private Map<String, String> usrPwd = new HashMap<String, String>();
@Override protected void store(String user, String encryptedPassword)
{
usrPwd.put(user, encryptedPassword);
}
@Override protected String retrieve(String user)
{
return usrPwd.get(user);
}
}
“我们利用HashMap来储存数据,密码保持明文。这大概是最简单的一种子类了。”冒号仿佛在轻轻地把玩着一件小物什,“为安全起见,最好还是将密码加密。于是我们设计了稍微复杂一点的子类——”
/** 一个安全的验证类,数据放在内存,密码经过SHA-1加密 */
class Sha1Authenticator extends SimpleAuthenticator
{
private static final String ALGORITHM = "SHA-1"; // SHA-1算法
private static final String CHARSET = "UTF-8"; // 避免依赖平台
@Override protected String encrypt(String plainText)
{
try
{
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
md.update(plainText.getBytes(CHARSET));
byte digest[] = md.digest();
// BASE64编码比十六进制编码节省空间
//为简便起见用到了非标准的API,因此以下代码有警告
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()); // 不可能发生
}
}
}
逗号质疑道:“不是具体类不宜被继承的吗?怎么Sha1Authenticator类却继承了具体类SimpleAuthenticator?”
冒号略表赞许:“很高兴你没有忘记这个原则。不过考虑到Sha1Authenticator类需要覆盖父类的encrypt方法,这么做也是情有可原的。当然最好选择让该类直接继承抽象类Authenticator,但作为示例代码,我们还是希望它简洁一些,不想让过多的细枝末节掩盖核心主干。下面是测试代码——”
{ // 为避免额外依赖,没有采用JUnit等单元测试工具
public static void main(String[] args)
{
test(new SimpleAuthenticator());
test(new Sha1Authenticator());
}
// 测试给定的Authenticator
private static void test(Authenticator authenticator) // 子类型多态
{
test(authenticator, "user", "password");
test(authenticator, "user", "newPassword");
test(authenticator, "admin", "admin");
test(authenticator, "guest", null);
test(authenticator, null, "pass");
authenticator.save("scott", "tiger");
assert(!authenticator.authenticate("scott", "TIGER")); // 大小写敏感
assert(!authenticator.authenticate("SCOTT", "tiger")); // 大小写敏感
}
private static void test(Authenticator authenticator, String user, String password)
{
authenticator.save(user, password);
assert(authenticator.authenticate(user, password));
}
}
引号觉得眼熟:“这不是上节课讲的模板方法模式吗?”
“正是此公。”冒号确认,“该模式的核心思想是:固定整体框架和流程以保证可重用性,留出一些子类定制点以保证可扩展性。在测试代码的两个test方法中,传入的参数是Authenticator类,但数据存放和密码加密的方式是在运行中才确定的,即先后遵照SimpleAuthenticator类和Sha1Authenticator类的实现。这就是我们所说的子类型多态的效果——让不同的实现代码应用于相同的场合。假设没有多态机制,这种效果就只能靠if/else或switch之类的条件语句才能实现,非常地痛苦。”
冒号的眉头皱成了粗体的“川”字。
“还有更好的方法吗?”句号察言观色,断定老冒还留有后手。
果不其然,冒号的眉毛立刻又舒展开来,中气充沛地应道:“有!诸位请看——”
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));
}
}
冒号加以引导:“如果仔细比较两种设计,就会发现它们很相似。后者只不过把前者对子类开放的接口合成为自己的两个成员。再看接口的实现类——”
{
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 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的多态性更加隐蔽。”冒号如是说。
叹号挑剔说:“后一种创建实例稍显麻烦一些。”
“但它是以小弊换大利。”冒号朗声而道,“首先,后者用的是合成与接口继承,比前者的实现继承更值得推荐,理由在上堂课业已阐明。其次,假设共有M种数据存取方式,包括内存、文件、数据库等等;共有N种加密方式,包括明文、SHA-1、SHA-256、MD5等等。按第一种设计,需要(M×N)个实现类;按第二种设计,只要(M+N)个实现类。这还只是两种变化因素,假如需要考虑更多的因素,二者差距将更大。比如增加编码方式:加密后的数据可以选择费空间省时间的十六进制编码、费时间省空间的BASE64编码、省时间省空间却包含非打印字符的原始形式等;比如增加安全强度:引入salt、nonce或IV等[3];比如增加密码状态:已生效密码、未生效密码、已过期密码等等。对比下面的UML类图,孰优孰劣更加一目了然。”
众人眼前出现了两幅图——
冒号指着屏幕问:“图二不仅比图一少了三个实现类,而且可重用性也更高。大家说是为什么?”
引号应答:“图一中的九个Authenticator的子类只能作为验证类来重用,而图二中六个实现类不仅可以合作完成验证类的功能,还能分别单独提供键值存储和加密字符串的功能。”
冒号作出肯定:“这就是职责分离的好处。存储与加密本是两样不相干的工作,必要时可以合作,但平时最好分开管理,符合‘低耦合、高内聚’的原则。”
问号注意到图中的注释,遂问:“第二种采用的是策略模式?”
冒号颔首:“简单地说,策略模式(strategy pattern或policy pattern)的基本思想是:把一个模块所依赖的某类算法委交其他模块实现。比如Java中的Comparable和Comparator、C#中的IComparer就是比较算法的接口,当一个类的某个方法接收了此种类型的参数,实质上就采用了策略模式。”
逗号不以为奇:“这岂非很平常?”
“你认为设计模式真的高不可攀吗?”冒号反问道,“包括模板方法模式,你们很可能也在编程实践中采用过,只不过相交不相识罢了。”
句号看出:“模板方法模式与策略模式非常神似,都是把一个类的可变部分移交给其他类处理。”
“照你这么说,绝大多数设计模式都是神似的,这也是为什么我们不专门谈设计模式的缘故。GoF设计模式是OOP大树上结出的硕果,在你心中培养的OOP成熟之前,匆忙缔结的果实多半是青涩弱小的。”冒号忠告,“我们也不会对设计模式避而不谈,但凡提及都是水到渠成的产物。再说回这两种设计模式,虽然有相通的思想,也能解决相同的问题,在稳定性与灵活性之间都取得了某种平衡,但还是各有侧重的。模板方法模式突出的是稳定坚固的骨架,策略模式突出的是灵活多变的手腕。不妨拿国家政策作比:一个强调对内要稳,老一辈制订了大政方针,下一代必须在坚持原则的前提下进行完善;一个强调对外要活,不能或不便自行开发的技术不妨从国外引进。”
叹号一乐:“哈!设计模式上升到了政策模式。”
冒号抽丝剥茧:“正如模板方法模式可看作控制反转的特例,策略模式与依赖注射(Dependency Injection)也异曲同工。第二个Authenticator所依赖的两个功能KeyValueKeeper和Encrypter,就是是通过构造方法‘注射’进来的[4]。当然策略只是一种特殊的依赖,是自内而外的——将算法抽出来外包;依赖注射的机制更复杂、涵盖面更广,是自外而内的——从外部嵌入定制功能。后者被广泛地用于框架应用之中,尤以Spring Framework和Google Guice为代表。”
引号听得起劲:“这下热闹了,设计模式、框架与OOP范式全搅和到一块了。”
“还有GP范式呢。”冒号顺接话题,“让我们再用C++的模板来实现一下Authenticator类吧。没有继续采用Java,是因为它的泛型仍离不开子类型多态。”
说着,他换上了C++代码——
#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两个接口换成了模板参数。由于模板是在编译期间实例化的,因此没有动态绑定的运行开销,但缺点是不能动态改变策略[5]。”冒号分析道,“至此,我们通过一个验证类的三种解法,分别展示了三种形式的多态:基于类继承的多态、基于接口继承的多态和基于模板的多态。它们殊途同归,都能让代码更简洁、更灵活、可重用性更高、更易维护和扩展。”
问号想到一个问题:“C语言既没有子类型多态也没有参数多态,又如何保证高质量的C程序呢?”
冒号眉梢轻挑:“C语言有指针啊,C++、Java和C#的多态在底层就是用指针实现的。C中的函数指针比Java中的接口更加灵活高效,当然对程序员的要求也更高。”
引号蓦地记起:“重载不也是一种多态吗?”
“刚才所说的多态都属于通用多态(universal polymorphism)。此外,还有一类特别多态(ad-hoc polymorphism),常见有两种形式。一种是强制多态(coercion polymorphism),即一种类型的变量在作为参数传递时隐式转换成另一种类型,比如一个整型变量可以匹配浮点型变量的函数参数。另一种就是重载多态(overloading polymorphism),它允许不同的函数或方法拥有相同的名字。特别多态浅显易懂,其重要性与通用多态也不可同日而语,故不在我们关注之列。只是要注意一点,正如子类型应遵守超类型的规范,同名的函数或方法也应遵守相同的规范。如果为贪图取名方便而滥用重载,早晚因小失大。”冒号告诫道。
逗号突发奇论:“一个多态类型的对象可以在不同的类型之间变来变去,是不是叫‘变态类型’更生动些?”
“我看你就属于典型的变态类型。”句号乘机拿他开涮。
全班哈哈大笑。
-
在静态类型语言中,继承是多态的基础,多态是继承的目的。
-
多态结合了静态类型的安全性和动态类型的灵活性。
-
多态可分为通用多态和特别多态两种。
-
通用多态主要包括参数多态和包含多态(或子类型多态)。它们都是为了克服静态类型过于严格的语法限制。
-
特别多态主要包括强制多态和重载多态。
-
参数多态是静态绑定,重在算法的普适性,好让相同的实现代码应用于不同的场合。
-
包含多态是动态绑定,重在接口与实现的分离度,好让不同的实现代码应用于相同的场合。
-
策略模式授予客户自由选择算法(策略)的权力。
-
模板方法模式重在稳定坚固的骨架,策略模式重在灵活多变的手腕。
-
合理地运用基于类继承的多态、基于接口继承的多态和基于模板的多态,能增强程序的简洁性、灵活性、可维护性、可重用性和可扩展性。