跨 domain 的 API 问题(下)

编译时选择

很显然如果我们只是提供这些类,但并不要求它们继承任何父类,我们甚至可以将成员函数改变成 static method,这样一来它们和某个东西的 trait class 更加接近,只是互相没有关系。

namespace domain_a {
  class gender {
  public:
    static void transfrom(const domain_a_input& in, domain_a_output& output) const override {
      // implementation
    }
  };
}

我们可以实现一些诸如下面的方法

// domain A
template <typename Label>
void do_something() {
  auto input = get_input();
  auto output = prepare_output();
  Label::transform(input, output);
}

但是如果需要打破编译时和运行时的 boundary,我们往往需要根据某个变量的不同取值进行 dispatching,于是一个常见的结构如下:

// domain A
enum DomainALabel {
  gender, // ...
};

void do_something(DomainALabel l, const domain_a_input& in, domain_a_output& output) {
  std::function<void(const domain_a_input& in, domain_a_output& output)> func;
  switch (l) {
    case DomainALabel::gender:
      func = &domain_a::gender::transform;
      break;
    // ...
    default:
      // fail
  }
  // ...
  func(input, output);
}

这种做法可以让我们把各个 domain 的实现放在各个 domain 里面,通过 std::function 直接 bind 对应的实现,灵活性很高(比如可以灵活的 share 实现)。但是 boilerplate 仍然很高,集中的定义需要分散。

enum

我们是否发现不同 domain 里面都需要的仅仅是这些东西的一个标识。实现本身应该分散到每个 domain 之中。同时如果上面的 dispatching pattern 都是如此一样,我们是否能设计一个 template 生成这种结构?

很显然表示的话我们可以使用 enum,它和 string 不一样的地方在于它本身可以作为 template 参数,这样一来我们未必使用单独的 label 类,我们可以使用一族 enum 参数的模板实现。

enum label {
  unknown = -1,
  gender = 0,
  // ...
};

template <label l>
void transform(...) {
}

但是这样并不适合统一减少 template 的 boilerplate。一种方式是 somehow 能够推断 std::function 的类型,或者直接存放对应的函数指针。另一点是与其 build 一个 lookup 的 map 还不如要求这个 enum 是连续的整数,这样我们可以通过 offset 获得对应的函数。

template <template <label> typename Api>
using label_ops_lookup = std::array<typename api_sig<Api>::type, label_arraysize>;

template <template <label> typename Api>
label_ops_lookup<Api> label_ops() {
  label_ops_lookup<Api> result;
  initialize<0, label_arraysize, Api>::set(result);
  return result;
}

我们这里使用了一个常数 label_arraysize 表示最多有效的 label 个数,比如只有 gender 的话为 1。我们要求 Api 是一个以 label 为模板参数的模板类,我们通过 type inference 计算其对应的 API signature:api_sig::type,比如一般都应该是函数指针。我们通过 initialize 这个模板类来初始化这个 lookup 结构并返回给 caller。

为了简单起见,我们假定所有的 Api 类都应该提供一个 func 的 static function,比如

namespace domain_a {
template <label l>
struct transform {
  static void func(const domain_a_input& in, domain_a_output& output);
};
}

然后我们的 initialize 类需要通过 compile time 递归将对应的 function pointer 设置到数组中

template <int M, int N, template <label> Api>
struct initialize {
  template <typename Container>
  static void set(Container& c) {
    c[M] = &Api<static_cast<label>(M)>::func;
    initialize<M+1, N, Api>(c);
  }
};

template <int N, template <label> Api>
struct initialize<N, N, Api> {
  template <typename Container>
  static void set(Container& c) {
  }
};

比较难的大约就是获得 func 的类型了,由于可能存在没有 specialization 的情况,我们只能试试 Api<static_cast(M)>::func 是否存在,如果不存在应该继续尝试 M+1,直到我们能够找到一个为止。这很显然需要一个 SFINAE 的技巧

template <typename T>
struct has_func {
  struct no {};

  template <typename U>
  static auto test(int) -> decltype(&U::func);

  template <typename U>
  static no test(...);

  static const bool value = !std::is_same<no, decltype(test<T>(0))>::value;

  using type = decltype(test<T>(0));
};

这样我们通过 has_func<Api<static_cast(M)>> 就能判断其是否存在 func 的定义,然后通过 compile time recursion 就能取得一个非 no 的结果作为返回,同时我们可以使用 std::is_pointer 等 type_traits 的工具进行判定,避免用户提供的不是我们想要的 static function。

这样一来我们可以想象每个 module 里面使用这个东西的地方都可以如下使用

void do_something(label l, const domain_a_input& in, domain_a_output& output) {
  static const auto transforms = label_ops<domain_a::transform>();
  // ...
  transforms[l](in, output);
  // ...
}

这个代价很显然被限制在运行时一个 offset 的 lookup 而已。实现与对应的 module 应该在一起,调用也非常的 clean。唯一的 boilerplate 仅仅只有 wrap 在外面的 struct,我们甚至可以提供一些 generic adaptor 把一些 template function 弄成这个结构,或者提供特化。作为一个固定的 pattern,我们能够很容易 identify 每个 domain 里面使用这个 label_ops 的位置从而方便维护这类代码。

很显然这个版本仅仅支持每种 label 都要存在 specialization 的 case,通过稍许改动我们可以提供一个 label_ops 接受一个 default value,这样可以在具有缺失的 case 提供 failover handler。而这个版本的 label_ops 会强迫每个 label 都必须提供一个实现,而且是在 compile time,也是一个很有用的 feature(试想某人增加了一个新的 label enum,然后就会发现若干个 domain 里面某个部分编译出错,随着他提供了对应的实现,整个系统又重新编译通过)。

Advertisements
跨 domain 的 API 问题(下)

一个有关“跨 domain 的 API 问题(下)”的想法

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s