[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:
Following up on our discussion in dev meeting:

tldr: Both scikit-survival and quantile-forests generalize RF in different ways compared to what I am proposing. I am proposing generalizations for the split function. Scikit-survival proposes a generalization of the criterion. Quantile-forests proposes a generalization of how the leaf nodes are set, which is also something honest forests generalizes.

#### Scikit-survival

IIUC, this package cannot subclass `BaseDecisionTree` rn because of fundamental differences in `y` variable implemented in scikit-survival. The Criterion changes, but that can be passed in directly to the Decision Tree class. Another change is the structure of the `y` labels variable. In scikit-survival, each sample in `y` is a tuple consisting of a `binary event indicator as first field, and time of event or time of censoring as
second field`. Basically, it is whether or not sample survived and then the length of survival. Rn `event_times` is directly passed into `LogRankCriterion` [here](https://github.com/sebp/scikit-survival/blob/52c713dc8e3beb266f9cad7e6785dce5fa282f01/sksurv/tree/tree.py#L217), which prevents it from using `BaseDecisionTree`.

I think scikit-survival can benefit from a refactoring if we enable also a private `_set_criterion` function, which would enable subclassing of `BaseDecisionTree` that alters the Criterion class as well. For example:

```
def _set_criterion(self, X, y):
     # sets criterion class given metadata from the input X and y
```

This could potentially allow more exotic criterions. However, I can see this not being 100% necessary.

###### skurv refs
See the tree code: https://github.com/sebp/scikit-survival/blob/52c713dc8e3beb266f9cad7e6785dce5fa282f01/sksurv/tree/tree.py#L217-L230

And relevant Cython criterion that subclasses scikit-learn: https://github.com/sebp/scikit-survival/blob/master/sksurv/tree/_criterion.pyx

#### Quantile Forest

Quantile random forest (QRF) implements the entire forest using Cython. Fundamentally, the QRF needs to store information about all observations in its leaf nodes, rather than just the mean of the labels, which is currently done in scikit-learn's RF.

This refactoring unfortunately would not play a role here because it operates at generalizing the splitting. 

However, QRF and honest forests are candidates that probably would benefit from a generalization of setting the leaves within the RF code of scikit-learn. Both QRF and honest forests use the leaves differently than that of RF, but both QRF and honest forests perform the splitting in the same way. The refactoring here would simply help QRF/honest-forests take advantage of more exotic splitting functions.

cc: @ogrisel @jjerphan @thomasjpfan Feel free to add anyone I missed.

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