Xây dựng biểu đồ trong C++

Trình tạo biểu đồ C++ là một công cụ mạnh mẽ để:

  • Xây dựng đồ thị phức tạp
  • Tham chiếu biểu đồ (ví dụ: đặt thực thể đại diện trên InferenceCalculator, bật/tắt các phần của biểu đồ)
  • Loại bỏ đồ thị trùng lặp (ví dụ: thay vì đồ thị chuyên dụng cho CPU và GPU trong pbtxt, bạn có thể dùng một mã duy nhất để tạo biểu đồ bắt buộc, chia sẻ nhiều nhất có thể)
  • Hỗ trợ đầu vào/đầu ra biểu đồ không bắt buộc
  • Tuỳ chỉnh biểu đồ trên mỗi nền tảng

Cách sử dụng cơ bản

Hãy xem cách sử dụng trình tạo biểu đồ C++ cho một biểu đồ đơn giản:

# Graph inputs.
input_stream: "input_tensors"
input_side_packet: "model"

# Graph outputs.
output_stream: "output_tensors"

node {
  calculator: "InferenceCalculator"
  input_stream: "TENSORS:input_tensors"
  input_side_packet: "MODEL:model"
  output_stream: "TENSORS:output_tensors"
  options: {
    [drishti.InferenceCalculatorOptions.ext] {
      # Requesting GPU delegate.
      delegate { gpu {} }
    }
  }
}

Hàm tạo CalculatorGraphConfig ở trên có thể có dạng như sau:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<std::vector<Tensor>> input_tensors =
      graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
  SidePacket<TfLiteModelPtr> model =
      graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();

  auto& inference_node = graph.AddNode("InferenceCalculator");
  auto& inference_opts =
      inference_node.GetOptions<InferenceCalculatorOptions>();
  // Requesting GPU delegate.
  inference_opts.mutable_delegate()->mutable_gpu();
  input_tensors.ConnectTo(inference_node.In("TENSORS"));
  model.ConnectTo(inference_node.SideIn("MODEL"));
  Stream<std::vector<Tensor>> output_tensors =
      inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();

  // Graph outputs.
  output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

Tóm tắt ngắn gọn:

  • Sử dụng Graph::In/SideIn để nhận dữ liệu đầu vào trong biểu đồ dưới dạng Stream/SidePacket
  • Sử dụng Node::Out/SideOut để nhận kết quả của nút dưới dạng Stream/SidePacket
  • Sử dụng Stream/SidePacket::ConnectTo để kết nối luồng và gói phụ với đầu vào nút (Node::In/SideIn) và đầu ra biểu đồ (Graph::Out/SideOut)
    • Bạn có thể sử dụng một toán tử "lối tắt" >> thay cho hàm ConnectTo (ví dụ: x >> node.In("IN")).
  • Stream/SidePacket::Cast được dùng để truyền luồng dữ liệu hoặc gói phụ của AnyType (ví dụ: Stream<AnyType> in = graph.In(0);) sang một loại cụ thể
    • Việc sử dụng các kiểu thực tế thay vì AnyType sẽ giúp bạn có một lộ trình tốt hơn để khai thác các tính năng của trình tạo biểu đồ và cải thiện khả năng đọc biểu đồ.

Cách sử dụng nâng cao

Hàm số hiệu dụng

Hãy trích xuất mã xây dựng dự đoán vào một hàm hiệu dụng chuyên dụng để giúp dễ đọc và sử dụng lại mã:

