Małe funkcje vs. utrzymywanie zależnej funkcjonalności w tej samej funkcji

15

Mam klasę, która ustawia tablicę węzłów i łączy je ze sobą w strukturze graficznej. Czy najlepiej:

  1. Zachowaj funkcjonalność, aby zainicjować i połączyć węzły w jednej funkcji
  2. Mają funkcje inicjalizacji i połączenia w dwóch różnych funkcjach (i mają zależną kolejność, w której funkcje muszą być wywoływane - należy jednak pamiętać, że te funkcje są prywatne).

Metoda 1: (Zła w tym, że jedna funkcja robi dwie rzeczy, ALE utrzymuje zgrupowane funkcje zależne razem - nigdy nie należy łączyć węzłów bez inicjalizacji).

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
}

Metoda 2: (Lepiej w tym sensie, że jest to samo-dokumentowanie, ALE connectNodes () nigdy nie powinno być wywoływane przed setupNodes (), więc każdy, kto pracuje z wewnętrznymi elementami klasy, musi wiedzieć o tej kolejności).

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
}

Podekscytowany, aby usłyszeć jakieś myśli.

McFroob
źródło
Jednym ze sposobów rozwiązania tego problemu jest zdefiniowanie obiektów pośrednich, których można użyć tylko do utworzenia obiektów końcowych. Nie zawsze jest to właściwe rozwiązanie, ale jest przydatne, jeśli użytkownik interfejsu musi w jakiś sposób manipulować stanem pośrednim.
Joel Cornett

Odpowiedzi:

23

Problem, z którym masz do czynienia, nazywa się sprzężeniem czasowym

Masz rację, jeśli chodzi o zrozumiałość tego kodu:

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

Mogę zgadnąć, co się tam dzieje, ale powiedz mi, czy to sprawia, że ​​to, co się dzieje, jest trochę jaśniejsze:

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

Ma to tę dodatkową zaletę, że jest mniej sprzężone z modyfikowaniem zmiennych instancji, ale dla mnie czytelność jest numerem jeden.

To sprawia connectNodes(), że zależność od węzłów jest jawna.

candied_orange
źródło
1
Dzięki za link. Ponieważ moje funkcje są prywatne i wywoływane z konstruktora - init () w Swift - nie sądzę, że mój kod byłby tak zły, jak przykłady, które podłączyłeś (nie byłoby możliwe, aby klient zewnętrzny utworzył instancję za pomocą zmienna instancji zerowej), ale mam podobny zapach.
mcfroob
1
Dodany kod jest bardziej czytelny, więc zmienię go w tym stylu.
mcfroob
10

Oddzielne funkcje z dwóch powodów:

1. Funkcje prywatne są prywatne dla dokładnie takiej sytuacji.

Twoja initfunkcja jest publiczna, a jej interfejs, zachowanie i wartość zwracana są tym, czego musisz się martwić o ochronę i zmianę. Wynik, którego oczekujesz od tej metody, będzie taki sam, bez względu na to, jakiej implementacji używasz.

Ponieważ reszta funkcji jest ukryta za tym prywatnym słowem kluczowym, można go wdrożyć, jak chcesz ... więc równie dobrze możesz uczynić go ładnym i modułowym, nawet jeśli jeden bit zależy od drugiego wywołania pierwszego.

2. Łączenie węzłów może nie być funkcją prywatną

Co jeśli w pewnym momencie chcesz dodać inne węzły do ​​tablicy? Czy niszczysz istniejącą konfigurację i całkowicie ją ponownie inicjujesz? A może dodajesz węzły do ​​istniejącej tablicy, a następnie uruchamiasz connectNodesponownie?

Być connectNodesmoże może mieć rozsądną odpowiedź, jeśli tablica węzłów nie została jeszcze utworzona (wyrzuć wyjątek? Zwróć pusty zestaw? Musisz zdecydować, co ma sens w twojej sytuacji).

