Статическая / строгая типизация и рефакторинг
Мне кажется, что самая бесценная вещь в статическом / строго типизированном языке программирования заключается в том, что он помогает рефакторингу: если / когда вы меняете какой-либо API, то компилятор сообщит вам, что это изменение сломало.
Я могу представить написание кода на языке исполнения / слабо типизированном языке... но я не представляю рефакторинг без помощи компилятора, и я не могу представить написание десятков тысяч строк кода без рефакторинга.
Это правда?
5 ответов
I think you're conflating when types are checked with how they're checked. Runtime typing isn't necessarily weak.
The main advantage of static types is exactly what you say: they're exhaustive. You can be confident all call sites conform to the type just by letting the compiler do it's thing.
The main limitation of static types is that they're limited in the constraints they can express. This varies by language, with most languages having relatively simple type systems (c, java), and others with extremely powerful type systems (haskell, cayenne).
Из-за этого ограничения типов сами по себе недостаточны. Например, в Java типы более или менее ограничены проверкой совпадения имен типов. Это означает, что значение любого проверяемого ограничения должно быть закодировано в какой-либо схеме именования, отсюда и множество косвенных указателей и общей схемы, общей для Java-кода. C++ немного лучше в том, что шаблоны позволяют немного больше выразительности, но не приближаются к тому, что вы можете делать с зависимыми типами. Я не уверен, каковы недостатки более мощных систем типов, хотя очевидно, что некоторые или более людей будут использовать их в промышленности.
Даже если вы используете статическую типизацию, скорее всего, она недостаточно выразительна, чтобы проверять все, что вас волнует, поэтому вам также придется писать тесты. Вопрос о том, экономит ли вам статическая типография больше усилий, чем требуется, - это спор, который бушует целую вечность, и я не думаю, что он может дать простой ответ на все ситуации.
Что касается вашего второго вопроса:
Как мы можем безопасно перефакторизовать типизированный язык во время выполнения?
Ответ - тесты. Ваши тесты должны охватывать все случаи, которые имеют значение. Инструменты могут помочь вам оценить, насколько исчерпывающими являются ваши тесты. Инструменты проверки покрытия позволяют узнать, охвачены ли тесты строки кода или нет. Инструменты мутации тестов (шут, хекл) могут дать вам знать, если ваши тесты логически неполны. Приемочные тесты позволяют узнать, что написанное соответствует требованиям, и, наконец, регрессионные и тесты производительности гарантируют, что каждая новая версия продукта поддерживает качество последней.
Одной из замечательных особенностей правильного тестирования по сравнению с использованием сложных указаний типов является то, что отладка становится намного проще. При запуске тестов вы получаете конкретные ошибочные утверждения внутри тестов, которые четко выражают то, что они делают, а не тупые операторские сообщения об ошибках (например, ошибки шаблона C++).
Независимо от того, какие инструменты вы используете: написание кода, в котором вы уверены, потребует усилий. Скорее всего, это потребует написания множества тестов. Если штраф за ошибки очень высок, например, для аэрокосмического или медицинского программного обеспечения, вам может понадобиться использовать формальные математические методы, чтобы доказать поведение вашего программного обеспечения, что делает такую разработку чрезвычайно дорогой.
Я полностью согласен с вашим мнением. Именно гибкость, с которой динамически типизированные языки должны быть хороши, на самом деле делает код очень сложным в обслуживании. Действительно, существует ли такая вещь, как программа, которая продолжает работать, если типы данных изменяются нетривиальным образом без фактического изменения кода?
В то же время вы можете проверить тип передаваемой переменной и каким-то образом потерпеть неудачу, если это не ожидаемый тип. Вам все равно придется запускать свой код для устранения этих случаев, но по крайней мере что-то скажет вам.
Я думаю, что внутренние инструменты Google на самом деле выполняют компиляцию и, возможно, проверку типов в своем Javascript. Я хотел бы иметь эти инструменты.
Для начала я программист на Perl, поэтому, с одной стороны, я никогда не программировал с помощью сети статических типов. OTOH Я никогда не программировал с ними, поэтому я не могу говорить об их преимуществах. То, что я могу говорить, это то, что это как рефакторинг.
Я не считаю отсутствие статических типов проблемой для рефакторинга. Проблема в том, что у меня нет браузера с рефакторингом. У динамических языков есть проблема в том, что вы на самом деле не знаете, что на самом деле собирается делать код, пока вы на самом деле не запустите его. У Perl это больше, чем у большинства. У Perl есть дополнительная проблема с очень сложным, почти не разбираемым синтаксисом. Результат: нет инструментов рефакторинга (хотя они очень быстро работают над этим). Конечный результат - я должен сделать рефакторинг вручную. И это то, что вносит ошибки.
У меня есть тесты, чтобы поймать их... обычно. Я часто оказываюсь перед дымящейся кучей непроверенного и почти непроверяемого кода с проблемой "курица / яйцо", заключающейся в необходимости рефакторинга кода для его тестирования, но необходимости его тестирования для его рефакторинга. Ик. К этому моменту я должен написать какой-то очень тупой, высокий уровень "выполняет ли программа то же самое, что и раньше", вроде тестов, просто чтобы убедиться, что я ничего не сломал.
Статические типы, как предусмотрено в Java, C++ или C#, действительно решают только небольшой класс задач программирования. Они гарантируют, что вашим интерфейсам передаются биты данных с правильной меткой. Но только то, что вы получаете коллекцию, не означает, что коллекция содержит данные, которые, по вашему мнению, содержат. Потому что вы получаете целое число, не означает, что вы получили правильное целое число. Ваш метод принимает объект User, но зарегистрирован ли этот пользователь?
Классический пример: public static double sqrt(double a)
является сигнатурой для функции квадратного корня Java. Квадратный корень не работает на отрицательных числах. Где это написано в подписи? Это не так. Еще хуже, где говорится, что эта функция вообще делает? Подпись только говорит, какие типы он принимает и что возвращает. Он ничего не говорит о том, что происходит между ними, и именно там живет интересный код. Некоторые люди пытались получить полный API-интерфейс, используя проектирование по контракту, что в целом можно охарактеризовать как встраивание во время выполнения входных, выходных и побочных эффектов вашей функции (или их отсутствия)... но это еще одно шоу.
API - это гораздо больше, чем просто сигнатуры функций (если бы это было не так, вам не понадобилась бы вся эта описательная проза в Javadocs), а рефакторинг - это гораздо больше, чем просто изменение API.
Самое большое преимущество рефакторинга, которое дает статически типизированный, статически скомпилированный, не динамический язык, - это возможность писать инструменты рефакторинга для выполнения довольно сложных рефакторингов за вас, потому что он знает, где находятся все вызовы ваших методов. Я очень завидую IntelliJ IDEA.
Одним из преимуществ использования var в C# 3.0 является то, что вы часто можете изменять тип, не нарушая никакого кода. Тип должен по-прежнему выглядеть одинаково - должны существовать свойства с одинаковыми именами, методы с одинаковой или сходной сигнатурой должны существовать. Но вы действительно можете перейти на совершенно другой тип, даже не используя что-то вроде ReSharper.
Я бы сказал, что рефакторинг выходит за рамки того, что может проверять компилятор, даже в статически типизированных языках. Рефакторинг - это просто изменение внутренней структуры программы без влияния на внешнее поведение. Даже в динамических языках есть вещи, которые вы можете ожидать и тестировать, вы просто теряете немного помощи от компилятора.