إنشاء الرسوم البيانية بلغة C++

تعد أداة إنشاء الرسوم البيانية لـ C++ أداة قوية من أجل:

  • إنشاء الرسوم البيانية المعقّدة
  • الرسوم البيانية لضبط الإعدادات (مثل ضبط مفوَّض على InferenceCalculator، وتفعيل/إيقاف أجزاء من الرسم البياني)
  • إزالة الرسوم البيانية المتكررة (على سبيل المثال، بدلاً من الرسوم البيانية المخصصة لوحدة المعالجة المركزية (CPU) ووحدة معالجة الرسومات في pbtxt، يمكن أن يكون لديك رمز واحد ينشئ الرسوم البيانية المطلوبة، ويشارك أكبر قدر ممكن)
  • دعم المدخلات/المخرجات الاختيارية للرسم البياني
  • تخصيص الرسوم البيانية لكل نظام أساسي

الاستخدام الأساسي

دعونا نرى كيف يمكن استخدام أداة إنشاء الرسم البياني C++ لرسم بياني بسيط:

# 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 {} }
    }
  }
}

قد تبدو الدالة لإنشاء CalculatorGraphConfig أعلاه كما يلي:

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();
}

ملخص قصير:

  • استخدام Graph::In/SideIn للحصول على إدخالات الرسم البياني كـ Stream/SidePacket
  • استخدام Node::Out/SideOut للحصول على مخرجات العُقد مثل Stream/SidePacket
  • استخدِم Stream/SidePacket::ConnectTo لتوصيل ساحات المشاركات والحِزم الجانبية بمدخلات عُقدة (Node::In/SideIn) ومخرجات الرسم البياني (Graph::Out/SideOut)
    • هناك عامل تشغيل "اختصار" >> يمكنك استخدامه بدلاً من الدالة ConnectTo (مثل x >> node.In("IN")).
  • تُستخدَم Stream/SidePacket::Cast لبثّ مجموعة بث أو حزمة جانبية من AnyType (مثل Stream<AnyType> in = graph.In(0);) إلى نوع معيّن.
    • يؤدي استخدام الأنواع الفعلية بدلاً من AnyType إلى وضعك على مسار أفضل للاستفادة من إمكانات أداة إنشاء الرسوم البيانية وتحسين سهولة قراءة الرسوم البيانية.

الاستخدام المتقدّم

وظائف الأدوات

هيا نستخرج كود إنشاء الاستنتاج إلى دالة أداة مخصصة للمساعدة في سهولة القراءة وإعادة استخدام التعليمة البرمجية:

// 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();
}

نتيجةً لذلك، توفّر "RunInference" واجهة واضحة توضّح المُدخلات/المخرجات وأنواعها.

يمكن إعادة استخدامها بسهولة، على سبيل المثال، إنها بضع أسطر فقط إذا كنت ترغب في إجراء استنتاج نموذج إضافي:

  // 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);

وليس عليك تكرار الأسماء والعلامات (InferenceCalculator، TENSORS، أو MODEL) أو إضافة ثوابت مخصّصة هنا وهناك، إذ تتم ترجمة هذه التفاصيل إلى الدالة RunInference.

فئات المرافق

وبالتأكيد، لا يتعلق الأمر بالدوال فقط، فمن المفيد في بعض الحالات تقديم فئات المساعدة التي يمكن أن تساعد في جعل التعليمة البرمجية لإنشاء الرسم البياني أكثر قابلية للقراءة وأقل عرضة للخطأ.

توفّر MediaPipe الآلة الحاسبة PassThroughCalculator التي تمرر المدخلات الخاصة بها:

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"
}

لنرى رمز إنشاء C++ البسيط لإنشاء الرسم البياني أعلاه:

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();
}

على الرغم من أنّ تمثيل pbtxt قد يكون عرضة للخطأ (عندما يكون لدينا العديد من الإدخالات لاجتيازه)، يبدو رمز C++ أسوأ من ذلك، إذ إنّ العلامات الفارغة المتكرّرة واستدعاء Cast. دعونا نتعرّف على كيفية تحسين الأداء من خلال تقديم PassThroughNodeBuilder:

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_;
};

والآن يمكن أن يظهر رمز إنشاء الرسم البياني على النحو التالي:

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();
}

لا يمكنك الآن ترتيب أو فهرس غير صحيح في رمز الإنشاء وحفظ بعض الكتابة عن طريق تخمين نوع Cast من الإدخال PassThrough.

افعل ولا تفعل

تحديد إدخالات الرسم البياني في البداية إن أمكن

