Funciones pequeñas versus mantener la funcionalidad dependiente en la misma función

15

Tengo una clase que configura una matriz de nodos y los conecta entre sí en una estructura similar a un gráfico. ¿Es mejor:

  1. Mantenga la funcionalidad para inicializar y conectar los nodos en una función
  2. Tenga la funcionalidad de inicialización y conexión en dos funciones diferentes (y tenga un orden dependiente en el que deben llamarse las funciones, aunque tenga en cuenta que estas funciones son privadas).

Método 1: (Malo en que una función está haciendo dos cosas, PERO mantiene la funcionalidad dependiente agrupada: los nodos nunca deben conectarse sin inicializarse primero).

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étodo 2: (Mejor en el sentido de que es autodocumentado, PERO connectNodes () nunca debe llamarse antes que setupNodes (), por lo que cualquier persona que trabaje con los componentes internos de la clase debe saber sobre este orden).

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
}

Emocionado de escuchar cualquier pensamiento.

Mcfroob
fuente
Una forma de resolver esto definiendo objetos intermedios que solo se pueden usar para crear los objetos finales. Esta no siempre es la solución correcta, pero es útil si el usuario de la interfaz necesita manipular un estado intermedio de alguna manera.
Joel Cornett

Respuestas:

23

El problema con el que está lidiando se llama acoplamiento temporal

Tiene razón en preocuparse por lo comprensible que es este código:

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

Puedo adivinar lo que está sucediendo allí, pero dime si esto aclara un poco más lo que está sucediendo:

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

Esto tiene el beneficio adicional de estar menos acoplado a la modificación de variables de instancia, pero para mí ser legible es el número uno.

Esto hace que connectNodes()la dependencia de los nodos sea explícita.

naranja confitada
fuente
1
Gracias por el enlace. Dado que mis funciones son privadas y se llaman desde el constructor - init () en Swift - no creo que mi código sea tan malo como los ejemplos que vinculó (sería imposible que un cliente externo instanciara una instancia con un variable de instancia nula), pero tengo un olor similar.
mcfroob
1
El código que agregó es más legible, por lo que lo refactorizaré con ese estilo.
mcfroob
10

Funciones separadas , por dos razones:

1. Las funciones privadas son privadas para exactamente esta situación.

Su initfunción es pública, y su interfaz, comportamiento y valor de retorno es lo que necesita para preocuparse por proteger y cambiar. El resultado que espera de ese método será el mismo, independientemente de la implementación que utilice.

Dado que el resto de la funcionalidad está oculta detrás de esa palabra clave privada, puede implementarse como desee ... por lo que podría hacerlo agradable y modular, incluso si un bit depende del otro que se llama primero.

2. La conexión de nodos entre sí podría no ser una función privada

¿Qué sucede si en algún momento desea agregar otros nodos a la matriz? ¿Destruye la configuración que tiene ahora y la reinicia completamente? ¿O agrega nodos a la matriz existente y luego se ejecuta connectNodesnuevamente?

Posiblemente connectNodespuede tener una respuesta sensata si la matriz de nodos aún no se ha creado (¿lanzar una excepción? ¿Devolver un conjunto vacío? Tiene que decidir qué tiene sentido para su situación).

Jen
fuente
Estaba pensando de la misma manera que 1, y podría lanzar una excepción o algo si los nodos no se inicializaran, pero no es particularmente intuitivo. Gracias por la respuesta.
mcfroob
4

También puede encontrar (dependiendo de lo compleja que sea cada una de estas tareas) que esta es una buena costura para dividir otra clase.

(No estoy seguro si Swift funciona de esta manera pero con un pseudocódigo :)

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
    }
}

Esto separa las responsabilidades de crear y modificar nodos para separar las clases: NodeGeneratorsolo se preocupa por crear / recuperar nodos, mientras que YourClasssolo se preocupa por conectar los nodos que se le dan.

Willoller
fuente
2

Además de ser el propósito exacto de los métodos privados, Swift le brinda la capacidad de usar funciones internas.

Los métodos internos son perfectos para funciones que tienen un solo sitio de llamada, pero sienten que no justifican ser funciones privadas separadas.

Por ejemplo, es muy común tener una función de "entrada" recursiva pública, que verifica las condiciones previas, establece algunos parámetros y delega en una función recursiva privada que hace el trabajo.

Aquí hay un ejemplo de cómo se vería eso en este caso:

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
    }
}

Preste atención a cómo uso los valores y parámetros de retorno para pasar datos, en lugar de mutar un estado compartido. Esto hace que el flujo de datos sea mucho más obvio a primera vista, sin necesidad de saltar a la implementación.

Alexander - Restablece a Monica
fuente
0

Cada función que declara conlleva la carga de agregar documentación y generalizarla para que otras partes del programa puedan utilizarla. También conlleva la carga de comprender cómo otras funciones en el archivo pueden estar usándolo para alguien que lee el código.

Sin embargo, si no lo utilizan otras partes de su programa, no lo expondría como una función separada.

Si su idioma lo admite, aún puede tener una función-hace-una-cosa usando funciones anidadas

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

El lugar de la declaración es muy importante, y en el ejemplo anterior está claro, sin necesidad de más pistas, que las funciones internas están destinadas a ser utilizadas solo dentro del cuerpo de la función externa.

Incluso si los está declarando como funciones privadas, supongo que todavía son visibles para todo el archivo. Por lo tanto, deberá declararlos cerca de la declaración de la función principal y agregar cierta documentación que aclare que solo deben ser utilizados por la función externa.

No creo que tengas que hacer estrictamente uno u otro. Lo mejor que puede hacer varía caso por caso.

Desglosarlo en múltiples funciones sin duda agrega una sobrecarga de comprensión de por qué hay 3 funciones y cómo funcionan todas juntas, pero si la lógica es compleja, entonces esta sobrecarga adicional puede ser mucho menor que la simplicidad introducida al romper la lógica compleja en partes más simples.

Peeyush Kushwaha
fuente
Opción interesante Como usted dice, creo que podría ser un poco desconcertante el por qué la función se declaró así, pero mantendría la dependencia de la función bien contenida.
mcfroob
Para responder algunas de las incertidumbres en esta pregunta: 1) Sí, Swift admite funciones internas, y 2) Tiene dos niveles de "privado". privatepermite el acceso solo dentro del tipo de cerramiento (struct / class / enum), mientras que fileprivatepermite el acceso en todo el archivo
Alexander - Restablece Monica el