// Updates graph to run inference.
Stream<std::vector<Tensor>> RunInference(
    Stream<std::vector<Tensor>> tensors, SidePacket<TfLiteModelPtr> model,
    const InferenceCalculatorOptions::Delegate& delegate, Graph& graph) {
  auto& inference_node = graph.AddNode("InferenceCalculator");
  auto& inference_opts =
      inference_node.GetOptions<InferenceCalculatorOptions>();
  *inference_opts.mutable_delegate() = delegate;
  tensors.ConnectTo(inference_node.In("TENSORS"));
  model.ConnectTo(inference_node.SideIn("MODEL"));
  return inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<std::vector<Tensor>> input_tensors =
      graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
  SidePacket<TfLiteModelPtr> model =
      graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();

  InferenceCalculatorOptions::Delegate delegate;
  delegate.mutable_gpu();
  Stream<std::vector<Tensor>> output_tensors =
      RunInference(input_tensors, model, delegate, graph);

  // Graph outputs.
  output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));

  return graph.GetConfig();
}

Do đó, RunInference cung cấp một giao diện rõ ràng cho biết các đầu vào/đầu ra và loại đầu vào.

Có thể dễ dàng sử dụng lại, ví dụ: chỉ một vài dòng nếu bạn muốn chạy một suy luận bổ sung cho mô hình:

  // Run first inference.
  Stream<std::vector<Tensor>> output_tensors =
      RunInference(input_tensors, model, delegate, graph);
  // Run second inference on the output of the first one.
  Stream<std::vector<Tensor>> extra_output_tensors =
      RunInference(output_tensors, extra_model, delegate, graph);

Bạn không cần sao chép tên và thẻ (InferenceCalculator, TENSORS, MODEL) hay giới thiệu các hằng số riêng ở đây – những thông tin chi tiết đó được bản địa hoá thành hàm RunInference.

Lớp tiện ích

Và chắc chắn, không chỉ tập trung vào các hàm, trong một số trường hợp, bạn nên giới thiệu các lớp tiện ích để giúp mã xây dựng biểu đồ của bạn dễ đọc hơn và ít gặp lỗi hơn.

MediaPipe cung cấp công cụ tính PassThroughCalculator, chỉ đơn giản là truyền thông tin đầu vào:

input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"

output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"

node {
  calculator: "PassThroughCalculator"
  input_stream: "float_value"
  input_stream: "int_value"
  input_stream: "bool_value"
  # The order must be the same as for inputs (or you can use explicit indexes)
  output_stream: "passed_float_value"
  output_stream: "passed_int_value"
  output_stream: "passed_bool_value"
}

Hãy xem mã xây dựng C++ đơn giản để tạo biểu đồ trên:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
  Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
  Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();

  auto& pass_node = graph.AddNode("PassThroughCalculator");
  float_value.ConnectTo(pass_node.In("")[0]);
  int_value.ConnectTo(pass_node.In("")[1]);
  bool_value.ConnectTo(pass_node.In("")[2]);
  Stream<float> passed_float_value = pass_node.Out("")[0].Cast<float>();
  Stream<int> passed_int_value = pass_node.Out("")[1].Cast<int>();
  Stream<bool> passed_bool_value = pass_node.Out("")[2].Cast<bool>();

  // Graph outputs.
  passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
  passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
  passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

Mặc dù biểu diễn pbtxt có thể dễ gặp lỗi (khi chúng tôi có nhiều dữ liệu đầu vào cần truyền qua), mã C++ thậm chí còn tệ hơn: các thẻ trống lặp lại và lệnh gọi Cast. Hãy xem chúng ta có thể cải thiện bằng cách giới thiệu PassThroughNodeBuilder như thế nào:

class PassThroughNodeBuilder {
 public:
  explicit PassThroughNodeBuilder(Graph& graph)
      : node_(graph.AddNode("PassThroughCalculator")) {}

  template <typename T>
  Stream<T> PassThrough(Stream<T> stream) {
    stream.ConnectTo(node_.In(index_));
    return node_.Out(index_++).Cast<T>();
  }

 private:
  int index_ = 0;
  GenericNode& node_;
};