Jen
źródło
Myślałem w taki sam sposób jak 1 i mogłem rzucić wyjątek lub coś takiego, gdyby węzły nie zostały zainicjowane, ale nie jest to szczególnie intuicyjne. Dzięki za odpowiedzi.
mcfroob
4

Może się również okazać (w zależności od stopnia złożoności każdego z tych zadań), że jest to dobry szew do podzielenia innej klasy.

(Nie jestem pewien, czy Swift działa w ten sposób, ale pseudo-kod :)

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

To rozdziela obowiązki związane z tworzeniem i modyfikowaniem węzłów w oddzielnych klasach: NodeGeneratorzależy tylko na tworzeniu / pobieraniu węzłów, a YourClasstylko na podłączaniu węzłów.

willoller
źródło
2

Oprócz tego, że jest to dokładny cel prywatnych metod, Swift daje możliwość korzystania z funkcji wewnętrznych.

Metody wewnętrzne są idealne dla funkcji, które mają tylko jedną witrynę wywołań, ale wydaje się, że nie uzasadniają oddzielnych funkcji prywatnych.

Na przykład bardzo często występuje publiczna rekurencyjna funkcja „wejścia”, która sprawdza warunki wstępne, ustawia niektóre parametry i deleguje do prywatnej funkcji rekurencyjnej, która wykonuje tę pracę.

Oto przykład, jak może to wyglądać w tym przypadku:

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

Zwróć uwagę na to, jak używam zwracanych wartości i parametrów do przekazywania danych, zamiast mutowania stanu wspólnego. To sprawia, że ​​przepływ danych jest znacznie bardziej oczywisty na pierwszy rzut oka, bez potrzeby przeskakiwania do implementacji.

Alexander - Przywróć Monikę
źródło
0

Każda deklarowana funkcja niesie za sobą ciężar dodawania dokumentacji i generalizowania jej, tak aby można ją było wykorzystać w innych częściach programu. Niesie również ciężar zrozumienia, jak inne funkcje w pliku mogą go używać dla kogoś, kto czyta kod.

Jeśli jednak nie jest używany przez inne części twojego programu, nie ujawniłbym go jako osobnej funkcji.

Jeśli twój język to obsługuje, nadal możesz mieć funkcję jedna funkcja robi jedną rzecz, używając funkcji zagnieżdżonych

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

Miejsce deklaracji ma bardzo duże znaczenie i w powyższym przykładzie jest jasne, bez potrzeby dalszych wskazówek, że funkcje wewnętrzne mają być używane tylko w ciele funkcji zewnętrznej.

Nawet jeśli deklarujesz je jako funkcje prywatne, zakładam, że są one nadal widoczne dla całego pliku. Musisz więc zadeklarować je blisko deklaracji funkcji głównej i dodać dokumentację, która wyjaśnia, że ​​mają być używane tylko przez funkcję zewnętrzną.

Nie sądzę, że musicie robić dokładnie jedno lub drugie. Najlepsze rzeczy do zrobienia różnią się w zależności od przypadku.

Podzielenie go na wiele funkcji z pewnością zwiększa ogólne zrozumienie, dlaczego istnieją 3 funkcje i jak one wszystkie działają ze sobą, ale jeśli logika jest złożona, to ten dodatkowy narzut może być znacznie mniejszy niż prostota wprowadzona przez rozbicie złożonej logiki na prostsze części.

Peeyush Kushwaha
źródło
Ciekawa opcja. Jak mówisz, myślę, że może to być nieco zastanawiające, dlaczego funkcja została zadeklarowana w ten sposób, ale utrzymałaby zależność funkcji w dobrym stanie.
mcfroob
Aby odpowiedzieć na niektóre niepewności w tym pytaniu: 1) Tak, Swift obsługuje funkcje wewnętrzne oraz 2) Ma dwa poziomy „prywatny”. privatezezwala na dostęp tylko w obrębie typu załączającego (struct / class / enum), natomiast fileprivateumożliwia dostęp do całego pliku
Alexander - Przywróć Monikę