Diagramme in C++ erstellen

Der C++ Graph Builder ist ein leistungsstarkes Tool für:

  • Komplexe Grafiken erstellen
  • Grafiken parametrisieren (z.B. einen Bevollmächtigten für InferenceCalculator festlegen, Teile der Grafik aktivieren/deaktivieren)
  • Grafiken deduplizieren (z. B. können Sie in PBtxt anstelle von speziellen CPU- und GPU-Grafiken die erforderlichen Grafiken mit einem einzigen Code erstellen und so viel wie möglich teilen).
  • Optionale Ein- und Ausgaben von Diagrammen unterstützen
  • Anpassen von Grafiken nach Plattform

Grundlegende Nutzung

Sehen wir uns an, wie der C++ Graph Builder für eine einfache Grafik verwendet werden kann:

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

Die Funktion zum Erstellen des obigen CalculatorGraphConfig könnte so aussehen:

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

Kurze Zusammenfassung:

  • Verwenden Sie Graph::In/SideIn, um Grafikeingaben als Stream/SidePacket zu erhalten
  • Verwenden Sie Node::Out/SideOut, um Knotenausgaben als Stream/SidePacket abzurufen
  • Verwenden Sie Stream/SidePacket::ConnectTo, um Streams und Nebenpakete mit Knoteneingaben (Node::In/SideIn) und grafischen Ausgaben (Graph::Out/SideOut) zu verbinden
    • Es gibt einen Verknüpfungsoperator >>, den Sie anstelle der ConnectTo-Funktion verwenden können (z.B. x >> node.In("IN")).
  • Stream/SidePacket::Cast wird verwendet, um den Stream oder das Nebenpaket von AnyType (z. B. Stream<AnyType> in = graph.In(0);) in einen bestimmten Typ umzuwandeln.
    • Wenn Sie tatsächliche Typen anstelle von AnyType verwenden, erhalten Sie einen besseren Weg, um Funktionen zum Erstellen von Graphen zu nutzen und die Lesbarkeit der Diagramme zu verbessern.

Erweiterte Nutzung

Hilfsfunktionen

Lassen Sie uns den Code zur Inferenzkonstruktion in eine dedizierte Dienstprogrammfunktion extrahieren, um die Lesbarkeit und die Wiederverwendung von Code zu verbessern:

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

Daher bietet RunInference eine übersichtliche Schnittstelle, die die Ein-/Ausgaben und deren Typen angibt.

Es kann einfach wiederverwendet werden. Wenn Sie beispielsweise eine zusätzliche Modellinferenz ausführen möchten, sind nur wenige Zeilen vorhanden:

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

Außerdem müssen Sie keine Namen und Tags (InferenceCalculator, TENSORS, MODEL) duplizieren oder an dieser Stelle spezielle Konstanten einführen – diese Details werden in die RunInference-Funktion übersetzt.

Dienstprogrammklassen

Sicherlich geht es nicht nur um Funktionen. In einigen Fällen ist es vorteilhaft, Dienstprogrammklassen einzuführen, die dazu beitragen, dass der Code zur Erstellung von Diagrammen lesbarer und weniger fehleranfällig ist.

MediaPipe bietet den Rechner PassThroughCalculator, der einfach die Eingaben durchläuft:

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

Sehen wir uns den einfachen C++-Konstruktionscode an, um das obige Diagramm zu erstellen:

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

Während die pbtxt-Darstellung fehleranfällig ist (wenn viele Eingaben übergeben werden müssen), sieht C++-Code noch schlimmer aus: wiederholte leere Tags und Cast-Aufrufe. Sehen wir uns an, wie wir das mit PassThroughNodeBuilder noch verbessern können:

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

Der Code zur Konstruktion der Grafik kann jetzt so aussehen:

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

Es ist nun nicht mehr möglich, eine falsche Reihenfolge oder einen falschen Index in Ihrem Passthrough-Konstruktionscode zu verwenden. Sparen Sie sich Zeit für die Eingabe, indem Sie den Typ für Cast aus der PassThrough-Eingabe erraten.

Empfohlene und zu vermeidende Vorgehensweisen

Definieren Sie die Grafikeingaben nach Möglichkeit ganz am Anfang