Giờ đây, mã xây dựng biểu đồ có thể trông giống như sau:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
  Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
  Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();

  PassThroughNodeBuilder pass_node_builder(graph);
  Stream<float> passed_float_value = pass_node_builder.PassThrough(float_value);
  Stream<int> passed_int_value = pass_node_builder.PassThrough(int_value);
  Stream<bool> passed_bool_value = pass_node_builder.PassThrough(bool_value);

  // Graph outputs.
  passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
  passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
  passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

Giờ đây, bạn không thể có thứ tự hoặc chỉ mục không chính xác trong mã xây dựng truyền qua và lưu một số nội dung nhập bằng cách đoán loại cho Cast từ đầu vào PassThrough.

Những việc nên làm và việc không nên làm

Xác định dữ liệu đầu vào của biểu đồ ngay từ đầu (nếu có thể)

Trong mã dưới đây:

  • Có thể khó đoán được bạn có bao nhiêu dữ liệu đầu vào trong biểu đồ.
  • Nhìn chung, dễ xảy ra lỗi và khó duy trì trong tương lai (ví dụ: chỉ mục có chính xác không? tên, thì sao nếu một số dữ liệu đầu vào bị xoá hoặc đặt ở chế độ không bắt buộc?, v.v.).
  • Việc sử dụng lại RunSomething bị hạn chế vì các biểu đồ khác có thể có dữ liệu đầu vào khác

KHÔNG NÊN – ví dụ về mã không hợp lệ.

Stream<D> RunSomething(Stream<A> a, Stream<B> b, Graph& graph) {
  Stream<C> c = graph.In(2).SetName("c").Cast<C>();  // Bad.
  // ...
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  Stream<A> a = graph.In(0).SetName("a").Cast<A>();
  // 10/100/N lines of code.
  Stream<B> b = graph.In(1).SetName("b").Cast<B>()  // Bad.
  Stream<D> d = RunSomething(a, b, graph);
  // ...

  return graph.GetConfig();
}

Thay vào đó, hãy xác định dữ liệu đầu vào trong biểu đồ ngay đầu trình tạo biểu đồ:

NÊN – ví dụ về mã tốt.

Stream<D> RunSomething(Stream<A> a, Stream<B> b, Stream<C> c, Graph& graph) {
  // ...
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).SetName("a").Cast<A>();
  Stream<B> b = graph.In(1).SetName("b").Cast<B>();
  Stream<C> c = graph.In(2).SetName("c").Cast<C>();

  // 10/100/N lines of code.
  Stream<D> d = RunSomething(a, b, c, graph);
  // ...

  return graph.GetConfig();
}

Sử dụng std::optional nếu bạn có luồng đầu vào hoặc gói phụ không phải lúc nào cũng được xác định và đặt ngay từ đầu:

NÊN – ví dụ về mã tốt.

std::optional<Stream<A>> a;
if (needs_a) {
  a = graph.In(0).SetName(a).Cast<A>();
}

Xác định đầu ra của biểu đồ ở cuối cùng

Trong mã dưới đây:

  • Có thể khó mà đoán được bạn có bao nhiêu kết quả đầu ra trong biểu đồ.
  • Nhìn chung, dễ xảy ra lỗi và khó duy trì trong tương lai (ví dụ: đây có phải là một chỉ mục chính xác không? tên không? Nếu một số phần phụ thuộc bị xoá hoặc biến thành không bắt buộc, v.v.).
  • Việc sử dụng lại RunSomething bị hạn chế vì các biểu đồ khác có thể có kết quả khác

KHÔNG NÊN – ví dụ về mã không hợp lệ.

void RunSomething(Stream<Input> input, Graph& graph) {
  // ...
  node.Out("OUTPUT_F")
      .SetName("output_f").ConnectTo(graph.Out(2));  // Bad.
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // 10/100/N lines of code.
  node.Out("OUTPUT_D")
      .SetName("output_d").ConnectTo(graph.Out(0));  // Bad.
  // 10/100/N lines of code.
  node.Out("OUTPUT_E")
      .SetName("output_e").ConnectTo(graph.Out(1));  // Bad.
  // 10/100/N lines of code.
  RunSomething(input, graph);
  // ...

  return graph.GetConfig();
}

