Рассказы о программной инженерии — 1

Однажды команде разработчиков пришлось очистить интерфейс между двумя компонентами системы баз данных, компонентами Storage и Evaluation. Оценка извлеченных (сжатых) значений из хранилища через этот интерфейс. Чтобы получить значения, оценочный код должен был пройти через запутанные круги малопонятных структур данных LookupInfo и LookupHelper, которые были столь же уродливыми, сколь и неэффективными. Они снова и снова поднимались на вершину Profiles. Они не были задокументированы, и никто больше не знал, чем они занимались…

Вот кое-что из того, что им пришлось убирать:

LookupConstraint lookup_constraint(…);
Store *store = db->store->GetStoreType(lookup_constraint).first;
LookupHelper lookup_helper(lookup_constraint);
LookupInfo info;
store->Find(&lookup_helper, &info);
bool needs_filter = 
NeedFilter((&lookup_helper)->lookup_constraint, store->key_order);
bool record_key_lookup = store->IsRecordKeyLookup((&lookup_helper)->lookup_constraint);
Iterator* iter = new Store::Iterator(store->store_.get(), info.page_offset, info.record_offset, info.filter, needs_filter, record_key_lookup);

Начат рефакторинг. О чудо, команда упустила идею разделения и инкапсуляции. Вот что они произвели:

struct LookupData {
 const uint64_t cost_;
 const RecordHeader* header; 
};
struct Store {
 uint64_t CalculateHash(const string& key) const; 
 void Prefetch(uint64_t hash) const;
 LookupData Lookup(const string& key, uint64_t hash) const;
};

Это было действительно проще, чем первоначальный беспорядок. Но они позволяют деталям Хранилища (точнее, фактической структуре памяти объектов в Хранилище) просачиваться в Оценку — этот указатель RecordHeader, возвращаемый в LookupData, указывает прямо на адрес в Хранилище памяти. Они призвали Простоту и Скорость. Они были непреклонны в том, что любая дальнейшая абстракция обязательно противоречила бы Спиду. Они даже беспокоились о Prefetching…

Оценка стала глубоко запутанной с расположением памяти в хранилище. Он стал многословным, его было трудно читать и еще труднее рассуждать. Он превратился в страницу за страницей вложенных циклов и операторов if-then-else, так как прямое чтение значений из макета хранилища было непростым. Они не видели, что Хранилище должно полностью владеть расположением памяти Ценностей. Это Хранилище должно нести ответственность за декодирование Ценностей для своих Клиентов. Они сказали, что есть только один Клиент (Оценка), так что в любом случае интерфейс не нужен. Тщательный интерфейс был бы необходим только в том случае, если бы было несколько клиентов, но никогда не было бы нескольких клиентов, они были уверены в этом. И код в любом случае должен был быть где-то написан, верно? Действительно ли имело значение где? Они также были уверены в том, что им никогда не придется менять структуру памяти. Таким образом, они не видели проблем с копированием и вставкой кода для повторения и распаковки значений из другой части системы. Это было на самом деле «проще» и «эффективнее»… Конечно, с тех пор им пришлось менять компоновку Хранилища 3 раза.

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

template <class Key, class Value> 
class Store {
public:
class Iterator { // encapsulates details of store
 public:
  Value operator*(); // dereference
  void operator++(); // increment
  friend bool operator==(Iterator& other); 
 private:
  ...
 };
struct LookupData {
  const uint64_t cost_; 
  Iterator begin, end;
 };
LookupData Lookup(const Key& key) const;
private:
 ...
};

Эти Инженеры в параллельной вселенной знали о старой идее STL об отделении Контейнеров от Алгоритмов и обеспечении их взаимодействия через Итераторы. Они долго и упорно обдумывали эту идею и обнаружили, что она может быть полезна во многих ситуациях. И за годы напряженной работы они пришли к выводу, что идея разделения и инкапсуляции ответственности была хорошей идеей. Они проанализировали интерфейс между хранилищем и оценкой, чтобы выявить обязанности и требования каждого из них. Они пришли к выводу, что Evaluation требуется не больше и не меньше, чем итератор над значениями в хранилище. Итератор будет давать значения оценке, заботясь о таких деталях, как структура памяти и распаковка. Они решили, что Оценочный код не должен проникать во внутренности Хранилища. Им это казалось проще и чище. Он также обещал более простое сопровождение и эволюцию, а также разделение труда и более простое тестирование по частям.

Они упаковали не только декодирование Values ​​в operator* разыменования, но и, намного позже, некоторые инструкции предварительной выборки в incrementoperator++. Это значительно увеличило скорость их Iterator. Их Хранилище имело четко определенную единственную роль хранения Ценностей. Единственная роль их итератора заключалась в эффективном переборе значений, и они сделали его точные обязанности явными, поняв, что итератор ввода — это именно то, что им нужно. И их оценочный код стал короче и читабельнее. Он не был обременен скучным, вырезанным и вставленным кодом для распаковки значений. Также было легче рассуждать, потому что обязанности были четко разделены и упакованы в небольшие блоки. Они получали дополнительные баллы, написав модульные тесты для Итератор. Это значительно увеличило скорость их разработки, потому что когда они выпустили Iterator, в нем не было ошибок.

Интерфейс у них работал. Это не было утечкой. У них были четкие и простыеединицы рассуждений и тестирования для структурирования их программного обеспечения, без неявных предположений. Они могли строить из этих кирпичей безопасно и быстро. Они успешно поняли и применили ключевую концепцию разработки программного обеспечения: разделить обязанности, изолировать и упаковать их так, чтобы их было легко понять, протестировать и повторно использовать. Затем вы можете комбинируйте эти элементарные детали, как кирпичики Lego.