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

Здесь представлены все хорошо известные шаблоны, принципы и практики, которые помогут любому разработчику стать более профессиональным и создавать лучшее программное обеспечение. Почти все в нашей области слышали о SOLID (https://en.wikipedia.org/wiki/SOLID), которые представляют собой набор принципов, которые помогут вам в этом, быть лучшим профессионалом и создавать лучшее программное обеспечение. Хотя то, что я видел на практике, и некоторые разработчики программного обеспечения, имеющие опыт проведения собеседований, вероятно, могут это подтвердить, так это то, что многие разработчики имеют представление о том, что они собой представляют, но не понимают и не практикуют некоторые или большинство из них. Сегодня я хочу поговорить о втором из его принципов, O в SOLID, Принцип открытости / закрытости. Я хочу показать распространенную плохую практику, которая нарушает этот принцип, и то, что я делаю, чтобы избежать этой ловушки, и что я пытаюсь реорганизовать, когда это удобно.

Я должен сказать, что не мне в голову пришла эта идея, я узнал ее от Джимми Богарда (создателя AutoMapper и MediatR) в презентации, которую он сделал, если я правильно помню, об усилении слабых бизнес-доменов. С тех пор я использовал эту технику, которую он реализовал на C # (используя Полиморфизм), но эта статья о том, как поделиться с вами тем, как я это делаю в JavaScript, который, кстати, не использует ECMAScript 6. «Классы», а простая фабрика функция.

Первоначальное требование:

Вам необходимо создать функцию, которая принимает аргумент с возможными значениями «четное» и «нечетное» и возвращает массив с четными или нечетными числами (в зависимости от значения аргумента), которые находятся в диапазоне от 0 до 10 (оба заканчивается эксклюзивно). Предел 10 будет фиксированным для простоты, поскольку наша цель в этом упражнении - не создание алгоритма, а разработка кода.

Распространенное решение:

const getSequence = type => {
    if (type === 'even') {
        return getEvenNumbers();
    } else if (type === 'odd') {
        return getOddNumbers();
    } else {
        throw Error(`Invalid argument value ${type}`);
    }
};

Мы еще не реализовали getEvenNumbers или getOddNumbers, и, честно говоря, они нам не нужны, хотя они будут включены для завершения. Кроме того, он выглядит как вполне нормальный код с операторами if / else-if / else, которые охватывают возможные допустимые сценарии (для целей статьи нас не интересуют крайние случаи здесь).

Вариант приведенного выше кода будет:

const getSequence = type => {
    switch(type) {
        case 'even':
            return getEvenNumbers();
        case 'odd':
            return getOddNumbers();
        default:
            throw Error(`Invalid argument value ${type}`);
    }
};

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

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

const getEvenNumbers = limit => includeIfConditionIsMet(limit,    number => number % 2 === 0);

const getOddNumbers = limit => includeIfConditionIsMet(limit, number => number % 2 !== 0);

const includeIfConditionIsMet = (limit = 10, predicate) => {
    return (function inner(array, number) {
        if (number === limit) {
            return array;
        }
        return inner(predicate(number) ? [...array, number] : array, number + 1);
    })([], 1);
};

Эта последняя функция использует лимит по умолчанию, равный 10, и предикат (единственное, что варьируется между getEvenNumbers и getOddNumbers). , чтобы построить и вернуть желаемый массив. Как вы знаете, эту последнюю функцию можно было реализовать разными способами. Поскольку здесь мы не особо заботимся о деталях его реализации, я просто использовал рекурсию, потому что мне не нравятся for, foreach, while или for of loops (я не люблю императивное программирование, вы можете найти почему в одной из моих других статей https://itnext.io/a-more-declarative-solution -to-the-t9-problem-in-both-javascript-and-c-13ab7f7a859b ), и я не очень беспокоюсь о том, чтобы съесть все мои кадры стека в этом примере задачи (да, я знаю, что оптимизация хвостового вызова не попал в ECMAScript в конце).

Второе требование:

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

Давайте придерживаться решения if / else для первого и второго «требования». Вы уверены, что произведете впечатление на босса, когда он увидит, как быстро вы закончите. Теперь ваш код выглядит так:

const getElementsByPositionType = (type, elements) => {
    if (type === 'even') {
        return getElementsInEvenPosition(elements);
    } else if (type === 'odd') {
        return getElementsInOddPosition(elements);
    } else {
        throw Error(`Invalid argument value ${type}`);
    }
};

Для завершения, включая реализацию getElementsInEvenPosition, вам будет очень легко придумать getElementsInOddPosition.

const getElementsInEvenPosition = elements => elements.reduce((a, c, i) => i % 2 === 0 ? a.concat(c) : a, []);

Но на самом деле ваш босс видит проблему сразу же во время сеанса проверки кода. Похоже, что между функциями getSequence и getElementsByPositionType много дублирования. Пункты проверки if / else-if / else идентичны, и теперь он почти уверен, что вы скопировали и вставили первый и только что внесли в него изменения (Бог знает, что я сделал). Ему интересно, собираетесь ли вы делать то же самое каждый раз, когда появляется новое «требование», подобное этому, и он сообщает вам о своей проблеме.

Проблема (часть I):

Дублирование - это нехорошо, оно порождает множество проблем, о которых, я уверен, вы знаете. Поверьте, я видел кодовые базы с 15 операторами плюс if / else или switch, которые проверяли один и тот же диапазон возможных значений. Наиболее типичный случай, с которым я столкнулся, связан с перечислениями в C #. Например, класс Person, у которого есть свойство Type, которое представляет собой перечисление со значениями Manager, VP и т. Д .; и существует разная логика для расчета чего-либо для каждого из них, например, бонусной выплаты, но они также отличаются друг от друга в нескольких других операциях, поэтому для каждого отдельного расчета на основе типа существует Оператор switch на основе свойства enum Type. Когда наступает день, когда необходимо добавить новое значение в перечисление, бедняга, который должен это сделать, должен проверить всю кодовую базу в поисках тех операторов switch и добавьте новое предложение case к каждому из них. И не пропустите ни одного, потому что это будет ваша вина, вы будете тем, кого повесят, а не тем, кто создал enum, или армией солдат, которые добавили в переключатель / case / default ад.

Но пока у вас не было этой проблемы, у вас просто проблема с дублированием ... просто подождите.

Третье требование:

Вы все еще не оправились от того, что ваше программистское эго упало на два или три шага после того, как ваш босс не выглядел очень довольным тем, как вы решили последнее «требование»; и все же у него есть новая задача для вас. «Наш продукт должен поддерживать аналогичные функции для простых чисел», - это фраза, которую вы не сможете выкинуть из головы во время того прекрасного первого свидания, которое у вас будет, потому что он / она может не увидеть вас снова, но ваш босс увидит.

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

const getSequence = type => {
    if (type === 'even') {
        return getEvenNumbers();
    } else if (type === 'odd') {
        return getOddNumbers();
    } else if (type === 'prime') {
        return getPrimeNumbers();
     } else {
        throw Error(`Invalid argument value ${type}`);
    }
};

const getElementsByPositionType = (type, elements) => {
    if (type === 'even') {
        return getElementsInEvenPosition(elements);
    } else if (type === 'odd') {
        return getElementsInOddPosition(elements);
    } else if (type === 'prime') {
        return getElementsInPrimePosition();
     } else {
        throw Error(`Invalid argument value ${type}`);
    }
};

Хорошо, что было всего два места, куда можно было пойти, возможно, вам не повезет через два года, когда проект будет намного масштабнее и сложнее.

Вы можете реализовать getElementsInPrimePosition как упражнение.

Что-то в вашем сердце подсказывает вам, что вы не должны показывать это своему боссу, у вас может не хватить денег, чтобы заплатить за следующее свидание, если вы это сделаете (я знаю, что вы хорошо справились в первую ночь, и он / она увидит вас снова ).

Проблема (часть II):

Сначала вы нарушили принцип DRY https://en.wikipedia.org/wiki/Don%27t_repeat_yourself, а на этот раз вы нарушаете принцип открытости / закрытости. Https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle. Последнее в основном гласит: программные объекты (классы, модули, функции и т. Д.) Должны быть открыты для расширения, но закрыты для модификации. Вы сделали открытую операцию на этих двух местах и ​​изменили код. Каждый раз, когда вы изменяете код подобным образом, особенно во многих разных местах, вы сильно рискуете что-то сломать, а также создаете код, который сложнее отслеживать, поддерживать и расширять. Не будьте эгоистичны, думайте о той бедной душе, которая придет к этому коду после вас, который может быть вашим собственным, в следующем году, в следующем месяце или через 2 часа.

Если вы хотите узнать больше о принципе открытости / закрытости и в целом принципах SOLID, я рекомендую вам прочитать Agile Principles, Patterns, and Practices in C # Роберт К. Мартин.

Решение:

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

const asEnumeration = dictionary => {
    return Object.freeze({
        fromValue: value => {
            if (dictionary[value]) {
                return dictionary[value];
            }
            throw Error(`Invalid enumeration value ${value}`); 
        }
    });
};

Я использовал Enumeration как часть этого имени функции, чтобы отдать дань уважения имени, которое Джимми Богард дал своему абстрактному классу, который является краеугольным камнем в его дизайне C #, направленном на решение всех вышеупомянутых проблем.

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

const numbersEnumeration = asEnumeration({
    'even': {
        getSequence: getEvenNumbers
    },
    'odd': {
        getSequence: getOddNumbers
    }
});

Теперь у меня есть числовое перечисление, ключи которого являются возможными значениями нашего бизнес-домена, «четным», «нечетным» и однодневным «простым» или в любом другом случае. Затем я могу реализовать getSequence следующим образом:

const getSequence = type => numbersEnumeration.fromValue(type).getSequence(10);

Затем, когда появилось новое «требование» для «элементов по типу позиции», вместо того, чтобы повторяться в двух или, возможно, 15 разных местах, вы просто добавили бы в одном месте новое свойство к каждому объекту в словаре, например:

const numbersEnumeration = asEnumeration({
    'even': {
        getSequence: getEvenNumbers,
        getElementsByPositionType: getElementsInEvenPosition,
    },
    'odd': {
        getSequence: getOddNumbers,
        getElementsByPositionType: getElementsInOddPosition,
    }
});

Тогда getElementsByPositionType было бы следующим:

const getElementsByPositionType = (type, elements) => numbersEnumeration.fromValue(type).getElementsByPositionType(elements);

Вы можете выполнить рефакторинг кода и вызвать getSequence и getElementsInPrimePosition следующим образом:

const fromValue = type => numbersEnumeration.fromValue(type);

const getSequence = type => fromValue(type).getSequence(10);

const getElementsByPositionType = (type, elements) => fromValue(type).getElementsByPositionType(elements);
 
console.log(getSequence('even'));
console.log(getElementsByPositionType('odd', [6, 7, 8, 9, 10]));

Теперь, когда появляется логика простых чисел или любая другая логика в этом отношении, вы просто добавляете новый объект в словарь сопоставления (изменения снова только в одном месте) вот так:

const numbersEnumeration = asEnumeration({
    'even': {
        getSequence: getEvenNumbers,
        getElementsByPositionType: getElementsInEvenPosition,
    },
    'odd': {
        getSequence: getOddNumbers,
        getElementsByPositionType: getElementsInOddPosition,
    },

    'prime': {
        getSequence: getPrimeNumbers,
        getElementsByPositionType: getElementsInPrimePosition,
    }
});

Больше нет операторов if / else / switch, которые можно было бы отслеживать и изменять во всей кодовой базе. Я должен признать, что нет кода, который был бы полностью закрыт для модификации, поэтому не воспринимайте это буквально и не расстраивайтесь, если вы обнаружите, что изменяете существующий код, пытаясь не нарушать этот принцип, нет возможности добавлять или изменять функции в системе без модификации кода. Но, безусловно, лучше делать это, добавляя или изменяя код в одном месте, а не в нескольких местах, и предпочтительно добавляя / расширяя, чем изменяя.

Я также должен сказать, что этот метод предназначен не для замены всех операторов if / else / switch, а для замены тех, которые работают с фиксированным набором значений, таких как эти перечисления типы содержат, и это, как правило, создает много дублирования вокруг них. Я использую эту технику уже много лет (сначала я начал делать ее на C #, а затем на JavaScript), и она меня ни разу не подводила.

Кодируете ли вы на таком языке, как C #, через классы, наследование и полиморфизм (или, что еще лучше, композицию https://en.wikipedia.org/wiki/Composition_over_inheritance) или на таком языке, как JavaScript (используя фабричные функции, объекты и даже композицию объектов), если вы хотите создать сильный домен (который, на мой взгляд, является полной противоположностью анемичного домена https://www.martinfowler.com/bliki/AnemicDomainModel.html ) enum-подобные типы должны иметь поведение, а не просто быть примитивными типами .

Удачного кодирования!