Thay vào đó, hãy xác định kết quả của biểu đồ ở cuối trình tạo biểu đồ:

NÊN – ví dụ về mã tốt.

Stream<F> RunSomething(Stream<Input> input, Graph& graph) {
  // ...
  return node.Out("OUTPUT_F").Cast<F>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // 10/100/N lines of code.
  Stream<D> d = node.Out("OUTPUT_D").Cast<D>();
  // 10/100/N lines of code.
  Stream<E> e = node.Out("OUTPUT_E").Cast<E>();
  // 10/100/N lines of code.
  Stream<F> f = RunSomething(input, graph);
  // ...

  // Outputs.
  d.SetName("output_d").ConnectTo(graph.Out(0));
  e.SetName("output_e").ConnectTo(graph.Out(1));
  f.SetName("output_f").ConnectTo(graph.Out(2));

  return graph.GetConfig();
}

Giữ các nút tách biệt với nhau

Trong MediaPipe, luồng gói và các gói phụ có ý nghĩa như các nút xử lý. Mọi yêu cầu về đầu vào và sản phẩm đầu ra của nút đều được biểu thị rõ ràng và độc lập về các luồng và các gói phụ mà nút này sử dụng và tạo ra.

KHÔNG NÊN – ví dụ về mã không hợp lệ.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));

  auto& node2 = graph.AddNode("Calculator2");
  node1.Out("OUTPUT").ConnectTo(node2.In("INPUT"));  // Bad.

  auto& node3 = graph.AddNode("Calculator3");
  node1.Out("OUTPUT").ConnectTo(node3.In("INPUT_B"));  // Bad.
  node2.Out("OUTPUT").ConnectTo(node3.In("INPUT_C"));  // Bad.

  auto& node4 = graph.AddNode("Calculator4");
  node1.Out("OUTPUT").ConnectTo(node4.In("INPUT_B"));  // Bad.
  node2.Out("OUTPUT").ConnectTo(node4.In("INPUT_C"));  // Bad.
  node3.Out("OUTPUT").ConnectTo(node4.In("INPUT_D"));  // Bad.

  // Outputs.
  node1.Out("OUTPUT").SetName("b").ConnectTo(graph.Out(0));  // Bad.
  node2.Out("OUTPUT").SetName("c").ConnectTo(graph.Out(1));  // Bad.
  node3.Out("OUTPUT").SetName("d").ConnectTo(graph.Out(2));  // Bad.
  node4.Out("OUTPUT").SetName("e").ConnectTo(graph.Out(3));  // Bad.

  return graph.GetConfig();
}

Trong mã trên:

  • Các nút được kết nối với nhau, ví dụ: node4 biết đầu vào của nút này đến từ đâu (node1, node2, node3) và việc này khiến việc tái cấu trúc, bảo trì và sử dụng lại mã trở nên phức tạp
    • Kiểu sử dụng như vậy là hạ cấp từ cách biểu diễn proto, trong đó các nút được phân tách theo mặc định.
  • Các lệnh gọi node#.Out("OUTPUT") bị trùng lặp và dễ đọc vì bạn có thể dùng tên rõ ràng hơn đồng thời cũng cung cấp một kiểu thực tế.

Vì vậy, để khắc phục các vấn đề trên, bạn có thể viết mã xây dựng biểu đồ sau:

NÊN – ví dụ về mã tốt.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  // `node1` usage is limited to 3 lines below.
  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();

  // `node2` usage is limited to 3 lines below.
  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();

  // `node3` usage is limited to 4 lines below.
  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();

  // `node4` usage is limited to 5 lines below.
  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Giờ đây, nếu cần, bạn có thể dễ dàng xoá node1 và đặt b làm dữ liệu đầu vào của biểu đồ cũng như không cần cập nhật cho node2, node3, node4 (tương tự như trong cách biểu diễn proto), vì chúng được tách riêng.

