Skip to content

Commit 0c0d746

Browse files
committed
Inline explanation from #210
1 parent 74f2535 commit 0c0d746

File tree

1 file changed

+116
-2
lines changed

1 file changed

+116
-2
lines changed

doc/migration.md

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ Change your class declarations so that they no longer inherit from the generated
2727

2828
In cases where the return type has changed from `std::shared_ptr<service::Object>` to `std::shared_ptr<object::Interface>` or `std::shared_ptr<object::Union>`, wrap the concrete type in `std::make_shared<T>(...)` for the polymorphic type and return that.
2929

30-
If your implementation is tightly coupled with the object hierarchy from the schema, there are some strategies for separating them discussed in [#210](https://github.com/microsoft/cppgraphqlgen/issues/210).
31-
3230
## Simplify Field Accessor Signatures
3331

3432
Examine the generated object types and determine what the expected return type is from each field getter method. Replace the `service::FieldResult` wrapped type with the expected return type (possibly including the awaitable wrapper).
@@ -39,6 +37,122 @@ Parameters can be passed as a `const&` reference, a `&&` r-value reference, or b
3937

4038
Remove any unused `service::FieldParams` arguments. If your method does not take that as the first parameter, the generated template method will drop it and pass the rest of the expected arguments to your method.
4139

40+
## Decoupling Implementation Types from the Generated Types
41+
42+
If your implementation is tightly coupled with the object hierarchy from the schema, here's an example of how you might decouple them. Let's assume that you have a schema that looks something like this:
43+
```graphql
44+
interface Node
45+
{
46+
id: ID!
47+
}
48+
49+
type NodeTypeA implements Node
50+
{
51+
id: ID!
52+
# More fields specific to NodeTypeA...
53+
}
54+
55+
type NodeTypeB implements Node
56+
{
57+
id: ID!
58+
# More fields specific to NodeTypeB...
59+
}
60+
61+
# ...and so on for NodeTypeC, NodeTypeD, etc.
62+
```
63+
If you want a collection of `Node` interface objects, the C++ implementation using inheritance in prior versions might look something like:
64+
```c++
65+
class NodeTypeA : public object::NodeTypeA
66+
{
67+
// Implement the field accessors with an exact match for the virtual method signature...
68+
service::FieldResult<response::IdType> getId(service::FieldParams&&) const override;
69+
};
70+
71+
class NodeTypeB : public object::NodeTypeB
72+
{
73+
// Implement the field accessors with an exact match for the virtual method signature...
74+
service::FieldResult<response::IdType> getId(service::FieldParams&&) const override;
75+
};
76+
77+
std::vector<std::shared_ptr<service::Object>> nodes {
78+
std::make_shared<NodeTypeA>(),
79+
std::make_shared<NodeTypeB>(),
80+
// Can insert any sub-class of service::Object...
81+
};
82+
```
83+
It's up to the you to make sure the `nodes` vector in this example only contains objects which actually implement the `Node` interface. If you want to do something more sophisticated like performing a lookup by `id`, you'd either need to request that before inserting an element and up-casting to `std::shared_ptr<service::Object>`, or you'd need to preserve the concrete type of each element, e.g. in a `std::variant` to be able to safely down-cast to the concrete type.
84+
85+
As of 4.x, the implementation might look more like this:
86+
```c++
87+
class NodeTypeImpl
88+
{
89+
public:
90+
// Need to override this in the sub-classes to construct the correct sub-type wrappers.
91+
virtual std::shared_ptr<object::Node> make_node() const = 0;
92+
93+
const response::IdType& getId() const noexcept final;
94+
95+
// Implement/declare any other accessors you want to use without downcasting...
96+
97+
private:
98+
const response::IdType _id;
99+
};
100+
101+
class NodeTypeA
102+
: public NodeTypeImpl
103+
, public std::enable_shared_from_this<NodeTypeA>
104+
{
105+
public:
106+
// Convert to a type-erased Node.
107+
std::shared_ptr<object::Node> make_node() const final
108+
{
109+
return std::make_shared<object::Node>(std::make_shared<object::NodeTypeA>(shared_from_this()));
110+
}
111+
112+
// Implement NodeTypeA and any NodeTypeImpl override accessors...
113+
};
114+
115+
class NodeTypeB
116+
: public NodeTypeImpl
117+
, public std::enable_shared_from_this<NodeTypeB>
118+
{
119+
public:
120+
// Convert to a type-erased Node.
121+
std::shared_ptr<object::Node> make_node() const final
122+
{
123+
return std::make_shared<object::Node>(std::make_shared<object::NodeTypeB>(shared_from_this()));
124+
}
125+
126+
// Implement NodeTypeB and any NodeTypeImpl override accessors...
127+
};
128+
129+
std::vector<std::shared_ptr<NodeTypeImpl>> nodes {
130+
std::make_shared<NodeTypeA>(),
131+
std::make_shared<NodeTypeB>(),
132+
// Can only insert sub-classes of NodeTypeImpl...
133+
};
134+
135+
std::vector<std::shared_ptr<object::Node>> wrap_nodes()
136+
{
137+
std::vector<std::shared_ptr<object::Node>> result(nodes.size());
138+
139+
std::transform(nodes.cbegin(), nodes.cend(), result.begin(), [](const auto& node) {
140+
return node
141+
? node->make_node()
142+
: std::shared_ptr<object::Node> {};
143+
});
144+
145+
return result;
146+
}
147+
```
148+
This has several advantages over the previous version.
149+
150+
- You can declare your own inheritance heirarchy without any constraints inherited from `service::Object`, such as already inheriting from `std::enable_shared_from_this<service::Object>` and defininig `shared_from_this()` for that type.
151+
- You can add your own common implementation for the interface methods you want, e.g. `NodeTypeImpl::getId`.
152+
- Best of all, you no longer need to match an exact method signature to override the `object::NodeType*` accessors. For example, `NodeTypeImpl::getId` uses a more efficient return type, does not require a `service::FieldParams` argument (which is likely ignored anyway), and it can be `const` and `noexcept`. All of that together means you can use it as an internal accessor from any of these types as well as the field getter implementation.
153+
154+
The type erased implementation gives you a lot more control over your class hierarchy and makes it easier to use outside of the GraphQL service.
155+
42156
## CMake Changes
43157

44158
By default, earlier versions of `schemagen` would generate a single header and a single source file for the entire schema, including the declaration and definition of all of the object types. For any significantly complex schema, this source file could get very big. Even the `Today` sample schema was large enough to require a special `/bigobj` flag when compiling with `MSVC`. It also made incremental builds take much longer if you only added/removed/modified a few types, because the entire schema needed to be recompiled.

0 commit comments

Comments
 (0)