Petites fonctions vs maintien des fonctionnalités dépendantes dans la même fonction

15

J'ai une classe qui met en place un tableau de nœuds et les connecte les uns aux autres dans une structure de type graphique. Est-il préférable de:

  1. Conservez la fonctionnalité pour initialiser et connecter les nœuds dans une seule fonction
  2. Avoir la fonctionnalité d'initialisation et de connexion dans deux fonctions différentes (et avoir un ordre dépendant sur lequel les fonctions doivent être appelées - mais gardez à l'esprit que ces fonctions sont privées.)

Méthode 1: (mauvaise dans la mesure où une fonction fait deux choses, MAIS elle conserve les fonctionnalités dépendantes regroupées - les nœuds ne doivent jamais être connectés sans avoir été initialisés au préalable.)

init() {
    setupNodes()
}

private func setupNodes() {
    // 1. Create array of nodes
    // 2. Go through array, connecting each node to its neighbors 
    //    according to some predefined constants
}

Méthode 2: (Mieux dans le sens où il est auto-documenté, MAIS connectNodes () ne doit jamais être appelé avant setupNodes (), donc toute personne travaillant avec les internes de la classe doit connaître cet ordre.)

init() {
    setupNodes()
}

private func setupNodes() {
    createNodes()
    connectNodes()
}

private func createNodes() {
    // 1. Create array of nodes
}

private func connectNodes() {
    // 2. Go through array, connecting each node to its neighbors 
    //    according to some predefined constants
}

Excité d'entendre des pensées.

mcfroob
la source
Une façon de résoudre ce problème en définissant des objets intermédiaires qui ne peuvent être utilisés que pour créer les objets finaux. Ce n'est pas toujours la bonne solution mais elle est utile si l'utilisateur de l'interface doit manipuler un état intermédiaire d'une manière ou d'une autre.
Joel Cornett

Réponses:

23

Le problème que vous traitez est appelé couplage temporel

Vous avez raison de vous inquiéter de la compréhension de ce code:

private func setupNodes() {
    createNodes();
    connectNodes();
}

Je peux deviner ce qui se passe là-bas, mais dites-moi si cela rend les choses un peu plus claires:

private func setupNodes() {
    self.nodes = connectNodes( createNodes() );
}

Cela a l'avantage supplémentaire d'être moins couplé à la modification des variables d'instance, mais pour moi, être lisible est le numéro un.

Cela rend connectNodes()explicite la dépendance de nœuds.

candied_orange
la source
1
Merci pour le lien. Comme mes fonctions sont privées et appelées depuis le constructeur - init () dans Swift - je ne pense pas que mon code serait aussi mauvais que les exemples que vous avez liés (il serait impossible pour un client externe d'instancier une instance avec un variable d'instance nulle), mais j'ai une odeur similaire.
mcfroob
1
Le code que vous avez ajouté est plus lisible, je vais donc refactoriser dans ce style.
mcfroob
10

Fonctions distinctes , pour deux raisons:

1. Les fonctions privées sont privées pour exactement cette situation.

Votre initfonction est publique, et c'est son interface, son comportement et sa valeur de retour que vous devez vous soucier de protéger et de modifier. Le résultat que vous attendez de cette méthode sera le même quelle que soit l'implémentation que vous utilisez.

Puisque le reste de la fonctionnalité est caché derrière ce mot-clé privé, il peut être implémenté comme vous le souhaitez ... vous pouvez donc aussi le rendre agréable et modulaire, même si un bit dépend de l'autre appelé en premier.

2. La connexion de nœuds entre eux peut ne pas être une fonction privée

Que se passe-t-il si à un moment donné vous souhaitez ajouter d'autres nœuds à la baie? Détruisez-vous la configuration que vous avez maintenant et réinitialisez-la complètement? Ou ajoutez-vous des nœuds à la baie existante, puis réexécutez-vous connectNodes?

Peut éventuellement connectNodesavoir une réponse sensée si le tableau de nœuds n'a pas encore été créé (lever une exception? Retourner un ensemble vide? Vous devez décider ce qui a du sens pour votre situation).

Jen
la source
Je pensais de la même manière que 1, et je pourrais lancer une exception ou quelque chose si les nœuds n'étaient pas initialisés, mais ce n'est pas particulièrement intuitif. Merci pour la réponse.
mcfroob
4