Nhìn chung, mã ở trên sao chép biểu đồ proto chặt chẽ hơn:

input_stream: "a"

node {
  calculator: "Calculator1"
  input_stream: "INPUT:a"
  output_stream: "OUTPUT:b"
}

node {
  calculator: "Calculator2"
  input_stream: "INPUT:b"
  output_stream: "OUTPUT:C"
}

node {
  calculator: "Calculator3"
  input_stream: "INPUT_B:b"
  input_stream: "INPUT_C:c"
  output_stream: "OUTPUT:d"
}

node {
  calculator: "Calculator4"
  input_stream: "INPUT_B:b"
  input_stream: "INPUT_C:c"
  input_stream: "INPUT_D:d"
  output_stream: "OUTPUT:e"
}

output_stream: "b"
output_stream: "c"
output_stream: "d"
output_stream: "e"

Ngoài ra, giờ đây bạn có thể trích xuất các hàm hiệu dụng để sử dụng lại trong các biểu đồ khác:

NÊN – ví dụ về mã tốt.

Stream<B> RunCalculator1(Stream<A> a, Graph& graph) {
  auto& node = graph.AddNode("Calculator1");
  a.ConnectTo(node.In("INPUT"));
  return node.Out("OUTPUT").Cast<B>();
}

Stream<C> RunCalculator2(Stream<B> b, Graph& graph) {
  auto& node = graph.AddNode("Calculator2");
  b.ConnectTo(node.In("INPUT"));
  return node.Out("OUTPUT").Cast<C>();
}

Stream<D> RunCalculator3(Stream<B> b, Stream<C> c, Graph& graph) {
  auto& node = graph.AddNode("Calculator3");
  b.ConnectTo(node.In("INPUT_B"));
  c.ConnectTo(node.In("INPUT_C"));
  return node.Out("OUTPUT").Cast<D>();
}

Stream<E> RunCalculator4(Stream<B> b, Stream<C> c, Stream<D> d, Graph& graph) {
  auto& node = graph.AddNode("Calculator4");
  b.ConnectTo(node.In("INPUT_B"));
  c.ConnectTo(node.In("INPUT_C"));
  d.ConnectTo(node.In("INPUT_D"));
  return node.Out("OUTPUT").Cast<E>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  Stream<B> b = RunCalculator1(a, graph);
  Stream<C> c = RunCalculator2(b, graph);
  Stream<D> d = RunCalculator3(b, c, graph);
  Stream<E> e = RunCalculator4(b, c, d, graph);

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Các nút riêng biệt để dễ đọc hơn

KHÔNG NÊN – ví dụ về mã không hợp lệ.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();
  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();
  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();
  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();
  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();
  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Trong mã trên, có thể bạn khó mà hiểu được ý tưởng về nơi mỗi nút bắt đầu và kết thúc. Để cải thiện điều này và giúp ích cho trình đọc mã, bạn chỉ cần để trống các dòng trước và sau mỗi nút:

NÊN – ví dụ về mã tốt.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();

  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();

  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();

  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Ngoài ra, cách trình bày ở trên phù hợp hơn với cách trình bày proto CalculatorGraphConfig.

Nếu bạn trích xuất các nút thành hàm hiệu dụng, thì các nút đó sẽ nằm trong phạm vi của các hàm đã có sẵn và biết rõ vị trí bắt đầu và kết thúc của nút. Do đó, bạn có thể sử dụng:

NÊN – ví dụ về mã tốt.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  Stream<B> b = RunCalculator1(a, graph);
  Stream<C> c = RunCalculator2(b, graph);
  Stream<D> d = RunCalculator3(b, c, graph);
  Stream<E> e = RunCalculator4(b, c, d, graph);

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}