Малые функции против сохранения зависимой функциональности в той же функции

15

У меня есть класс, который устанавливает массив узлов и соединяет их друг с другом в виде графа. Это лучше всего:

  1. Сохраняйте функциональность для инициализации и соединения узлов в одной функции
  2. Имейте функции инициализации и подключения в двух разных функциях (и имейте зависимый порядок, в котором функции должны быть вызваны - хотя имейте в виду, что эти функции являются частными.)

Метод 1: (Плохо, что одна функция делает две вещи, НО сохраняет зависимые функции сгруппированными - узлы никогда не должны соединяться без предварительной инициализации.)

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
}

Способ 2: (лучше в том смысле, что он самодокументируется, но connectNodes () никогда не следует вызывать перед setupNodes (), поэтому любой, кто работает с внутренними компонентами класса, должен знать об этом порядке.)

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
}

Рад слышать любые мысли.

mcfroob
источник
Один из способов решить эту проблему - определить промежуточные объекты, которые можно использовать только для создания конечных объектов. Это не всегда правильное решение, но полезно, если пользователь интерфейса должен каким-либо образом манипулировать промежуточным состоянием.
Джоэл Корнетт

Ответы:

23

Проблема, с которой вы имеете дело, называется временной связью

Вы правы, заботясь о том, насколько понятен этот код:

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

Я могу догадаться, что там происходит, но скажите мне, если это делает то, что еще происходит, немного яснее:

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

Это имеет дополнительное преимущество, заключающееся в том, что он менее связан с изменением переменных экземпляра, но для меня удобочитаемость - это номер один.

Это делает connectNodes()зависимость от узлов явной.

candied_orange
источник
1
Спасибо за ссылку. Так как мои функции являются частными и вызываются из конструктора - init () в Swift - я не думаю, что мой код будет таким же плохим, как примеры, которые вы связали (для внешнего клиента было бы невозможно создать экземпляр с помощью переменная нулевого экземпляра), но у меня похожий запах.
Макфруб
1
Код, который вы добавили, более читабелен, так что я буду рефакторинг в этом стиле.
Макфруб
10

Отдельные функции по двум причинам:

1. Частные функции являются частными именно для этой ситуации.

Ваша initфункция общедоступна, а ее интерфейс, поведение и возвращаемое значение - это то, что вам нужно беспокоиться о защите и изменении. Результат, который вы ожидаете от этого метода, будет одинаковым независимо от того, какую реализацию вы используете.

Поскольку остальная часть функциональности скрыта за этим приватным ключевым словом, она может быть реализована так, как вам нравится ... так что вы также можете сделать ее красивой и модульной, даже если один бит зависит от того, какой другой вызывается первым.

2. Соединение узлов друг с другом не может быть частной функцией

Что если в какой-то момент вы захотите добавить другие узлы в массив? Вы уничтожаете имеющуюся настройку и полностью инициализируете ее? Или вы добавляете узлы в существующий массив и затем запускаете connectNodesснова?

Возможно, connectNodesможет иметь вменяемый ответ, если массив узлов еще не был создан (сгенерировать исключение, вернуть пустой набор, вы должны решить, что имеет смысл для вашей ситуации).

Джен
источник
Я думал так же, как 1, и мог бы выдать исключение или что-то еще, если бы узлы не были инициализированы, но это не особенно интуитивно понятно. Спасибо за ответ.
Макфруб
4

Вы также можете обнаружить (в зависимости от сложности каждой из этих задач), что это хороший шов для разделения другого класса.

(Не уверен, что Swift работает таким образом, но псевдокод :)

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

Это разделяет обязанности по созданию и изменению узлов на отдельные классы: NodeGeneratorзаботится только о создании / извлечении узлов, а YourClassзаботится только о подключении тех узлов, которые ему даны.

willoller
источник
2

В дополнение к тому, что это является точной целью частных методов, Swift дает вам возможность использовать внутренние функции.

Внутренние методы идеально подходят для функций, которые имеют только один сайт вызова, но чувствуют, что они не оправдывают отдельную частную функцию.

Например, очень часто есть общедоступная рекурсивная функция «entry», которая проверяет предварительные условия, устанавливает некоторые параметры и делегирует частную рекурсивную функцию, которая выполняет эту работу.

Вот пример того, как это может выглядеть в этом случае:

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

Обратите внимание на то, как я использую возвращаемые значения и параметры для передачи данных, а не изменяю общее состояние. Это делает поток данных намного более очевидным на первый взгляд, без необходимости переходить к реализации.

Александр - Восстановить Монику
источник
0

Каждая функция, которую вы объявляете, несет в себе бремя добавления документации и ее обобщения, чтобы ее можно было использовать в других частях программы. Это также несет бремя понимания того, как другие функции в файле могут использовать его для тех, кто читает код.

Однако, если он не используется другими частями вашей программы, я бы не стал представлять его как отдельную функцию.

Если ваш язык поддерживает это, вы все равно можете иметь одно целое с помощью вложенных функций

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

Место объявления имеет очень большое значение, и в приведенном выше примере ясно, не требуя каких-либо дополнительных указаний, что внутренние функции предназначены для использования только в теле внешней функции.

Даже если вы объявляете их как частные функции, я предполагаю, что они все еще видны всему файлу. Таким образом, вам необходимо объявить их близко к объявлению основной функции и добавить некоторую документацию, поясняющую, что они должны использоваться только внешней функцией.

Я не думаю, что вы должны строго делать одно или другое. Лучшее, что можно сделать, зависит от конкретного случая.

Разбиение его на несколько функций, безусловно, добавляет непонимание, почему существует 3 функции и как они все работают друг с другом, но если логика сложная, то эти дополнительные издержки могут быть намного меньше, чем простота, представленная разбиванием сложной логики. на более простые части.

Peeyush Kushwaha
источник
Интересный вариант. Как вы говорите, я думаю, что было бы немного озадачивать, почему функция была объявлена ​​таким образом, но она сохраняла бы зависимость функции хорошо сдержанной.
Макфруб
Чтобы ответить на некоторые неопределенности в этом вопросе: 1) Да, Swift поддерживает внутренние функции, и 2) Он имеет два уровня «частного». privateразрешает доступ только внутри вложенного типа (struct / class / enum), в то время как fileprivateразрешает доступ ко всему файлу
Александр - Восстановить Монику