Vous pouvez également trouver (selon la complexité de chacune de ces tâches) que c'est une bonne couture pour diviser une autre classe.

(Je ne sais pas si Swift fonctionne de cette façon mais un pseudo-code :)

class YourClass {
    init(generator: NodesGenerator) {
        self.nodes = connectNodes(generator.make())
    }
    private func connectNodes() {

    }
}

class NodesGenerator {
    public func make() {
        // Return some nodes from storage or make new ones
    }
}

Cela sépare les responsabilités de la création et de la modification des nœuds en classes distinctes: NodeGeneratorne se soucie que de créer / récupérer des nœuds, tandis YourClassque se soucie uniquement de connecter les nœuds qui lui sont donnés.

Willoller
la source
2

En plus d'être le but exact des méthodes privées, Swift vous donne la possibilité d'utiliser des fonctions internes.

Les méthodes internes sont parfaites pour les fonctions qui n'ont qu'un seul site d'appel, mais ont l'impression qu'elles ne justifient pas d'être des fonctions privées distinctes.

Par exemple, il est très courant d'avoir une fonction "entrée" récursive publique, qui vérifie les conditions préalables, configure certains paramètres et délègue à une fonction récursive privée qui fait le travail.

Voici un exemple de ce à quoi cela pourrait ressembler dans ce cas:

init() {
    self.nodes = setupNodes()

    func setupNodes() {
        var nodes = createNodes()
        connect(Nodes: nodes)
    }

    private func createNodes() -> [Node]{
        // 1. Create array of nodes
    }

    func connect(Nodes: [Node]) {
        // 2. Go through array, connecting each node to its neighbors 
        //    according to some predefined constants
    }
}

Faites attention à la façon dont j'utilise les valeurs de retour et les paramètres pour faire circuler les données, plutôt que de muter un état partagé. Cela rend le flux de données beaucoup plus évident à première vue, sans avoir à sauter dans l'implémentation.

Alexander - Rétablir Monica
la source
0

Chaque fonction que vous déclarez entraîne le fardeau d'ajouter de la documentation et de la généraliser afin qu'elle soit utilisable par d'autres parties du programme. Il porte également le fardeau de comprendre comment d'autres fonctions du fichier peuvent l'utiliser pour quelqu'un qui lit le code.

Si toutefois il n'est pas utilisé par d'autres parties de votre programme, je ne l'exposerais pas comme une fonction distincte.

Si votre langue le prend en charge, vous pouvez toujours avoir une fonction-une-chose en utilisant des fonctions imbriquées

function setupNodes ()  {
  function createNodes ()  {...} 
  function connectNodes ()  {...}
  createNodes() 
  connectNodes() 
} 

Le lieu de la déclaration est très important, et dans l'exemple ci-dessus, il est clair sans avoir besoin d'indices supplémentaires que les fonctions internes sont destinées à être utilisées uniquement dans le corps de la fonction externe.

Même si vous les déclarez en tant que fonctions privées, je suppose qu'elles sont toujours visibles pour l'ensemble du fichier. Vous devez donc les déclarer proches de la déclaration de la fonction principale et ajouter de la documentation qui précise qu'ils ne doivent être utilisés que par la fonction externe.

Je ne pense pas que vous ayez à faire strictement l'un ou l'autre. La meilleure chose à faire varie au cas par cas.

Le décomposer en plusieurs fonctions ajoute certainement une surcharge pour comprendre pourquoi il y a 3 fonctions et comment elles fonctionnent toutes les unes avec les autres, mais si la logique est complexe, cette surcharge supplémentaire peut être bien inférieure à la simplicité introduite par la décomposition de la logique complexe. en parties plus simples.

Peeyush Kushwaha
la source
Option intéressante. Comme vous le dites, je pense que ce pourrait être un peu déroutant de savoir pourquoi la fonction a été déclarée comme ça, mais cela garderait la dépendance de la fonction bien contenue.
mcfroob
Pour répondre à certaines des incertitudes de cette question: 1) Oui, Swift prend en charge les fonctions internes, et 2) Il a deux niveaux de "privé". privateautorise l'accès uniquement dans le type englobant (struct / class / enum), tandis qu'il fileprivateautorise l'accès à l'ensemble du fichier
Alexander - Reinstate Monica