Pequenas funções vs. manter a funcionalidade dependente na mesma função

15

Eu tenho uma classe que configura uma matriz de nós e os conecta um ao outro em uma estrutura semelhante a um gráfico. É melhor:

  1. Mantenha a funcionalidade para inicializar e conectar os nós em uma função
  2. Tenha a funcionalidade de inicialização e conexão em duas funções diferentes (e tenha uma ordem dependente na qual as funções devem ser chamadas - embora tenha em mente que essas funções são privadas).

Método 1: (Ruim, porque uma função está fazendo duas coisas, mas mantém a funcionalidade dependente agrupada - os nós nunca devem ser conectados sem serem inicializados primeiro.)

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: (Melhor no sentido de que é auto-documentado, MAS connectNodes () nunca deve ser chamado antes de setupNodes (), portanto, qualquer pessoa que trabalhe com os internos da classe precisa conhecer essa ordem.)

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
}

Animado para ouvir qualquer pensamento.

mcfroob
fonte
Uma maneira de resolver isso definindo objetos intermediários que só podem ser usados ​​para criar os objetos finais. Essa nem sempre é a solução certa, mas é útil se o usuário da interface precisar manipular um estado intermediário de alguma forma.
Joel Cornett

Respostas:

23

O problema com o qual você está lidando é chamado de acoplamento temporal

Você está certo em se preocupar com o quão compreensível esse código é:

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

Eu posso adivinhar o que está acontecendo lá, mas me diga se isso torna o que mais está acontecendo um pouco mais claro:

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

Isso tem o benefício adicional de ser menos acoplado à modificação de variáveis ​​de instância, mas, para mim, ser legível é o número um.

Isso torna connectNodes()explícita a dependência de nós.

candied_orange
fonte
1
Obrigado pelo link. Como minhas funções são privadas e chamadas pelo construtor - init () no Swift -, não acho que meu código seja tão ruim quanto os exemplos que você vinculou (seria impossível para um cliente externo instanciar uma instância com um variável de instância nula), mas tenho um cheiro semelhante.
Mcfroob 28/12/16
1
O código que você adicionou é mais legível, então refatorarei nesse estilo.
Mcfroob 28/12/16
10

Funções separadas , por dois motivos:

1. Funções privadas são privadas exatamente para esta situação.

Sua initfunção é pública e sua interface, comportamento e valor de retorno é o que você precisa para se preocupar em proteger e mudar. O resultado que você espera desse método será o mesmo, independentemente da implementação usada.

Como o restante da funcionalidade está oculto por trás dessa palavra-chave privada, ela pode ser implementada da maneira que você quiser ... para que você a torne agradável e modular, mesmo que um pouco dependa do outro chamado primeiro.

2. Conectar nós um ao outro pode não ser uma função privada

E se, em algum momento, você desejar adicionar outros nós à matriz? Você destrói a configuração que possui agora e reinicia-a completamente? Ou você adiciona nós à matriz existente e executa connectNodesnovamente?

Possivelmente connectNodespode ter uma resposta sã se a matriz de nós ainda não tiver sido criada (lance uma exceção? Retorne um conjunto vazio? Você precisará decidir o que faz sentido para a sua situação).

Jen
fonte
Eu estava pensando da mesma maneira que 1, e poderia lançar uma exceção ou algo assim se os nós não fossem inicializados, mas não é particularmente intuitivo. Obrigado pela resposta.
Mcfroob 28/12/16
4

Você também pode descobrir (dependendo da complexidade de cada uma dessas tarefas) que essa é uma boa solução para dividir outra classe.

(Não tenho certeza se Swift funciona dessa maneira, mas com pseudo-có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
    }
}

Isso separa as responsabilidades de criar e modificar nós para separar classes: NodeGeneratorapenas se preocupa em criar / recuperar nós, enquanto YourClassapenas se preocupa em conectar os nós que ele recebe.

willoller
fonte
2

Além de ser esse o objetivo exato dos métodos particulares, o Swift oferece a capacidade de usar funções internas.

Os métodos internos são perfeitos para funções que possuem apenas um site de chamada, mas parecem que não justificam serem funções privadas separadas.

Por exemplo, é muito comum ter uma função pública de "entrada" recursiva, que verifica pré-condições, configura alguns parâmetros e delega para uma função privada recursiva que faz o trabalho.

Aqui está um exemplo de como isso pode parecer neste 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 atenção em como eu uso valores e parâmetros de retorno para transmitir dados, em vez de alterar um estado compartilhado. Isso torna o fluxo de dados muito mais óbvio à primeira vista, sem a necessidade de pular para a implementação.

Alexander - Restabelecer Monica
fonte
0

Toda função que você declara carrega o ônus de adicionar documentação e torná-la generalizada para que possa ser usada por outras partes do programa. Ele também carrega o ônus de entender como outras funções no arquivo podem estar usando-o para alguém ler o código.

Se, no entanto, não for usado por outras partes do seu programa, eu não o exporia como uma função separada.

Se o seu idioma suportar, você ainda pode ter uma função faz uma coisa usando funções aninhadas

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

O local da declaração é muito importante e, no exemplo acima, fica claro, sem a necessidade de mais pistas, que as funções internas devem ser usadas apenas dentro do corpo da função externa.

Mesmo se você as declarar como funções privadas, presumo que elas ainda estejam visíveis para o arquivo inteiro. Portanto, você precisará declará-los perto da declaração da função principal e adicionar alguma documentação que esclareça que eles devem ser usados ​​apenas pela função externa.

Não acho que você precise fazer estritamente um ou outro. A melhor coisa a fazer varia caso a caso.

Dividi-lo em várias funções certamente adiciona uma sobrecarga de entendimento sobre por que existem três funções e como todas elas funcionam, mas se a lógica é complexa, essa sobrecarga adicionada pode ser muito menor do que a simplicidade introduzida ao quebrar a lógica complexa. em partes mais simples.

Peeyush Kushwaha
fonte
Opção interessante. Como você diz, acho que seria um pouco confuso o motivo pelo qual a função foi declarada assim, mas manteria a dependência da função bem contida.
Mcfroob 28/12/16
Para responder a algumas das incertezas nesta pergunta: 1) Sim, o Swift suporta funções internas e 2) Possui dois níveis de "privado". privatepermite acesso somente no tipo de anexo (struct / class / enum), enquanto fileprivatepermite acesso ao longo de todo o arquivo
Alexander - Restabelecer Monica