依赖注入与对象封装

依赖注入(Dependency Injection)是单元测试中非常重要的技术,以这个思想为基础,Java的世界里测试技术(比如Google Guice)已经达到了非常完善的高度。与之类比,C++的世界要落后太多,绝大部分时候都要自己动手造轮子。在自己动手的过程中,我常常被一些基本的问题困扰,比如,如果一个对象(父对象)的某个成员变量是另外一个对象(子对象),那么我们是应该在父对象的构造函数里创建子对象呢,还是在外面创建子对象然后再传递进构造函数?

在外面创建子对象然后再传递进父对象的构造函数符合依赖注入的基本原理。这种做法对于测试的好处是不言而喻的。我们可以很容易的创建一个 Mock 子对象,设置好它的行为,然后将它传入父对象中,然后对父对象进行各种测试。然而这种方法将子对象的创建代码都放在父对象之外,这给使用父对象的其他代码带来了很大的不便,因为客户程序需要同时创建父对象和子对象。如果对象的包含关系有好几层(比如子对象还有自己的子对象),按照这种方法编写程序很快就变得非常繁琐。

如果退一步想,上面这样使用依赖注入实际是违反对象封装(Encapsulate in OOP) 的基本思想的,因为父对象的使用者应该不需要知道子对象的存在。在实践中,我经常使用的程序库基本上都只要求用户创建顶层对象,而不使用依赖注入的方法。这样的程序接口要简洁好用得多。

在我看来,要真正用好依赖注入,我们需要仔细的考虑到底哪些对象是当前对象的Dependency,哪些对象实际上属于当前的对象。

  • 对于Dependency关系,使用Dependency Injection适合的的。比如一个外部的 RPC Service Stub,或者一个数据库连接。一般来讲,创建这些对象的方法也是Well-Known,在父对象的构造函数中要求传入这些对象不会对客户代码带来特别的困难。
  • 对于真正属于父对象的子对象(a.k.a. 一个确定的 Has-A 关系),那么使用 Dependency Injection 很多时候是不合适的。正确的做法是不应该要求构造函数中需要传入这些子对象。如果为了测试方便,设置一些 Setter 方法传入 Mock 对象来覆盖默认生成的子对象,这是可行的,因为单元测试是白盒测试的一种,测试代码了解一些对象内部的实现并不是什么问题。

参考资料:https://stackoverflow.com/questions/31121611/dependency-inversion-principle-solid-vs-encapsulation-pillars-of-oop

Flume 的 EncodeState 和 DecodeState

Flume里面每个Fn都有两个成员函数:

void Fn::EncodeState(string* state);
static Fn* Fn::DecodeState(const string& state);

今日终于彻底搞明白了它们的用途。主要推理过程如下:

  • Flume的执行过程很有意思,它首先把用户写的主程序执行一遍,计算所有必要的Pipeline Operations,然后才真正开始处理数据。
  • 所有的 Fn 都在主程序里面创建了一个实例。Fn的构造参数可以是主程序执行的时候算出来的动态数据,但是不能包含任何Flume操作的结果。
  • Flume Worker 只运行 Fn 而不执行主程序,所以不知道这些构造参数的值。它们生成Fn的唯一办法,就是主程序在它的 Fn 实例上调用 EncodeState,把构造参数序列化,然后再传到Worker上去。Worker调用 DecodeState来生成一个Fn的实例。

这个东西更合适的名字也许应该是 EncodeConstructorParameters。

周期函数的平均值

周期函数的平均值就等于在一个完整周期上的积分除以周期的长度。下面是一段Python程序:

import sympy

# Calculate the Expectation of v_k ** 2, 
# since v_k is a periodical function on 0 to 3, we 
# can do it as the following. 
k = sympy.symbols('k')
v_k = 1.2 * sympy.sin( 2 * sympy.pi * k / 3)
v_k_square = v_k * v_k
ex = sympy.integrate(v_k_square, (k, 0, 3)) / 3
print('E[v(k)**2]', ex)

# The result is:
# E[v(k)**2] 0.720000000000000

翻译成为数学公式就是:

(1)   \begin{equation*} \int_{x=0}^{3} (1.2 \cdot \sin ( \dfrac{2 \pi x}{3}))^2 dx = 0.72 \end{equation*}

顺便赞一下,这个sympy真的是很强,自己第一次用的时候,惊为天人!

支持垃圾收集的编程语言

能够支持垃圾收集的编程语言,在绝大多数环境下,当然要比那些不支持这个特性的编程语言方便。就算大家打扫屋子,也是过一段时间打扫一次,而不是一有垃圾马上就动手。

只可惜,自己被这个C++给困住了,周围的项目都是C++,可以访问的库也都是C++,没有机会使用其他支持垃圾收集的编程语言。

当然了,如果不在多个线程之间使用指针传递数据,那么 C++ 提供的 std::unique_ptr 再加上一些编程规范,也能够很好的自动管理内存,不需要关心对象的生命周期,比如:

    • 所有使用 new 申请的内存空间立刻使用 std::unique_ptr 包装起来。
    • 当把这种通过new申请来的数据传递给自己的调用者时,永远返回 std::unique_ptr。
    • 当把这种通过new申请来的数据传递给自己调用的函数时,永远使用原始指针(a.k.a. T* ) 或者常量引用 (a.k.a. const T&)。
    • 所有的程序永远不调用 delete。

这样一来,C++ 的对象作用域规则就可以保证一块数据不再需要的时候通过 std::unique_ptr 的析构函数自动将其释放,绝大部分实践中这样也已经足够好了。