في الرمز أدناه:

  • قد يكون من الصعب تخمين عدد الإدخالات في الرسم البياني.
  • يمكن أن يكون هناك خطأ بشكل عام ويصعب الحفاظ عليه في المستقبل (على سبيل المثال، هل هو فهرس صحيح؟ اسم؟ ماذا لو تمت إزالة بعض الإدخالات أو تم جعلها اختيارية؟ وغير ذلك).
  • إعادة استخدام RunSomething محدودة لأن الرسوم البيانية الأخرى قد تحتوي على إدخالات مختلفة

إجراء غير مسموح به: مثال على رمز برمجي غير صالح

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();
}

بدلاً من ذلك، حدد إدخالات الرسم البياني في بداية أداة إنشاء الرسم البياني:

إجراء مطلوب: مثال على الرموز البرمجية الجيدة

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();
}

يمكنك استخدام std::optional إذا كان لديك تدفق إدخال أو حزمة جانبية لم يتم تحديدها دائمًا ووضعها في البداية:

إجراء مطلوب: مثال على الرموز البرمجية الجيدة

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

تحديد مخرجات الرسم البياني في النهاية

في الرمز أدناه:

  • قد يكون من الصعب تخمين عدد المخرجات الموجودة في الرسم البياني.
  • يمكن أن يكون عرضة للخطأ بشكل عام ويصعب الحفاظ عليه في المستقبل (على سبيل المثال، هل هو فهرس صحيح؟ اسم؟ ماذا لو تمت إزالة بعض النقاط أو تم جعلها اختيارية؟ وغير ذلك).
  • إعادة استخدام RunSomething محدودة لأنّ الرسوم البيانية الأخرى قد يكون لها مخرجات مختلفة.

إجراء غير مسموح به: مثال على رمز برمجي غير صالح

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();
}

بدلاً من ذلك، حدِّد مخرجات الرسم البياني في نهاية أداة إنشاء الرسم البياني:

إجراء مطلوب: مثال على الرموز البرمجية الجيدة

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();
}

إبقاء العُقد منفصلة عن بعضها البعض

في MediaPipe، يكون لتدفقات حزم البيانات والحزم الجانبية أهمية كبيرة مثل عُقد المعالجة. ويتم التعبير عن أي متطلبات لإدخال الأجزاء ومنتجات الإخراج بوضوح وبشكل مستقل من حيث ساحات المشاركات والحزم الجانبية التي تستهلكها وتنتجها.

إجراء غير مسموح به: مثال على رمز برمجي غير صالح

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();
}

في الرمز أعلاه:

  • تقترن العُقد بعضها ببعض، على سبيل المثال تعرِف node4 مصدر مدخلاتها (node1 وnode2 وnode3)، ما يزيد من تعقيد عملية إعادة الهيكلة والصيانة وإعادة استخدام الرمز.
    • يُعد نمط الاستخدام هذا رجوعًا إلى تمثيل أوّلي، حيث يتم فصل العُقد بشكل تلقائي.
  • يتم تكرار مكالمات node#.Out("OUTPUT") وتتأثر سهولة القراءة، حيث يمكنك استخدام أسماء أوضح بدلاً من ذلك وتقديم نوع فعلي أيضًا.

لذلك، لإصلاح المشكلات المذكورة أعلاه، يمكنك كتابة رمز إنشاء الرسم البياني التالي:

إجراء مطلوب: مثال على الرموز البرمجية الجيدة

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();
}

إذا لزم الأمر، يمكنك بسهولة إزالة node1 وإدخال b في الرسم البياني بدون الحاجة إلى إجراء أي تعديلات على node2 وnode3 وnode4 (كما هو الحال في التمثيل الأوّلي) لأنهما يتم فصلهما عن بعضهما البعض.

بشكل عام، يكرر الكود أعلاه الرسم البياني الأوّلي بشكل أكبر:

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"

علاوة على ذلك، يمكنك الآن استخراج دوال الأداة لإعادة استخدامها في رسوم بيانية أخرى:

إجراء مطلوب: مثال على الرموز البرمجية الجيدة

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();
}

عليك فصل العُقد لتسهيل قراءة المحتوى.

إجراء غير مسموح به: مثال على رمز برمجي غير صالح

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();
}

في التعليمة البرمجية أعلاه، قد يكون من الصعب استيعاب فكرة بداية ونهاية كل عقدة. لتحسين ذلك ومساعدة برامج قراءة التعليمات البرمجية، يمكنك ببساطة الحصول على سطور فارغة قبل كل عقدة وبعدها:

إجراء مطلوب: مثال على الرموز البرمجية الجيدة

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();
}

أيضًا، يتطابق التمثيل أعلاه مع تمثيل النموذج الأوّلي CalculatorGraphConfig بشكل أفضل.

إذا قمت باستخراج العُقد إلى دوال مفيدة، يتم تحديدها ضمن الدوال بالفعل، ومن الواضح مكان بدايتها وانتهائها، لذلك لا بأس في أن يكون لديك ما يلي:

إجراء مطلوب: مثال على الرموز البرمجية الجيدة

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();
}