IBM поставила систему управления спутником NASA с уровнем 0,1 дефекта на тысячу строк кода. Средний отраслевой показатель в то время колебался между 10 и 50. Они добились этого не нанимая более умных инженеров и не работая сверхурочно. Они добились этого, запретив разработчикам запускать собственный код.
Это был Cleanroom software engineering, разработанный Харланом Миллсом (Harlan Mills) в IBM в 1970-х годах. Название пришло из производства полупроводников, где цель состоит в том, чтобы предотвратить условия, создающие дефекты. Качество не встраивается тестированием. Оно проектируется изначально.
Проблема: тестирование находит дефекты, но не предотвращает их
Большинство подходов к разработке ПО исходят из цикла написание-тестирование-исправление. Вы пишете код, запускаете его, находите баги, исправляете их. Это кажется продуктивным. Также математически гарантировано, что при этом остаются дефекты.
Тестирование может доказать только наличие багов, но никогда их отсутствие. Дейкстра (Dijkstra) сказал это в 1969 году. Если ваш код имеет тысячу возможных путей выполнения, а тестовый набор покрывает из них пятьдесят, у вас остается 950 непротестированных путей. Средний отраслевой показатель дефектности 10-50 на KLOC — это не провал тщательности тестирования. Это предсказуемый результат процесса, который допускает существование дефектов.
Cleanroom переворачивает это. Вместо того чтобы писать код, а затем тестировать его, вы пишете код, который корректен по построению.
Как Cleanroom работает на самом деле
Метод включает три жесткие практики, работающие вместе.
Первое: инкрементальная разработка под statistical process control. Проект разбивается на небольшие инкременты, каждый из которых добавляет четко определенное подмножество функциональности. Каждый инкремент проходит спецификацию, проектирование, верификацию и тестирование как завершенная единица. Ключ в том, что дефектность измеряется по каждому инкременту. Если инкремент превышает целевой показатель дефектности, процесс останавливается, и команда выясняет, что пошло не так с методом, а не с отдельными людьми.
Второе: функционально-теоретическое проектирование с использованием box structures. Каждый компонент ПО определяется на трех уровнях:
- Black box: Внешнее поведение, определяемое чисто как математическая функция от входов к выходам. Никакого state, никаких деталей реализации.
- State box: Внутренняя функция перехода состояний. Какой state поддерживает компонент и как он изменяется?
- Clear box: Фактическая реализация, построенная из верифицированных компонентов.
Каждый уровень верифицируется относительно вышестоящего перед продолжением. Вы не пишете Clear box, пока State box не доказана корректной. Вы не пишете State box, пока Black box не доказана корректной. Это звучит медленно. Но это не так, потому что вы не занимаетесь отладкой. Вы не проводите полдня в дебаггере. Код работает с первой компиляции.
Третье — и это та часть, которая взрывает мозг: отсутствие execution testing со стороны разработчиков. Люди, которые пишут код, не компилируют, не запускают и не проводят unit testing. Тестирование выполняется отдельной командой с использованием статистических методов. Они рассматривают ПО как Black box и генерируют тестовые случаи на основе usage profiles, а не структуры кода.
Это разделение — не бюрократическая жестокость. Это ключевой механизм, который принуждает к корректности по построению. Если вы знаете, что не можете запустить свой код, чтобы “посмотреть, работает ли он”, вы вынуждены продумать каждый случай до того, как наберете его. Вы не можете полагаться на компилятор, чтобы поймать ваши ошибки, или на быстрый python script.py, чтобы обнаружить очевидный баг. Вы должны быть правы.
Числа, которые имеют значение
Подразделение IBM Federal Systems Division использовало Cleanroom в нескольких проектах в 1980-х и 1990-х годах. Результаты были согласованы между командами, языками и прикладными областями.
Система управления спутником NASA: 0,1 дефекта на KLOC на финальном тестировании. Система биллинга на COBOL: 0,3 дефекта на KLOC. Система реального времени на Ada: 0,4 дефекта на KLOC. Для сравнения — средние отраслевые показатели того времени: 10-50 дефектов на KLOC на unit test, 5-10 все еще остаются к моменту поставки.
Команды Cleanroom также поставляли быстрее. Отраслевые данные того времени показывали, что 50-70% усилий по разработке ПО уходили на тестирование и отладку. Команды Cleanroom тратили это время на проектирование, и код работал с первого раза.
Почему почти никто это не использует
Если Cleanroom настолько эффективен, почему же его не используют все?
Вы не можете “просто попробовать” в одном спринте. Метод взаимозависим. Statistical process control работает только если вы измеряете каждый инкремент. Box structures работают только при формальной верификации. Правило отсутствия запуска работает только если оно абсолютно. Частичное внедрение не дает никаких преимуществ, но все накладные расходы.
Рынок тоже изменился. В 1970-х и 1980-х годах ПО поставлялось на ленте. Баг в production был дорогим в исправлении. Сегодня мы поставляем over the wire, и можем применить патч за минуты. Экономический стимул для zero-defect ПО ослаб.
Большинству из нас также нравится запускать свой код. Немедленный feedback loop написание-запуск-исправление дает удовлетворение. Cleanroom требует отложить это удовлетворение до тех пор, пока вы полностью не продумали проблему. Для многих разработчиков это похоже на попытку писать прозу, не имея права перечитывать ее.
Что можно позаимствовать, не принимая всю религию
Вы, вероятно, не сможете внедрить полный Cleanroom в своей компании. Ваш менеджер посмотрел бы на вас странно. Но вы можете внедрить его части и получить реальную выгоду.
Пишите интерфейс до реализации. Определите входы, выходы и preconditions до того, как напишете body. Опишите, что функция обещает, а не только что она делает.
# precondition: items is a non-empty list of comparable elements
# postcondition: returns the smallest element in items
# raises: ValueError if items is empty
def min_item(items: list) -> any:
if not items:
raise ValueError("items must not be empty")
smallest = items[0]
for item in items[1:]:
if item < smallest:
smallest = item
return smallest
Это тривиальная функция, но дисциплина масштабируется. Для более сложного примера явно определите state machine перед ее реализацией.
from enum import Enum, auto
class ConnectionState(Enum):
DISCONNECTED = auto()
CONNECTING = auto()
CONNECTED = auto()
CLOSING = auto()
# Allowed transitions:
# DISCONNECTED -> CONNECTING (on connect())
# CONNECTING -> CONNECTED (on handshake complete)
# CONNECTING -> DISCONNECTED (on timeout/error)
# CONNECTED -> CLOSING (on close())
# CLOSING -> DISCONNECTED (on ack received)
# Any other transition is illegal and raises StateError
class StateMachine:
_transitions = {
ConnectionState.DISCONNECTED: {ConnectionState.CONNECTING},
ConnectionState.CONNECTING: {ConnectionState.CONNECTED, ConnectionState.DISCONNECTED},
ConnectionState.CONNECTED: {ConnectionState.CLOSING},
ConnectionState.CLOSING: {ConnectionState.DISCONNECTED},
}
def __init__(self):
self.state = ConnectionState.DISCONNECTED
def transition(self, new_state: ConnectionState) -> None:
if new_state not in self._transitions.get(self.state, set()):
raise StateError(f"Illegal transition: {self.state.name} -> {new_state.name}")
self.state = new_state
Явно кодируя допустимые переходы, вы делаете недопустимые состояния unrepresentable. Код не может войти в невалидное состояние, потому что state machine предотвращает это. Это философия box structures в миниатюре.
Пусть кто-то другой протестирует ваш код. Разделение авторства и верификации — самая радикальная и одновременно самая переносимая идея Cleanroom. Когда вы тестируете свой собственный код, вы тестируете свои собственные предположения. Кто-то другой попробует случай, который вы не рассмотрели, потому что казалось “очевидным”, что он не произойдет.
Измеряйте defect density на единицу работы. Отслеживайте, сколько багов ускользает из каждой фазы вашего процесса. Если ваш спринт систематически поставляется с integration bugs, проблема не в небрежных разработчиках. Проблема в том, что ваш процесс допускает существование integration bugs. Исправьте процесс.
Где это разваливается
Cleanroom — не универсальное решение. Он работает лучше всего, когда требования стабильны и корректность важнее time-to-market. Он плохо работает для исследовательской разработки и rapid prototyping, где цель состоит в том, чтобы выяснить, что хотят пользователи, а не корректно реализовать известную спецификацию.
Также требуется management buy-in. Вы не можете делать Cleanroom втайне. Statistical process control требует сбора данных по всей команде. Правило отсутствия запуска требует, чтобы руководство реально его применяло, даже когда приближаются дедлайны и разработчики хотят “просто быстро проверить, работает ли это”.
Вывод
0,1 дефекта на KLOC у IBM были не чудом. Это был результат процесса, спроектированного для предотвращения дефектов, а не их обнаружения. Конкретные практики — box structures, statistical testing и изоляция разработчиков — могут казаться чуждыми современной культуре разработки. Но лежащий в основе принцип вневременен: думать перед набором дешевле, чем отлаживать после.
Вам не нужно внедрять Cleanroom оптом. Начните с одной функции. Напишите ее contract до body. Попросите коллегу проверить логику перед запуском. Отслеживайте, откуда приходят ваши баги, и исправьте процесс, который их порождает. Zero defects на KLOC, вероятно, не ваша цель. Но перейти от 50 к 5 — достижимо, и метод тот же. Сначала думайте. Потом пишите. В последнюю очередь запускайте.