Który wzorzec, fabryka czy abstrakcyjna fabryka, powinienem użyć do tworzenia klas obiektów rozróżnianych przez stringa?

0

Mam klasę Node i różne deskryptory dla tych węzłów.

deskryptory rozróżnia się przez std::string category:

  • Kategoria `Math': AddNode, MultiplyNode

  • Kategoria 'Parameter: FloatNode, IntNode

    category jest typu string, ponieważ chcę, aby mój edytor wyświetlał listę kategorii, a pod każdą kategorią nazwe tego deskryptory, np.

-Math
  - Add
  - Multiply
-Parameter
  - Float
  - Int

Zastanawiam się, jaki jest najlepszy sposób na utworzenie konkretnego skategoryzowanego węzła.

Mam do przejrzenia dwie implementacje

Te implementacje mają wspólne ~40 pierwszych linii kodu:

#include <string>
#include <string_view>
#include <map>
#include <functional>
#include <iostream>
#include <utility>
#include <vector>
#include <memory>

struct Node {
    Node(std::string_view text)
        : text(text) {}

    void Execute() {
        std::cout << text << "\n";
    }

private:
    std::string text;
};

//Math Nodes descriptors
struct AddNode {
    static constexpr std::string_view getText() { return "add"; }
};
struct MultiplyNode {
    static constexpr std::string_view getText() { return "multiply"; }
};
//Math Nodes descriptors

//parameter Nodes descriptors
struct FloatNode {
    static constexpr std::string_view getText() { return "float"; }
};
struct IntNode {
    static constexpr std::string_view getText() { return "int"; }
};
//parameter Nodes descriptors

Pierwsza implementacja, prosta fabryka:

struct NodeFactory {
    using Registry = std::map<std::string, std::map<std::string, std::function<std::unique_ptr<Node>()>>>;

    NodeFactory() {
        RegisterMathNodes();
        RegisterParameterNodes();
    }

    ~NodeFactory() = default;
    NodeFactory(NodeFactory&) = delete;
    NodeFactory& operator=(NodeFactory&) = delete;
    NodeFactory(NodeFactory&&) = delete;
    NodeFactory& operator=(NodeFactory&&) = delete;

    [[nodiscard]] std::unique_ptr<Node> CreateNode(const std::string& category, const std::string& type) {
        if(registry.contains(category)) {
            auto& concreteRegistry = registry[category];
            if(concreteRegistry.contains(type)) {
                return std::move(concreteRegistry[type]());
            }
            return nullptr;
        }
        return nullptr;
    }

private:
    template<typename NodeT>
    [[nodiscard]] static std::function<std::unique_ptr<Node>()> CreateNode()
    {
        return [](){ return std::make_unique<Node>(NodeT::getText()); };
    }

    #define REGISTER(container, NodeT) container[#NodeT] = CreateNode<NodeT>()

    void RegisterMathNodes() {
        auto& mathRegistry = registry["Math"];
        REGISTER(mathRegistry, AddNode);
        REGISTER(mathRegistry, MultiplyNode);
    }

    void RegisterParameterNodes() {
        auto& parameterRegistry = registry["Parameter"];
        REGISTER(parameterRegistry, FloatNode);
        REGISTER(parameterRegistry, IntNode);
    }

    Registry registry;
};

int main() {
    NodeFactory factory;
    std::vector<std::shared_ptr<Node>> nodes;
    nodes.push_back(factory.CreateNode("Math", "AddNode"));
    nodes.push_back(factory.CreateNode("Parameter", "FloatNode"));
    nodes.push_back(factory.CreateNode("Math", "MultiplyNode"));
    nodes.push_back(factory.CreateNode("Math", "AddNode"));

    for(const auto& node : nodes) {
        node->Execute();
    }

    return 0;
}

https://godbolt.org/

druga implementacja, każda kategoria ma osobną abryke:

struct TNodeFactory {
    using Registry = std::map<std::string, std::map<std::string, std::function<std::unique_ptr<Node>()>>>;
    friend struct AbstractNodeFactory;

    TNodeFactory() = default;
    TNodeFactory(TNodeFactory&) = delete;
    TNodeFactory& operator=(TNodeFactory&) = delete;
    TNodeFactory(TNodeFactory&&) = delete;
    TNodeFactory& operator=(TNodeFactory&&) = delete;

    virtual ~TNodeFactory() = default;

    [[nodiscard]] std::unique_ptr<Node> CreateNode(const std::string& type) {
        const std::string& category = getCategory();
        if(registry.contains(category)) {
            auto& concreteRegistry = registry[category];
            if(concreteRegistry.contains(type)) {
                return std::move(concreteRegistry[type]());
            }
            return nullptr;
        }
        return nullptr;
    }

 protected:
    [[nodiscard]] virtual std::string getCategory() = 0;

    Registry registry;
};

namespace Internal
{
    template<typename NodeT>
    [[nodiscard]] static std::function<std::unique_ptr<Node>()> CreateNode()
    {
        return [](){ return std::make_unique<Node>(NodeT::getText()); };
    }
}
#define REGISTER(container, NodeT) container[#NodeT] = Internal::CreateNode<NodeT>()
struct MathNodeFactory : virtual TNodeFactory
{
    static constexpr std::string category = "Math";
    
    MathNodeFactory() {
        auto& mathRegistry = registry[getCategory()];
        REGISTER(mathRegistry, AddNode);
        REGISTER(mathRegistry, MultiplyNode);
    }

    ~MathNodeFactory() override = default;

private:
    std::string getCategory() override { return category; }
};

struct ParameterNodeFactory : virtual TNodeFactory
{
    static constexpr std::string category = "Parameter";

    ParameterNodeFactory() {
        auto& parameterRegistry = registry[getCategory()];
        REGISTER(parameterRegistry, FloatNode);
        REGISTER(parameterRegistry, IntNode);
    }

    ~ParameterNodeFactory() override = default;
private:
    std::string getCategory() override { return category; }
};

struct AbstractNodeFactory {
    AbstractNodeFactory()
    {
        factoriesRegistry[MathNodeFactory::category] = std::make_unique<MathNodeFactory>();
        factoriesRegistry[ParameterNodeFactory::category] = std::make_unique<ParameterNodeFactory>();
    }

    ~AbstractNodeFactory() = default;
    AbstractNodeFactory(AbstractNodeFactory&) = delete;
    AbstractNodeFactory& operator=(AbstractNodeFactory&) = delete;
    AbstractNodeFactory(AbstractNodeFactory&&) = delete;
    AbstractNodeFactory& operator=(AbstractNodeFactory&&) = delete;

    [[nodiscard]] std::unique_ptr<Node> CreateNode(const std::string& category, const std::string& type) {
        if(factoriesRegistry.contains(category)) {
            auto concreteFactory = factoriesRegistry[category].get();
            return concreteFactory->CreateNode(type);
        }
        return nullptr;
    }
private:
    std::map<std::string, std::unique_ptr<TNodeFactory>> factoriesRegistry;
};

int main() {
    AbstractNodeFactory factory;
    std::vector<std::shared_ptr<Node>> nodes;
    nodes.push_back(factory.CreateNode("Math", "AddNode"));
    nodes.push_back(factory.CreateNode("Parameter", "FloatNode"));
    nodes.push_back(factory.CreateNode("Math", "MultiplyNode"));
    nodes.push_back(factory.CreateNode("Math", "AddNode"));

    for(const auto& node : nodes) {
        node->Execute();
    }

    return 0;
}

https://godbolt.org/z/Wf1vvn9Px

Funkcja CreateNode jest tylko minimalnym przykładem, a rzeczywista implementacja jest bardziej skomplikowana. Chciałbym się tutaj tylko skupić nad wyborem odpowiedniego wzorca.

Której implementacji powinienem użyć? Czy może, w ogóle źle podszedłem do tematu i powinienem inaczej to zaimplementować?

2

Prostszy a wystarczający.Jesli wystarcza zwykła fabryka to bym jej użył

0

Co ty właściwie chcesz tutaj osiągnąć? Przekombinowany wydaje mi się ten kod. Nie widzę w ogóle powodu, żeby nawet robić jakieś fabryki. To mi wygląda jak kod, który buduje coś na kształt AST. Wtedy, dla mnie jest to przypadek na użycie dispatch table przyporządkowującej funkcje, żadnego dziedziczenia nawet.

0

Zastanów się, czy nie chcesz użyć std::variant i statycznego polimorfizmu opartego na nim.

Ale ogółem zgadzam się z przedmówcami, strasznie to przekombinowane.

0
elwis napisał(a):

Co ty właściwie chcesz tutaj osiągnąć? Przekombinowany wydaje mi się ten kod. Nie widzę w ogóle powodu, żeby nawet robić jakieś fabryki. To mi wygląda jak kod, który buduje coś na kształt AST. Wtedy, dla mnie jest to przypadek na użycie dispatch table przyporządkowującej funkcje, żadnego dziedziczenia nawet.

Dobra, chciałem wrzucić tylko zarys żeby nie walnąć niepotrzebnej ściany kodu, ale może jak pokaże więcej to będzie wam łatwiej coś mi podpowiedzieć.

Chce zrobić Node Graph Editor, gdzie Node będzie rysowany z jakimiś Pinami oraz połączenia między pinem jednego noda, a pinem drugiego, Link/Edge. Jest klasa NodeGraph, która jest ownerem Nodeów, Pinów i Linków. NodeGraph również pozwala na dodawanie konkretnego Nodea, poprzez właśnie tę fabrykę. Linków tutaj nie będę przedstawiał bo mija się to z celem.

struct Pin {
  Pin(std::string_view name, std::uint32_t id, std::uint32_t nodeID, float value)
    : name(name), id(id), nodeID(nodeID), value(value) {}

  std::string name{};
  std::uint32_t id{};
  std::uint32_t nodeID{};
  float value = 0.0f;
};

struct Node {
  void AddInput(const Pin& pin) {
    inputs.push_back(pin.id);
  }

  void AddOutput(const Pin& pin) {
    outputs.push_back(pin.id);
  }

  std::string name;
  std::vector<std::uint32_t> inputs;
  std::vector<std::uint32_t> outputs;
};

struct NodeGraph {
  NodeGraph() = default;
  ~NodeGraph() = default;
  NodeGraph(NodeGraph&) = delete;
  NodeGraph& operator=(NodeGraph&) = delete;
  NodeGraph(NodeGraph&&) = delete;
  NodeGraph& operator=(NodeGraph&&) = delete;

  Node* CreateNode(const std::string& category, const std::string& type)
  {
    return nodeFactory.CreateNode(category, type);
  }

//private
  Node* CreateNodeInternal(std::string_view name)
  {
    auto node = ArenaAllocate<Node>(name);
    nodes.push_back(node);
    return node;
  }
  
  void AddInputInternal(std::string_view name, Node& owner, float defaultValue)
  {
    auto pin = ArenaAllocate<Pin>(name, pins.size(), owner.getID(), defaultValue);
    pins.push_back(pin);
    owner.AddInput(*pin);
  }
  
  void AddOutputInternal(std::string_view name, Node& owner, float defaultValue)
  {
    auto pin = ArenaAllocate<Pin>(name, pins.size(), owner.getID(), defaultValue);
    pins.push_back(pin);
    owner.AddOutput(*pin);
  }

  template<typename T, size_t alignment = alignof(T), typename ... Args>
  T* ArenaAllocate(Args&& ... args)
  {
    void* const p = linearArena.allocate(sizeof(T), alignment);
	return p ? new(p) T(std::forward<Args>(args)...) : nullptr;
  }

  std::pmr::monotonic_buffer_resource linearArena{1024 * 1024};
  std::pmr::vector<Node*> nodes{ &linearArena };
  std::pmr::vector<Pin*> pins{ &linearArena };

  NodeFactory nodeFactory{*this};
};

AddNode, FloatNode itd to są deskryptory, one mają tylko opisywać strukture tego prawdziwego Node.

do opisywania tego jest stworzona klasa TypeDescriptor

template<typename T> struct TypeDescriptor;

#ifndef DESCRIBED
#define DESCRIBED(Class, ...) template<>\
	struct TypeDescriptor<Class>\
	{\
		static constexpr std::string_view memberString{#__VA_ARGS__};                                                                                               \
		static constexpr std::string_view delimiter{ ", " };                                                                                                        \
		static constexpr std::string_view namespaceName{ #Class, std::string_view(#Class).find_last_of(':') };                                                      \
		static constexpr std::string_view className{ #Class };                                                                                                      \
		static constexpr size_t membersCount = getNumberOfTokens(memberString, delimiter);                                                                          \
		static constexpr std::array<std::string_view, membersCount> memberNames = RemoveNamespace<membersCount>(SplitString<membersCount>(memberString, delimiter));\
		template<typename T, typename F>                                                                                                                            \
		constexpr static auto apply(F f, T& v)                                                                                                                      \
		{                                                                                                                                                           \
			return f(v.*__VA_ARGS__);                                                                                                                               \
		}                                                                                                                                                           \
		template<typename F>                                                                                                                                        \
		constexpr static auto applyToStaticType(F f)                                                                                                                \
		{                                                                                                                                                           \
			return f(__VA_ARGS__);                                                                                                                                  \
		}                                                                                                                                                           \
	};
#endif

W tym są funkcje pomocnicze jak RemoveNamespace, getNumberOfTokens, szkoda zachodu je tu wrzucać.
I implementacja już konkretnego deskryptora

struct AddNode {
  float* a;
  float* b;
  float out;
};
DESCRIBED(AddNode, &AddNode::a, &AddNode::b, &AddNode::out);

No i teraz, mając to wszystko, biorąc załóżmy tą najprostszą fabrykę

struct NodeGraph;
struct NodeFactory {
    using Registry = std::map<std::string, std::map<std::string, std::function<Node*()>>>;

    NodeFactory(NodeGraph& graph) : graph(graph) {
        RegisterMathNodes();
        RegisterParameterNodes();
    }

    ~NodeFactory() = default;
    NodeFactory(NodeFactory&) = delete;
    NodeFactory& operator=(NodeFactory&) = delete;
    NodeFactory(NodeFactory&&) = delete;
    NodeFactory& operator=(NodeFactory&&) = delete;

    [[nodiscard]] Node* CreateNode(const std::string& category, const std::string& type) {
        if(registry.contains(category)) {
            auto& concreteRegistry = registry[category];
            if(concreteRegistry.contains(type)) {
                return std::move(concreteRegistry[type]());
            }
            return nullptr;
        }
        return nullptr;
    }

private:
    void RegisterMathNodes();
    void RegisterParameterNodes();

    NodeGraph& graph;
    Registry registry;
};

Plik NodeFactory.cpp

#include "NodeFactory.h"
#include "NodeGraph.h"

template <typename>
struct CatchType {};

template<class TProcNode, typename TMember>
constexpr bool CreatePinImpl(NodeGraph& graph, Node& node, std::string_view pinName, CatchType<void>)
{
	static_assert(false);
	return false;
}

template<typename TMember>
constexpr bool CreatePinImpl(NodeGraph& graph, Node& node, std::string_view pinName, CatchType<TMember>)
{
	using TMemberRaw = std::remove_pointer_t<TMember>;
	static_assert(!std::is_void_v<TMemberRaw>);

	const bool isInput = std::is_pointer_v<TMember>;

	if (isInput) {
		graph.AddInputInternal(pinName, node, 1.0f);
	} else {
		graph.AddOutputInternal(pinName, node, 1.0f);
	}

	return true;
};

template<typename TMemberPtr>
constexpr bool CreatePin(NodeGraph& graph, Node& node, std::string_view pinName)
{
	using TMember = typename member_pointer::return_type<TMemberPtr>::type;

	const bool isArray = std::is_array_v<TMember>;
	using TMemberDecay = std::conditional_t<isArray, std::remove_pointer_t<std::decay_t<TMember>>, std::decay_t<TMember>>;
	return CreatePinImpl<TMemberDecay>(graph, node, pinName, CatchType<TMemberDecay>{});
}

template<typename NodeT>
static constexpr Node* CreateNodeFromDescriptor(NodeGraph& graph)
{
  using TDescriptor = TypeDescriptor<NodeT>;
  auto node = graph.CreateNodeInternal(TDescriptor::className.data());
  TDescriptor::applyToStaticType([&node, &graph](const auto&... members) {
    auto unpack = [&node, &graph, memberIndex = 0](auto memberPtr) mutable {
      using TMemberPtr = decltype(memberPtr);
      return CreatePin<TMemberPtr>(graph, *node, TDescriptor::memberNames[memberIndex++]);
    };

    return (unpack(members) && ...);
  });

  return node;
}

template<typename NodeT>
static std::pair<std::string, std::function<Node*()>> CreateRegistryEntry(NodeGraph& graph, std::string_view typeName)
{
  std::string name = CreateUserFriendlyTypeName(typeName);
  return { std::move(name), [&graph] { return CreateNodeFromDescriptor<NodeT>(graph); } };
}

#define REGISTER(graph, container, TNodeType) container.emplace(CreateRegistryEntry<TNodeType>(graph, #TNodeType))
void NodeFactory::RegisterMathNodes() {
  auto& mathRegistry = registry["Math"];
  REGISTER(graph, mathRegistry, AddNode);
  REGISTER(graph,mathRegistry, MultiplyNode);
}

void NodeFactory::RegisterParameterNodes() {
  auto& parameterRegistry = registry["Parameter"];
  REGISTER(graph,parameterRegistry, FloatNode);
  REGISTER(graph,parameterRegistry, IntNode);
}

Starałem się trochę okroić to wszystko ale i tak dużo tego wyszło.

i jak już uda wam się przebrnąć przez ścianę kodu to wtedy NodeEditor tworzy sobie NodeGraph i renderuje tam wszystko co potrzeba
a z tą listą kategorii to chodziło mi o coś takiego:
screenshot-20221202140602.png

wtedy gdy user kliknie sobie np. CombineRGB to wtedy na ekranie pojawia się odpowiedni node, stworzony przez NodeFactory na podstawie właśnie category, tzn. Utility i type tzn. CombineRGB

1 użytkowników online, w tym zalogowanych: 0, gości: 1