среда, 20 февраля 2013 г.

Основные постулаты ООП - взгляд с другой стороны

Попалась тут намедни одна статья, в которой автор рьяно критикует основные постулаты ОО-парадигмы. Ну, по крайней мере, их воплощение и применение при программировании на Delphi. Я соглашусь с его первым тезисом о том, что формат комментариев слишком тесен для "развития темы", и отвечу полноценным постом. Так вот. До сего момента я полагал (возможно, наивно), что приобретя определенный опыт в программировании разработчик начинает понимать - зачем и для чего все эти концепции нужны. Ан, оказывается, нет. Поскольку мне по долгу службы со всеми этими концепциями приходится работать достаточно плотно, то возьму на себя смелость объяснить - что к чему. Начну, пожалуй, с конца.

Интерфейсы

Нет, сейчас речь пойдет не о тех, которые с GUID'ами (о них - позже), а о самой концепции, о понятии "интерфейс" в широком смысле. А в широком смысле интерфейс - это своего рода контракт. Или, говоря иначе, это договор между объектом, который собирается использовать интерфейс (клиентом), и объектом, который этот интерфейс реализует (реализацией). Реализация интерфейса подписывается под тем, что выполняет все обещания и требования, предписываемые контрактом, а клиент, в свою очередь, обязуется не требовать большего. В этом смысле интерфейс ничем не отличается от трудового договора, который подписывает каждый программист при приеме на работу. Если договор простой и ясный - взаимодействие в его рамках происходит четко и эффективно. Если же формулировки договора размыты, аморфны, то рано или поздно это начинает приводить к тем или иным проблемам.
Для того, чтобы сделать контракт ясным - нужно хорошо понимать, какие роли будут отводиться каждой из его сторон, а для этого нужны четко сформулированные требования. Если требований нет, то становится не понятно - что и зачем делается, и, как следствие, возникают вопросы - "а зачем все это вообще надо?". Итак, в итоге получается следующая связка: требования -> интерфейсы -> реализация. При этом, интерфейс не обязан буквально повторять те функции, которые описаны в требованиях, но клиент интерфейса в том или ином виде должен все эти функции в свое распоряжение получить.
Процесс такого проектирования и описания интерфейсов нельзя назвать простым. Он требует определенных усилий. Но если провести его в полном объеме и грамотно, то в внутри разрабатываемого приложения будут проведены четкие границы между различными подсистемами (а также отдельными классами), внутри которых разработчик будет обладать определенной свободой действий. И границы эти непрозрачны.

Инкапсуляция

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

Наследование

В проектируемой системе всегда можно выделить группы классов, которые в схожих ситуациях должны (или будут) вести себя схожим образом. Ну, например, таким общим поведением могут описываться элементы контейнеров. Или элементы пользовательского интерфейса. В случае появления таких групп можно утверждать, что аспекты их общего поведения также описываются интерфейсом. По сути, реализация такого общего интерфейса и есть наследование. Все производные от некоторого класса (или интерфейса - см. ниже) классы обещают поддерживать контракт своего родителя. Тут, опять же, никакой глубокой философии, параллелей с реальным миром и т. п. не требуется. Группировка чисто логическая, и зависит от задач и требований. Собственно, сколько контрактов класс обязуется поддерживать, столько интерфейсов он должен унаследовать и реализовать.

Интерфейсы (те, которые с GUID'ами)

Это, по сути, тот же контракт (набор методов и обязательств), только максимально формализованный. Или, иначе говоря, интерфейс в узком смысле. Такой интерфейс используется для фиксации контракта в коде. Ну и (в качестве бонуса) для уменьшения связности между разными частями приложения. Ведь такого рода интерфейс только лишь обещания, которые конкретная его реализация обязуется выполнить. Лично я идеальным вариантом считаю такой, при котором разные компоненты системы взаимодействуют между собой посредством таких вот интерфейсов (в узком смысле), и не знают друг о друге никаких подробностей. Меньше знаний -> меньше связность -> больше свободы.

Бонус

Все вышеперечисленное является элементами "контрактного программирования" (design by contract) и, строго говоря, не накладывает на разработчика ограничений по использованию исключительно ООП-парадигмы. Разработку можно вести хоть в процедурном, хоть в функциональном, хоть в любом другом стиле. Главное, чтобы перечисленные принципы соблюдались. Ко всему вышеперечисленному обычно ещё добавляются такие элементы описания контрактов, как предусловия, постусловия и инварианты. Эти элементы используются для максимально полного описания ответственности клиента контракта и его реализации, но об этом как-нибудь в другой раз.

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

Отправить комментарий