понедельник, 18 июля 2011 г.

Разработка больших систем. Часть 3. О хорошем дизайне.

Как было упомянуто, одной из причин «плохих» багов является неверная архитектура приложения. И, хотя построение архитектуры является обязанностью архитектора, а не разработчика, разработчику полезно бывает уметь видеть стандартные ошибки в архитектуре и отличать правильную архитектуру от неправильной. Хотя бы для того, чтобы не испортить правильную архитектуру своими изменениями.

Здесь очень коротко изложу основные принципы дизайна.



Принцип отсутствия скрытых зависимостей

Если изменение в одном файле требует изменений в другом, а ты сделал первое и не сделал второе, то должна возникать ошибка на стадии компиляции.

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

Соблюсти этот принцип не так сложно, как может показаться. Достаточно мыслить каждый класс, как самостоятельную сущность, которая НИЧЕГО не знает о внутреннем строении других классов.

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

Во-вторых, каждый класс должен также относиться к тем классам и интерфейсам, которые он использует: он не должен ничего предполагать об их внутреннем строении. Он пользуется их публичным API от которого ожидает разумного поведения (исполнения их контракта). Если поведение неразумно — это повод исправить «неразумный» класс, в соответствии с контрактом, а не подстроиться под это неразумное поведение.

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

Изменение внутренностей классов становится локальным пока контракт продолжает соблюдаться. И только изменение публичных методов перестаёт быть локальным. Но оно немедленно приводит к ошибкам компиляции — все, кто использовал эти методы, более не собираются.

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

Упражнение

Назовите плохие методы в данном классе. Скажите, почему они плохие.
class AlertManager {
public List searchAlerts(String query);
public List searchAlerts2(String query);
public void processAlerts(List alerts);
public List doSearchAndThenDeleteAlertsIfBad(String query);
public Map getInternalAlertMap();
public Alert getAlertById(String id);
public void putAlert(String id,Alert alert);
}


Принцип одного контракта

В любой паре взаимодействующих классов один класс определяет весь контракт взаимодействия, а другой управляет этим взаимодействием, согласно этому контракту.
То есть если класс А вызывает публичное API класса Б, то класс Б не только не имеет права вызывать публичное API класса А, но и вообще не должен иметь прямую ссылка на класс А. Если классу А необходимо получать информацию о каких-то событиях от класса Б, то класс Б должен иметь в своем API методы для подписывания на данный вид информации, а класс А — использовать эти методы, согласно контракту класса Б.

Другими словами, из двух взаимодействующих классов ОДИН (в данном случае класс Б) определяет ВЕСЬ контракт взаимодействия. А другой управляет, оставаясь в рамках этого контракта.

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

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

Принцип 80-20

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

Например, был метод

doSearch(String query);

А вам хочется сделать из него метод

doSearch(String query,boolean useNewAdvacedApproach, double weightForDescriptionField,SpecialSearchParameters params);

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

Принцип незавоевания

Ваша библиотека, API , компонент (и вообще что угодно) НЕ ДОЛЖНО конфликтовать с использованием стандартных способов разработки, существовавших до вас. Другими словами - дополняйте, но не подменяйте. Должно допускаться "смешанное" кодирование: с использованием вашего API и стандартного вместе.

Этот принцип проще показать на примере.

Классический пример - ненавидимый мною проект Apache Cayeene. В этой ORM системе авторы додумались до требования наследовать все объекты модели от их собственных классов. Очевидно, что если вы возьмете две библиотеки, написанные столь блестящим образом, и решающие разные задачи (например Cayeene и столь же глупую библиотеку, служащую, скажем для сериализации объектов), то вы не сможете совместить их в одном приложении. Или одна или другая завоюет его все.

Комментариев нет: