前言
决策的最佳时机是被第一颗子弹击中时。 ———— Uncle Bob
一、“延迟”的含义
说起延迟思想,我首先联想到的是“精益软件开发论”里提到的七大原则之一的延迟决策。
查阅了一些资料后,我把“精益软件开发论”里的延迟决策归纳出了三层含义:
1、防止提前过度设计造成不必要的成本浪费;
2、等到做决策所需要的信息较充分后,再来做判断会比较正确;
3、与延迟决策配套的是快速响应的能力,这意味着设计要具有良好的扩展性、要高内聚低耦合等;
二、延迟思想在程序设计中的应用总结
A、业务设计层面
(1)配置文件
把某些与代码逻辑没有依赖关系的数据(如:数据库连接字符串、第三方平台的用户名等)放到配置文件,由程序动态读取配置文件获取,而不是直接把这些数据写死到代码中。把数据赋值的操作由代码编写时延迟到了用户使用程序时。
配置文件利用延迟思想实现了数据与代码的分离,让数据赋值这件事变得可扩展。
(2)底层业务框架
把某些共用的、常用的、底层的、核心的功能封装进业务框架,后续的开发人员在业务框架基础上进行二次开发。对于某些无需个性化的功能点,后续开发人员甚至只需要进行功能的勾选和配置就能轻而易举的完成二次开发。
例如微软针对CRM业务推出的Microsoft Dynamics CRM。后续开发者在Microsoft Dynamics CRM上搭建权限管理系统时,只需勾选角色,配置权限,就能完成一套简单权限系统的开发。
业务框架把常规功能点的组合以及具体功能的确立,从底层框架设计时期延迟到了二次开发时期,甚至更晚。底层框架开发者只需设计出来一套通用模型,而无需关心后续的具体业务。
业务框架利用延迟思想实现了业务设计的解耦。
B、代码设计层面
(1)懒加载
说起“延迟”,我马上会想到代码设计里的“懒”。
(1-1) 懒汉式单例模式
第一次在代码设计中用到“懒”这个词,还是我学单例模式那会儿,大家都知道单例模式有两种实现:饿汉式和懒汉式。
饿汉式(C#代码)
/// <summary>
/// 饿汉式
/// </summary>
class HungrySingleton
{
private static HungrySingleton Instance = new HungrySingleton();
public static HungrySingleton getInstance()
{
return Instance;
}
}
懒汉式(C#代码)
/// <summary>
/// 懒汉式
/// </summary>
class LazySingleton
{
private static LazySingleton Instance = null;
public static LazySingleton getInstance()
{
if (Instance == null)
{
Instance = new LazySingleton();
}
return Instance;
}
}
饿汉式强调所有权——提前初始化,懒汉式强调使用权——动态加载。
懒汉式单例模式利用延迟思想实现了资源的动态分配。
(1-2) Linq懒加载
所有 LINQ 查询操作都由以下三个不同的操作组成:获取数据源、创建查询、执行查询。
Linq(C#代码)
class IntroToLINQ
{
static void Main()
{
// 1. 获取数据源
int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };
// 2.创建查询
// numQuery的数据类型是IEnumerable<int>
var numQuery =
from num in numbers
where (num % 2) == 0
select num;
// 3. 执行查询
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num);
}
}
}
查询变量本身只存储查询命令,查询的实际执行将延迟到在 foreach 语句中循环访问查询变量之后进行。
Linq利用延迟思想将“数据操作的转换”和“数据操作的真正执行”进行了解耦。这样做的具体好处总结为两点:a.将查询分成多个步骤来创建,有利于查询表达式的书写;b.在真正拼接成完整的操作语句后才进行真正的操作,减少了访问数据源的次数,在具体项目中,体现为降低与数据库连接的次数,这一点对优化性能尤为重要。
(2)运行时多态(后绑定)
运行时多态(后绑定),相对应于编译时多态(前绑定)。
编译时多态,在编译时就能确定一定是执行父类方法,而非子类方法。
编译时多态(C#代码)
class Animal
{
public virtual void Speak()
{
Console.WriteLine("空气震动,发出声音");
}
}
class Cat:Animal
{
public new void Speak()
{
Console.WriteLine("喵喵");
}
}
运行时多态,从实例所属的类开始寻找匹配的方法执行,如果当前类中没有匹配的方法,则沿着继承关系逐层向上,依次在父类或各祖先类中寻找匹配方法,直到Object类。
运行时多态(C#代码)
class Dog:Animal
{
public override void Speak()
{
Console.WriteLine("汪汪");
}
}
class Program
{
public static void Main(string[] args)
{
Animal c = new Cat();
Animal d = new Dog();
c.Speak(); //前绑定--speak:空气震动,发出声音(而不是:喵喵)
d.Speak(); //后绑定--speak:汪汪
}
}
运行时多态利用延迟思想使得对象在实例化后才开始寻找执行哪个方法。这样做丰富了继承和多态的内涵,使得继承和多态在实际运用中更加灵活,尤其是针对继承体系下的异质类集合的设计,提供了更多可能性。
(3)反射
反射是延迟思想的产物。
官方定义:反射指程序可以访问、检测和修改它本身状态或行为的一种能力。Python,C#,Java都带有这个机制。
我的总结:反射主要就是获取运行时(相对于编码时)的元数据(描述数据的数据,也可以理解为程序代码本身)信息。
反射机制在软件设计中的应用场景很多,举个例子:在程序中先设计了一个接口(Interface IMyInterface),只要是实现了该接口的类所在的dll都可以作为插件插入该程序。此时,就需要通过反射机制,来调用dll中的类的方法。
反射利用延迟思想使得元数据可以在运行时动态获取,而不仅仅局限于在编码时期进行硬编码写死。这样做提高了程序的灵活性、扩展性、自适应能力等。
(4)泛型
和反射一样,泛型也是延迟思想的产物。
官方定义:泛型是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。
我的总结:在编写程序时,经常会遇到功能非常相似的模块,但是它们处理的数据类型不一样,泛型就是针对这种场景被发明出来的。泛型就是一种不确定的数据类型。泛型就是把参数类型的声明从编码时期推迟到了运行时期。
Java中泛型的使用主要体现在三个方面:泛型类、泛型接口、泛型方法。为了方便长期不使用泛型的同学回顾,我这里配一段最简单的泛型类的代码:
public class MyClass<T> {
private T value;
public MyClass(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
泛型利用延迟思想使得数据类型的指定可以在运行时才进行,而不仅仅局限于在编码时期进行硬编码写死。泛型的出现使程序的泛化设计和模块化设计变得更加简单方便。