[RFC] Modularize the tree class in both Python and Cython to enable easy extensions

### Describe the workflow you want to enable

As we are waiting for a reviewer to review #22754 , @thomasjpfan suggested we just move forward with our goals of creating a package of more exotic tree splits. E.g. https://arxiv.org/abs/1909.11799. While we wait for reviewers, the suggestion is to make a package within `scikit-learn-contrib`. 

Although we would like for #22754 to be eventually merged into scikit-learn, we understand reviewer backlog is an issue. To move forward while reviews occur, we would need to subclass existing scikit-learn code. Ideally, we would like to introduce minor refactoring changes that would make this task significantly easier.

We would like to subclass directly from scikit-learn without requiring us to keep an up-to-date fork of scikit-learn with all the bug fixes and maintenance that the dev team here does. We can limit this if we modularize the Python/Cython functions inside the `sklearn/tree` module.

### Describe your proposed solution

I am proposing two refactoring modifications that have no impact on the performance of the current tree estimators in scikit-learn.

1. Refactor the `BaseDecisionTree` Python class to have the following functions that can be overridden in a subclass:

- `_set_tree_class`: sets the Tree Cython class that the Python API uses
- `_set_splitter`: sets the Splitter Cython class that the Python API uses

For example, this makes the subclassing of BaseDecisionTree cleaner: https://github.com/scikit-learn/scikit-learn/blob/de06afa78458d773d7c0482ff17a55a235d0886b/sklearn/tree/_classes.py#L410-L416.

2. Refactor the `Tree` Cython class to have the following functions:

- `_set_node_values`: transfers split node values to the storage node
- `_compute_feature_value`: uses the storage node and the input data to compute the feature value to split on

For example, see https://github.com/scikit-learn/scikit-learn/blob/de06afa78458d773d7c0482ff17a55a235d0886b/sklearn/tree/_tree.pyx#L770-L787.

3. Refactor the `TreeBuilder` Cython class to pass around a `Split` pointer, rather than the struct itself

This will enable someone to use C-level functions to pass around another struct with a similar structure as `Split`.

For example, see https://github.com/scikit-learn/scikit-learn/blob/de06afa78458d773d7c0482ff17a55a235d0886b/sklearn/tree/_tree.pyx#L193.

### Describe alternatives you've considered, if relevant

Alternatives would require maintaining a copy of the `sklearn/tree` module and keep it up-to-date w/ sklearn changes. If this was just one Cython file, I would say it is possible, but the necessary ingredients span some of the underlying private API, making this a very time-consuming task. Introducing modularity into the private API that does not impact existing performance, therefore seems to be the best path forward?

Moreover, by introducing these refactoring changes, #22754 has a smaller diff and lower-cost to review.

### Additional context

**#22754 demonstrates that there is no performance regression, or issues w/ existing DecisionTree, or RandomForest when introducing these changes.**

COMMENT:
Thanks Thomas! Adding some comments to augment the discussion. The high level goal of all 3 I would say is to enable more flexibility wrt Python/Cython code to allow maximal reusability.

> I know I suggested it, but item 3 with the `pointer_size` is kind of hacky. I am okay with it being used internally, but I am uneasy about it as a third-party extension point. (I'm around +0.3)

After you showed me this trick, I perused more and this actually seems to be an "accepted" standard for "inheritance" of structs in C. Unfortunately, Cython takes after C in this regard for structs and not C++. Perhaps some proper documentation inline code to reduce technical confusion of anyone in the future reading the Tree code, so that way they don't need to have extensive C knowledge.

> Stepping back, the main issue is that the trees were designed around axis-aligned splits. For third party libraries that only need axis-aligned splits, it is easy to subclass. For scikit-learn, do we want to add these extension points to allow for non axis-aligned splits, when they are not in the library itself? I am slightly in favor at +0.5.

I somewhat agree with the point here. The extension points enable someone to subclass the existing code more flexibly I would argue rather than just enabling non axis-aligned splits. Say someone wants to define their own axis-aligned splitter and provide a Python API. e.g. some function that chooses splits using some graph traversal instead of at random. I'm just postulating here on ideas I've had in the past for improving RF. But even this hypothetical workflow is not possible in the current setup.

Labels: New Feature, module:ensemble, module:tree, cython, Needs Decision - Include Feature, Refactor