Im folgenden Code gilt:

  • Es kann schwierig sein, zu erraten, wie viele Eingaben in der Grafik enthalten sind.
  • Kann insgesamt fehleranfällig und in Zukunft schwer zu verwalten sein (z. B. ist es ein richtiger Indexname? Was passiert, wenn einige Eingaben entfernt oder optional gemacht werden?)
  • Die Wiederverwendung von RunSomething ist eingeschränkt, da andere Diagramme unterschiedliche Eingaben haben können

DON'T: Beispiel für schlechten Code.

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

Definieren Sie stattdessen Ihre Grafikeingaben ganz am Anfang des Graph-Builders:

DAS SOLLTEN SIE TUN – Beispiel für guten Code.

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

Verwenden Sie std::optional, wenn Sie einen Eingabestream oder ein Nebenpaket haben, das nicht immer definiert ist, und platzieren Sie es ganz am Anfang:

DAS SOLLTEN SIE TUN – Beispiel für guten Code.

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

Definieren Sie die Grafikausgaben ganz am Ende

Im folgenden Code gilt:

  • Es kann schwierig sein, zu erraten, wie viele Ausgaben das Diagramm enthält.
  • Kann insgesamt fehleranfällig und in Zukunft schwer zu verwalten sein (z. B. ist es ein richtiger Indexname? Was passiert, wenn einige Outpus entfernt oder optional gemacht werden?)
  • Die Wiederverwendung von RunSomething ist eingeschränkt, da andere Diagramme unterschiedliche Ausgaben haben können

DON'T: Beispiel für schlechten Code.

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

Definieren Sie stattdessen die Grafikausgaben ganz am Ende des Graph-Builders:

DAS SOLLTEN SIE TUN – Beispiel für guten Code.

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

Knoten voneinander entkoppelt halten

In MediaPipe sind Paketstreams und Nebenpakete so aussagekräftig wie Verarbeitungsknoten. Alle Knoteneingabeanforderungen und -ausgabeprodukte werden in Bezug auf die Streams und Nebenpakete, die sie verbrauchen und erzeugen, klar und unabhängig ausgedrückt.

DON'T: Beispiel für schlechten Code.

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

Im obigen Code gilt Folgendes:

  • Knoten sind miteinander gekoppelt, sodass node4 die Quelle der Eingaben kennt (node1, node2, node3) und die Refaktorierung, Wartung und Wiederverwendung von Code erschwert.
    • Ein solches Nutzungsmuster ist ein Downgrade von der Proto-Darstellung, bei der Knoten standardmäßig entkoppelt sind.
  • node#.Out("OUTPUT")-Aufrufe werden dupliziert, was die Lesbarkeit beeinträchtigt, da Sie stattdessen sauberere Namen verwenden und auch einen tatsächlichen Typ angeben könnten.

Um die oben genannten Probleme zu beheben, können Sie also den folgenden Code zur Konstruktion der Grafik schreiben:

DAS SOLLTEN SIE TUN – Beispiel für guten Code.

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

Jetzt können Sie bei Bedarf einfach node1 entfernen und b als Diagrammeingabe festlegen. node2, node3 und node4 müssen nicht aktualisiert werden (wie bei der Proto-Darstellung), da sie voneinander entkoppelt sind.

Insgesamt wird mit dem obigen Code das Proto-Diagramm genauer repliziert:

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"

Darüber hinaus können Sie jetzt Dienstfunktionen zur weiteren Wiederverwendung in anderen Grafiken extrahieren:

DAS SOLLTEN SIE TUN – Beispiel für guten Code.

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

Separate Knoten für bessere Lesbarkeit

DON'T: Beispiel für schlechten Code.

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

Im Code oben lässt sich nur schwer erkennen, wo jeder Knoten beginnt und endet. Um dies zu verbessern und Ihren Code-Readern zu helfen, können Sie vor und nach jedem Knoten einfach leere Zeilen einfügen:

DAS SOLLTEN SIE TUN – Beispiel für guten Code.

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

Außerdem entspricht die obige Darstellung der Proto-Darstellung von CalculatorGraphConfig besser.

Wenn Sie Knoten in Dienstprogrammfunktionen extrahieren, sind sie bereits in Funktionen eingegrenzt und es ist klar, wo sie beginnen und enden. Daher können Sie Folgendes tun:

DAS SOLLTEN SIE TUN – Beispiel für guten Code.

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