Статьи

Почему абстракция действительно важна

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

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

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

Затем я кратко опишу, как мы можем избежать такой ситуации.

Описание практического примера
Предположим, что у нас есть объект домена с именем  RawItem .

public class RawItem {
private final String originator;
private final String department;
private final String division;
private final Object[] moreParameters;
public RawItem(String originator, String department, String division, Object... moreParameters) {
this.originator = originator;
this.department = department;
this.division = division;
this.moreParameters = moreParameters;
}
}

Три первых параметра представляют ключ элемента.
Т.е. предмет исходит от создателя, отдела и подразделения.
«MoreParameters» просто для того, чтобы подчеркнуть, что у элемента больше параметров.

Этот триплет имеет два основных использования:
1. Как ключ для хранения в БД
2. Как ключ в картах (ключ к RawItem)

Хранение в БД на основе ключа
Таблицы БД зашиты для равномерного распределения элементов.
Разделение осуществляется с помощью хеш-ключа по модулю.
Эта функция работает со строкой.

Предположим, у нас есть N таблиц шардов: (RAW_ITEM_REPOSITORY_00, RAW_ITEM_REPOSITORY_01, .., RAW_ITEM_REPOSITORY_NN),
тогда мы будем распределять элементы на основе некоторой функции и по модулю:

String rawKey = originator + "_"  + department + "_" + division;
// func is String -> Integer function, N = # of shards
// Representation of the key is described below
int shard = func(key)%N;

Использование ключа в картах
Второе использование для триплета — отображение элементов для быстрого поиска.
Таким образом, когда НЕ используется абстракция, карты обычно будут выглядеть так:

Map<String, RawItem> mapOfItems = new HashMap<>();
// Fill the map...

«Улучшение» класса
Мы видим, что у нас есть общее использование ключа как строки, поэтому мы решили поместить строковое представление в RawItem.

// new member
private final String key;
// in the constructor:
this.key = this.originator + "_" + this.department + "_"  + this.division;
// and a getter
public String getKey() {
return key;
}

Оценка проекта
Здесь есть два потока:
1. Связь между распределением сегментирования и отображением элементов
2. Ключ отображения является строгим. любые изменения силы изменяют ключ, что может привести к трудностям в поиске ошибок

И затем приходит новое требование.
До сих пор триплет: составитель, отдел и подразделение составляли ключ элемента.
Но теперь приходит новое требование.
У подразделения может быть  подразделение .
Это означает, что, в отличие от ранее, мы можем иметь два разных предмета из одного и того же триплета. Элементы будут отличаться по атрибуту подразделения.

Сложно изменить
Что касается распределения БД, нам нужно сохранить составной ключ триплета.
Мы должны оставить функцию по модулю такой же. Таким образом, распределение останется с использованием триплетов, но схема изменится, и столбец «подразделение» также будет иметь место.
Мы изменим запросы, чтобы использовать  подразделение  вместе с оригинальным ключом.

Что касается сопоставления, нам нужно выполнить масштабный рефакторинг и передать  ItemKey  (см. Ниже) вместо просто String.

Абстракция ключа
Давайте создадим  ItemKey

public class ItemKey {
private final String originator;
private final String department;
private final String division;
private final String subdivision;
public ItemKey(String originator, String department, String division, String subdivision) {
this.originator = originator;
this.department = department;
this.division = division;
this.subdivision = subdivision;
}
public String asDistribution() {
return this.originator + "_" + this.department + "_"  + this.division;
}
}

И,

Map<ItemKey, RawItem> mapOfItems = new HashMap<>();
// Fill the map...
// new constructor for RawItem
public RawItem(ItemKey itemKey, Object... moreParameters) {
// fill the fields
}

Извлеченный урок и заключение
Я хотел показать, как простое решение может действительно навредить.

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

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

Такая ситуация была реальной
У нас на самом деле был этот поток в нашем дизайне.
Все было хорошо в течение  двух лет , пока нам не пришлось сменить ключ (добавить подразделение).
К счастью, весь наш код протестирован, чтобы мы могли увидеть, что сломалось, и исправить его.
Но это было больно.

Есть две абстракции, которые мы могли бы изначально реализовать:
1. Более очевидным является использование класса KEY (как описано выше). Даже если оно имеет только одно строковое поле
2. Любое использование карты должно быть проверено, выиграем ли мы, скрыв его с помощью абстракции

Вторая абстракция сложнее понять, полностью понять и реализовать.

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