Не зависящий от языка закон Деметры выражает принципы инкапсуляции и модульности в виде простого для применения правила, адресованного объектно-ориентированным программистам.

Когда можно сказать, что объектно-ориентированная программа написана в хорошем стиле? Существует ли некая формула или правило, следуя которому можно писать хорошие объектно-ориентированные программы? Какие метрики можно применить к объектно-ориентированной программе, чтобы определить, что она хорошо написана? Каковы отличительные характеристики хорошо написанных объектно-ориентированных программ? 

В этой статье мы предлагаем простой закон, именуемый законом Деметры, который, как мы полагаем, отвечает на все эти вопросы и помогает формализовать идеи, уже представленные в литературе [1, 2]. Существует два типа стилистических правил объектно-ориентированного проектирования и программирования: правила, относящиеся к структуре классов, и правила, касающиеся написания методов. Здесь мы сосредоточимся на стилистических правилах, которые ограничивают способы написания методов для заданного набора определений классов. Стилистические правила, определяющие структуру классов, уже были нами описаны в другой публикации [3].

Закон Деметры ограничивает структуру отправки сообщений в методах. Говоря неформально, закон предписывает, что каждый метод может отправлять сообщения лишь ограниченному набору объектов: объектам-аргументам, псевдопеременной self и непосредственным составным частям self. (Конструкция self в языках Smalltalk и Flavors называется this в С++ и Current в Eiffel.) Иными словами, каждый метод зависит от ограниченного набора объектов.

Цель закона Деметры — упорядочить и сократить зависимости между классами. Говоря неформально, один класс зависит от другого, если он вызывает функцию, определенную в другом классе. Мы полагаем, что закон Деметры способствует сопровождаемости и понятности кода, однако строгое доказательство этого потребовало бы масштабного эксперимента со статистической оценкой. Поскольку область объектно-ориентированного программирования сравнительно молода, крупные проекты, которые могли бы предоставить данные о преимуществах лучшего управления зависимостями, встречаются редко. Тем не менее, мы проанализировали наш собственный код (около 14 тысяч строк на Flavors и C++) и убеждены в преимуществах этого закона.

Мы сформулировали этот закон в ходе проектирования и реализации системы «Деметра», поэтому и назвали его в ее честь. «Деметра» предоставляет высокоуровневый интерфейс для основанных на классах объектно-ориентированных систем (краткое описание «Деметры» приведено во второй врезке; более полные описания представлены в других работах [3, 4]. Примеры в этой статье написаны в нотации «Деметры», которая также поясняется во врезке). Такой высокоуровневый интерфейс обеспечивает среду, в которой код может развиваться непрерывно, а не отдельными скачками.

Чтобы обеспечить такое непрерывное развитие, хотелось бы, чтобы программы были в некотором смысле хорошо управляемыми и стройными. Иными словами, необходимо, чтобы программы соблюдали определенный стиль, позволяющий легко вносить в них изменения, сводя к минимуму правки в других частях программ. Легкость внесения изменений — один из критериев, характеризующих хороший стиль объектно-ориентированного программирования. Следование закону Деметры позволит достичь хорошего стиля при условии, что программист следует и другим известным стилистическим правилам, таким как минимизация дублирования кода, минимизация числа аргументов и минимизация количества методов.

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

В одной из предыдущих статей [5] мы привели доказательство того, что любую объектно-ориентированную программу, написанную в плохом стиле, можно систематически преобразовать в хорошо структурированную программу, соответствующую закону Деметры. Из этого доказательства следует, что закон Деметры ограничивает не то, что может решить программист, а лишь то, как он это решает. 

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


Определения для закона Деметры

Клиент и поставщик. Точное определение понятия класса-знакомого основывается на понятиях клиента и поставщика: 

Клиент. Метод M является клиентом метода f, принадлежащего классу C, если внутри M сообщение f отправляется объекту класса C или самому классу C. Исключение: если метод f специализирован в одном или нескольких подклассах, то M считается клиентом только того метода f, который принадлежит старшему классу в иерархии подклассов. Метод M является клиентом класса C, если M — клиент какого-либо метода, принадлежащего классу C.

Поставщик. Если метод M — клиент класса C, как описано выше, то класс C является поставщиком метода M. Говоря неформально, класс-поставщик для метода M — это класс, чьи методы вызываются внутри M.

Класс-знакомый. Точное определение класса-знакомого:
Класс C1 является классом-знакомым метода M, принадлежащего классу C2, если C1 — поставщик метода M, но при этом C1 не является: 

  • классом аргумента метода M, включая C2;

  • классом переменной экземпляра C2;

  • суперклассом любого из вышеперечисленных классов. 

Говоря неформально, класс-знакомый метода M — это класс-поставщик, который не является классом аргумента M или переменной экземпляра класса, которому принадлежит M 

Предпочтительный класс-знакомый. Предпочтительным классом-знакомым метода M является либо класс объектов, непосредственно создаваемых в M (путем вызова конструктора класса-знакомого), либо класс глобальной переменной, используемой в M

Предпочтительный класс-поставщик. Предпочтительные классы-поставщики формально определяются через предпочтительные классы-знакомые: 

Класс B является предпочтительным классом-поставщиком метода M (принадлежащего классу C), если B — поставщик M и выполняется одно из следующих условий:

  • B является классом переменной экземпляра C или суперклассом такого класса;

  • B является классом аргумента M, включая C или суперкласс такого класса;

  • B является предпочтительным классом-знакомым M

Говоря неформально, предпочтительные классы-поставщики состоят из предпочтительных классов-знакомых метода, а также из классов его переменных экземпляра и аргументов. Отношения между классами-поставщиками, классами-знакомыми и их предпочтительными подмножествами показаны на рис. A.

Рис. A. Отношения между классами-поставщиками и классами-знакомыми и их предпочтительными подмножествами.
Рис. A. Отношения между классами-поставщиками и классами-знакомыми и их предпочтительными подмножествами.

Формы закона

Закон Деметры существует в двух формах: форма для классов и форма для объектов. Форма для классов имеет две версии: минимизационную и строгую. Строгая версия представляет собой особую форму для классов, которая жестко ограничивает зависимости между ними. Минимизационная версия допускает некоторые дополнительные зависимости между классами, но требует их минимизации и документирования посредством объявления классов-знакомых.

Форма для классов. Обе версии формы для классов выражены в понятиях классов и могут поддерживаться инструментом для контроля соблюдения правил.

Любой класс в объектно-ориентированном проекте или программе является потенциальным поставщиком для любого метода. Однако рекомендуется ограничивать поставщиков метода небольшим набором предпочтительных классов. Чтобы определить эти предпочтительные классы-поставщики, мы ввели понятие класса-знакомого [6, 7]. Точное определение класса-знакомого основывается на понятии поставщика; соответствующие точные определения приведены в первой врезке. Говоря неформально, класс-поставщик метода — это класс, чьи методы вызываются в этом методе. Класс-знакомый метода — это класс-поставщик, который не является ни классом аргумента, ни классом переменной экземпляра. Предпочтительный класс-знакомый метода — это либо класс объектов, непосредственно создаваемых в методе (путем вызова конструктора класса-знакомого), либо класс глобальной переменной, используемой в методе.

Как правило, классы-знакомые используются по трем причинам: 

  • Стабильность: если класс является стабильным или его интерфейс будет сохранять обратную совместимость, имеет смысл использовать его в качестве класса-знакомого во всех методах. Пользователь задает такие глобальные классы-знакомые отдельно, и они включаются в число классов-знакомых для всех методов.

  • Эффективность: для повышения эффективности пользователю может потребоваться доступ к переменным экземпляра других классов. В терминологии C++ это классы, для которых данный метод является дружественной функцией.

  • Создание объекта.

Минимизационная версия. Проще всего сформулировать минимизационную версию формы закона Деметры для классов: 

Сведите к минимуму количество классов-знакомых во всех методах. 

Мы подсчитываем общее количество классов-знакомых для всех методов: если класс является классом-знакомым для нескольких методов, он учитывается в подсчете столько раз, сколько встречается. 

Если статически типизированный язык, такой как C++ или Eiffel, расширить, добавив возможность объявления классов-знакомых, то не составит труда модифицировать компилятор для проверки соблюдения минимизационной версии следующим образом: каждый поставщик, который является классом-знакомым, должен быть объявлен в списке классов-знакомых этого метода.

Чтобы обеспечить простую проверку соблюдения закона на этапе компиляции или даже на этапе проектирования, пользователь должен указывать для каждого метода следующую информацию: (1) типы всех аргументов и результата и (2) классы-знакомые. Эта информация предоставляет читателю метода перечень типов, необходимых для понимания метода. Программа для контроля соблюдения правил должна отслеживать следующую дополнительную информацию о каждом методе: (1) отправку сообщений внутри метода и (2) классы объектов, непосредственно создаваемых методом.

Строгая версия. Строгая версия формы закона Деметры для классов утверждает: 

Все методы могут использовать только предпочтительные классы-поставщики. 

Эти классы формируются из предпочтительных классов-знакомых метода, а также классов переменных экземпляра и классов аргументов. (Точные определения приведены в первой врезке.) По сути, строгая версия основана на ограничении использования классов-знакомых.


Обзор системы «Деметра»

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

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

Примерами утилит, которые «Деметра» генерирует или применяет в общем виде, являются: определения классов на целевом языке, каркасы приложений, парсеры, форматтеры, проверщики типов, редакторы объектов, минимизаторы перекомпиляции, сопоставители шаблонов и унификаторы. Система «Деметра» помогает пользователю определять классы (как их структуру, так и высокоуровневую функциональность) с помощью нескольких вспомогательных инструментов, таких как средство проверки согласованности (семантические правила и проверка типов на уровне проектирования), обучаемый инструмент, который выводит определения классов из примеров описаний объектов, LL(1)-корректор для сканирования слева направо с одним упреждающим токеном, создающий левосторонний вывод, генератор скриптов на основе списков желаний и генератор плана разработки приложений.

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

class ReferenceSec has parts
ref_book_sec : BooksSec
    archive : Archive
end class ReferenceSec.

сlass Archive has parts
arch_microfiche : MicroficheFiles
    arch_docs : Documents
end class Archive.

class BooksSec has parts
    ref_books : ListofBooks
    ref_catalog : Catalog
end class BooksSec.

class ListofBooks is list
    repeat {Book}
end class ListofBooks. 

class Catalog is list
    repeat {Catalog_Entry}
end class Catalog.

 class Book has parts
   title : String
    author : String
    id : BookIdentifier
end class Book.

Определение-конструкция используется для построения класса из других классов и имеет вид:

 class C has parts 
   part_name_1 : SC_1
    part_name_2 : SC_2
    …
    part_name_n : SC_n 
end class C.                                                                                           

Объект класса C определяется как состоящий из n частей (называемых значениями его переменных экземпляра), причем каждая часть имеет имя (называемое именем переменной экземпляра), за которым следует тип (называемый типом переменной экземпляра). Это означает, что для любого экземпляра (или элемента) класса C имя part_name_i ссылается на элемент класса SC_i. Следующий пример описывает класс библиотеки, состоящий из справочного отдела, абонемента и отдела периодики:

class Library has parts
    reference: ReferenceSec
    loan: LoanSec
    journal: JournalSec
end class Library.

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

Определение-альтернатива позволяет выразить тип-объединение. Определение класса вида

class C is either
    A or B
end class C.

указывает, что элемент класса C является элементом либо класса A, либо класса B (исключающее «или»). Так, определение

class Book_Identifier is either
    ISBN or LibraryOfCongress
end class Book_Identifier.

выражает идею, что ссылка на идентификатор книги — это фактически ссылка либо на код ISBN, либо на код Библиотеки Конгресса. 

Определение-повторение — это просто разновидность определения-конструкции, в котором все части имеют один и тот же тип, и количество этих частей не задается. Определение вида 

class C is list
    repeat {A}
end class C.

указывает, что элементами класса C являются списки из нуля или более элементов класса A.

Нотация. В системе «Деметра» мы используем два вида нотации: сокращенную, основанную на расширенной форме Бэкуса — Наура (EBNF), и расширенную — нашу собственную, которая в значительной степени интуитивно понятна. В этой статье мы используем нашу расширенную нотацию. Абстрактный синтаксис сокращенной и расширенной нотаций идентичен: меняется лишь синтаксический «сахар».


На рис. 1 приведены пять примеров определения предпочтительных поставщиков. Чтобы отправить сообщение f объекту s, мы используем нотацию вызова функций из C++ (s -> f() эквивалентно «отправить сообщение f объекту s»). На рис. 1 класс B является предпочтительным поставщиком метода M.

Использование строгой версии формы закона для классов имеет ряд преимуществ.

Например, при изменении интерфейсов классов от C₁ до Cₙ модифицировать потребуется лишь предпочтительные клиентские методы этих классов. Как правило, предпочтительные клиентские методы класса образуют лишь небольшое подмножество всех методов программы; это существенно сокращает количество методов, подлежащих модификации. Данное преимущество наглядно показывает, что закон Деметры сдерживает распространение последствий изменений. Мы рассмотрели набор классов в этом примере, поскольку изменение интерфейса группы классов — это типичная задача, возникающая из-за зависимостей между их интерфейсами.

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

Применение закона также позволяет управлять сложностью программирования. Например, при чтении метода необходимо принимать во внимание лишь функциональность его предпочтительных классов-поставщиков. Как правило, эти предпочтительные поставщики образуют лишь небольшое подмножество всех классов приложения и к тому же тесно связаны с классом, к которому принадлежит метод. Такая связь облегчает запоминание этих классов и их функциональности.

Форма для объектов. Не составит труда расширить компилятор C++ для проверки соблюдения строгой версии формы для классов, однако платой за возможность проверки на этапе компиляции станет то, что одни программы, нарушающие дух закона, будут проходить проверку, а другие, следующие духу закона, — отвергаться. Версия закона для объектов утверждает: 

Все методы могут использовать только предпочтительные объекты-поставщики. 

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

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

Форма для объектов работает с предпочтительными объектами-поставщиками, аналогичными предпочтительным классам-поставщикам. Объектом-поставщиком для метода является любой объект, которому в этом методе отправляется сообщение. К предпочтительным объектам-поставщикам относятся: 

  • непосредственные части псевдопеременной self

  • объекты-аргументы метода (включая псевдопеременную self); 

  • объекты, либо напрямую создаваемые в методе, либо хранящиеся в глобальных переменных.

Программист определяет гранулярность «непосредственных составных частей» self для конкретного приложения. Например, непосредственными частями класса-списка являются элементы этого списка. Непосредственными частями объекта обычного класса являются объекты, хранящиеся в его переменных экземпляра.


;класс переменной экземпляра:
class C has parts
    s : B
    implements interface 
        M() returns Ident 
            {calls s -> f()}
end class C.

;класс аргумента:
class C has parts
    ; отсутствуют 
implements interface
M(s : B) returns Ident
    {calls s -> f()}
end class C.

 ;класс аргумента:
class B has parts
; отсутствуют
implements interface
    M() returns Ident
        {calls self -> f()}
        ; в C++ self называется this
end class B.

 ;класс создаваемого объекта:
class C has parts
    ; отсутствуют
implements interface
    M() returns Ident
    ; new_object — это новый объект
; класса B
    {calls new_object -> f()}
end class C.

 ; s — глобальная сущность типа B
class C has parts
    ; отсутствуют
    implements interface
        M() returns Ident
            {calls s -> f()}
end class C.

Рис. 1. Примеры определений клиента и поставщика. Строки с комментариями начинаются с точки с запятой.


Принципы

Задача закона Деметры — обеспечить максимально возможную модульность программного обеспечения. Закон позволяет эффективно сократить количество вложенных отправок сообщений (вызовов функций) и тем самым упрощает методы.

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

Принципы, охватываемые законом: 

• Управление связанностью. Широко известный принцип проектирования программного обеспечения — стремиться к минимальной связанности между абстракциями (такими как процедуры, модули и методы). Связанность может проявляться в различных типах связей. Одним из важных типов связей для методов является связь «использует» (или связь «вызов/возврат»), которая возникает при вызове одного метода другим. Закон Деметры существенно сокращает количество методов, доступных для вызова внутри данного метода, и тем самым ограничивает связанность методов по отношению «использует». Благодаря этому закон способствует повторному использованию методов и повышает уровень абстракции программного обеспечения.

• Сокрытие информации. Закон Деметры обеспечивает один из видов сокрытия информации [8]: сокрытие структуры. В общем случае закон запрещает методу напрямую обращаться к составной части объекта, находящейся на глубоком уровне его иерархии «является-частью». Вместо этого необходимо использовать промежуточные методы для пошагового обхода этой иерархии небольшими контролируемыми шагами. В некоторых объектно-ориентированных системах пользователь может защитить от внешнего доступа некоторые переменные экземпляра или методы класса, объявляя их приватными. Эта важная возможность дополняет действие закона, усиливая модульность. Но закон полезен даже в системах, поддерживающих сокрытие данных: он продвигает идею о том, что даже публичные переменные экземпляра и методы следует использовать ограниченным образом.

• Ограничение информации. Наша работа перекликается с работами Дэвида Парнаса и его коллег [9] в области модульной структуры сложных систем. Чтобы снизить затраты на модификацию бортового программного обеспечения самолета A-7E, они ограничили использование модулей, предоставляющих изменяемую информацию. В объектно-ориентированном программировании мы последовательно придерживаемся этого подхода, предполагая, что любой класс может измениться. Поэтому мы ограничиваем использование отправки сообщений, применяя закон Деметры. Ограничение информации дополняет сокрытие информации: вместо того чтобы скрывать определенные методы, вы делаете их публичными, но ограничиваете возможности их использования.

• Локализация информации. Многие учебники по программной инженерии подчеркивают важность локализации информации, и закон Деметры направлен на локализацию информации о типах. При анализе метода необходимо учитывать лишь классы, тесно связанные с классом, к которому принадлежит метод. Фактически, можно оставаться в неведении (и независимости) от остальной части системы. Как гласит поговорка, блаженство в неведении. Этот важный аспект закона помогает снижать сложность программирования. Закон также ограничивает видимость имен сообщений: в методе можно использовать только те имена сообщений, которые входят в интерфейсы предпочтительных классов-поставщиков. Это также способствует локализации информации.

• Структурная индукция. Закон Деметры связан с фундаментальным тезисом денотационной семантики: значение сложного выражения есть функция от значений его частей. Эта идея восходит к работе Фреге о принципе композициональности [10]. Этот принцип позволяет использовать структурную индукцию для доказательства свойств программ, таких как корректность относительно спецификации.

Пример

Чтобы продемонстрировать применение закона Деметры, рассмотрим программу, нарушающую как строгую, так и минимизационную версии формы этого закона для классов. В этом примере мы используем классы, определенные во фрагменте словаря классов для библиотеки на рис. 2.

В C++ отправка сообщения означает вызов (виртуальной) функции-члена. В примерах на C++ типы данных-членов и аргументов функций являются типами указателей на классы. Хотя примеры даны на C++, понятия, которые мы используем для объяснения программы, также будут понятны пользователям Smalltalk и Flavors.

Фрагмент программы на C++ на рис. 3 описывает книги в справочном отделе. (Чтобы сократить объем кода в примере, мы используем прямой доступ к переменным экземпляра вместо методов доступа.) Функция search_bad_style, принадлежащая ReferenceSec, передает сообщение отделам книг (BooksSec), микрофильмов (MicroficheFiles) и документов (Documents).

Эта функция нарушает закон Деметры. Первое сообщение, помеченное /**/, отправляет сообщение arch_microfiche объекту archive, который возвращает объект типа MicroficheFiles. Затем этот метод отправляет возвращенному объекту сообщение search. Однако MicroficheFiles не является ни переменной экземпляра, ни типом аргумента класса ReferenceSec. 

Поскольку структура всех классов четко определена словарем классов, может возникнуть соблазн принять метод search_bad_style на рис. 3 как допустимое решение, хотя он и нарушает закон Деметры. Но представим, что требуется внести изменение в словарь классов. Допустим, библиотека внедряет новую технологию и заменяет отделы микрофильмов и документов в объекте archive на CD-ROM или видеодиски: 

class Archive has parts
cd_rom_arch : CD_ROM_File
end class Archive.

class CD_ROM_File has parts
    cd_c_system : ComputerSystem 
    discs : CD_ROM _Discs
end class CD_ROM_File.

Теперь придется искать во всех методах, включая search_bad_style, ссылки на архив с микрофильмами. Гораздо проще было бы ограничить изменения только методами класса Archive. Этого можно добиться, переписав методы в хорошем стиле, что приведет к появлению функций search_good_style в классах ReferenceSec и Archive.


class Library has parts
    reference : ReferenceSec
    loan : LoanSec
    journal : JournalSec 
end class Library.

class ReferenceSec has parts
    ref_book_sec : BooksSec
    archive : Archive
end class ReferenceSec.

class Archive has parts 
    arch_microfiche : MicroficheFiles
    arch_docs : Documents
end class Archive.

class MicroficheFiles has parts

end class MicroficheFiles.

class Documents has parts

end class Documents.

class BooksSec has parts

end class BooksSec.

Рис. 2. Фрагмент словаря классов для библиотеки.



class ReferenceSec {
public:
    Archive* archive;
    BookSec* ref_book_sec;
 
boolean search_bad_style (Book* book) {
    return
        (ref_book_sec -> search(book) ||
/**/ archive -> arch_microfiche -> search(book) || 
/**/ archive -> arch_docs -> search(book)); 
        }
 
boolean search_good_style (Book* book) {
    return
        (ref_book_sec -> search(book) ||
        archive -> search_good_style(book)); 
    }
};
 
class Archive {
public:
    MicroficheFiles* arch_microfiche;
    Documents* arch_docs;
 
boolean search_good_style (Book* book) {
    return
    (arch_microfiche -> search(book) ||
    arch_docs -> search(book));
    }
};
 
class MicroficheFiles {
public:
    boolean search(Book* book) {}
};
 
class Documents {
public:
    boolean search(Book* book) {}
};
 
class Book {
    …
};

Рис. 3. Фрагмент на C++ для поиска книги в справочном отделе. Функция search_bad_style нарушает закон Деметры.


Использование хорошего стиля также ослабляет связанность по отношению «использует»: в исходной версии ReferenceSec был связан с BooksSec, Archive, MicroficheFiles и Documents, а теперь связан только с BooksSec и Archive.

Еще один способ оценить эффект применения закона Деметры — преобразовать программу (как в хорошем, так и в плохом стиле) в граф зависимостей. В таких графах узлами являются классы. Ребро от класса A к классу B помечено целым числом, которое указывает, сколько вызовов методов класса B совершают методы класса A. Если метка на ребре отсутствует, ее значение считается равным 1. Доступ к переменной экземпляра интерпретируется как вызов чтения этой переменной. На рис. 4a показан граф для программы, нарушающей закон Деметры; на рис. 4b — для программы, которая его соблюдает.


Рис. 4. Граф зависимостей для (a) кода в плохом стиле с рис. 3 и (b) кода в хорошем стиле. Числа на ребрах между классами указывают количество вызовов функций одного класса к функциям другого.
Рис. 4. Граф зависимостей для (a) кода в плохом стиле с рис. 3 и (b) кода в хорошем стиле. Числа на ребрах между классами указывают количество вызовов функций одного класса к функциям другого.

Допустимые нарушения

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

В качестве примера ситуации, когда затраты на применение закона превышают выгоду, рассмотрим следующий типичный метод, написанный в плохом стиле, в двух вариантах — на Flavors и C++. На Flavors:

 (defmethod (C:M) (p)
    (... (send (send p :F1) :F2) ...))

На C++:

void C::M(A* p)                                                                             
    {p -> F1() -> F2();
        // …
    }

Здесь p — экземпляр класса A, а F1 возвращает составную часть p. Если непосредственный состав класса A изменится, метод M также, возможно, придется изменять из-за F1.

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

Рассмотрим еще один пример, где затраты на следование закону могут превышать выгоду. Для приложения, решающего дифференциальные уравнения, словарь классов может содержать следующие определения: 

class Complex_Number has parts
    real_part : Real 
    imaginary_part : Real
end class Complex_Number.

На Flavors код, использующий эти определения, может выглядеть так:

(defmethod (Vector :R) (c)
    ( …
        (send (send c :real_part)
        :project self) ...))

Тот же код на C++:

void Vector::R(Complex_Number* c)
{
    с -> real_part -> project(this)
}

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

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

Следование закону Деметры при написании программ снижает частоту вложенной отправки сообщений и уменьшает сложность методов, но ведет к увеличению их количества. Рост числа методов может привести к проблеме избыточного количества операций у типа [8]. В этом случае применение закона может сделать абстракцию менее понятной, а реализацию и сопровождение — более сложными. Также может возрасти количество аргументов, передаваемых в некоторые методы.

Соответствие

Если метод не соответствует закону, как его преобразовать, чтобы добиться соответствия? В нашей предыдущей работе [5] мы описали алгоритм преобразования любой объектно-ориентированной программы в эквивалентную программу, удовлетворяющую строгой версии закона. 

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

Исходя из следующего рекурсивного определения, мы используем эти техники для преобразования программ в форму, удовлетворяющую закону Деметры. Оно звучит так: B является классом-частью A, если B является классом переменной экземпляра A или если B является классом-частью класса переменной экземпляра A.

Рассмотрим следующую программу, нарушающую строгую версию закона. На Flavors метод выглядит так:

(defmethod (C :M) ()
    (send (send self :m1) :m2))

 а на С++ — так: 

void C::M()
    {this -> m1() -> m2();}

T — класс объекта, возвращаемого m1. Это не предпочтительный класс-поставщик M. Мы различаем два случая:

  • T является классом-частью C

  • C является классом-частью T.


class Grammar is list 
    repeat {Rule} 
end class Grammar.

class Rule has parts
    body: Body 
end class Rule. 

 (a)

(defmethod (Grammar :parse) (rule_name)
    (send (send rule :look_up rule_name) :parse_details))

(defmethod (Grammar :look_up) (rule_name)
    …
    (send (send rule :look_up rule_name) :get_body))

(defmethod (Body :parse_details) ()
    …
)

 (b)

void Grammar::parse(Symbol* rule_name)
    {this -> look_up(rule_name) -> parse_details();}
 
Body* Grammar::look_up(Symbol* rule_name)
    { …
        return rule -> look_up(rule_name) -> get_body();
    }
 
void Body::parse_details()
    { … }

(c)

(d)

Рис. 5. Пример кода, нарушающего закон Деметры: (a) словарь классов, (b) код на Flavors, (с) код на С++ и (d) соответствующий граф зависимостей.



(a)

void Grammar::look_up(Symbol* rule_name)
    {this -> look_up(rule_name) -> parse_details();}

Rule* Grammar::look_up(Symbol* rule_name)
    {... return rule -> look_up(rule_name);}

void Rule::parse_details()
    {... this -> get_body(); …}

 (b)

(c)

Рис. 6. Вариант примера кода с рис. 5, преобразованного с помощью техники поднятия для соблюдения закона Деметры: (а) код на Flavors, (b) код на С++ и (c) соответствующий граф зависимостей.



class Grammar has parts 
    ruleList : RuleList 
end class Grammar.

 class RuleList is list
   repeat {Rule}
end class RuleList.

 class Rule has parts 
    body: Body
end class Rule.

(a)

(defmethod (Grammar :parse) (rule_name)
    (send (send-self :look_up rule_name) :parse_details))
 
(defmethod (Grammar :look_up) (rule_name)
; возвращает объект типа Rule
    …
    (send ruleList :look_up rule_name)) 

(defmethod (RuleList :look_up) (rule_name)
    …
)

(defmethod (Rule :parse_details) ()
    …
)

(b)

void Grammar::parse(Symbol* rule_name)
    {this -> look_up(rule_name) -> parse_details();} 

Rule* Grammar::look_up(Symbol* rule_name)
{ …
    ruleList -> look_up(rule_name);
}

void RuleList::look_up(Symbol* rule_name)
    { … }

void Rule::parse_details()
    { … }

 (c)

(d)

Рис. 7. Пример кода, нарушающего Закон Деметры, который нельзя исправить с помощью техники поднятия: (а) словарь классов, (b) код на Flavors, (c) код на С++ и (d) соответствующий граф зависимостей.



(defmethod (Grammar :parse) (rule_name)
    (send self :look_up_parse rule_name))

(defmethod (Grammar :look_up_parse) (rule_name)
    (send ruleList :look_up_parse rule_name))

(defmethod (RuleList :look_up_parse) (rule_name)
    (send (send-self :look_up rule_name) :parse_details))

(a)

void Grammar::parse(Symbol* rule_name)
    {this -> look_up_parse(rule_name);}

void Grammar::look_up_parse(Symbol* rule_name)
    {ruleList -> look_up_parse(rule_name);}

void RuleList::look_up_parse(Symbol* rule_name)
    {this -> look_up(rule_name) -> parse_details();}

 (b)

(c)

Рис. 8. Вариант примера кода с рис. 7, преобразованного с помощью техники спуска для соблюдения закона Деметры: (а) код на Flavors, (b) код на С++ и (c) соответствующий граф зависимостей.


Поднятие. Эта техника применима в первом случае (T является классом-частью C). Идея в том, что m1 нужно преобразовать так, чтобы он возвращал объект класса переменной экземпляра или аргумента C, а затем соответствующим образом изменить m2. Метод m2 поднимается вверх по иерархии классов, переходя от принадлежности классу T к принадлежности классу переменной экземпляра C.

Например, предположим, что требуется разобрать входные данные с помощью некоторой грамматики. Грамматика состоит из списка правил, подобного приведенному на рис. 5. Этот программный фрагмент использует один класс-знакомый (класс Body в методе parse класса Grammar) и представлен на рис. 5b и 5c.

Проблема данного фрагмента в том, что метод look_up класса Grammar возвращает объект типа Body, который не является типом переменной экземпляра Grammar. Чтобы преобразовать первый метод в хорошем стиле, необходимо, чтобы look_up возвращал экземпляр класса Rule, а затем изменить parse_details. На рис. 6 показан этот измененный вариант. Улучшенный программный фрагмент не использует ни одного класса-знакомого. 

Однако подход, основанный на поднятии, работает не всегда. Рассмотрим рис. 7. Этот программный фрагмент использует один класс-знакомый (класс Rule в методе parse класса Grammar). В данном случае преобразовать первый метод в хорошем стиле путем поднятия типа возвращаемого значения метода look_up невозможно.

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

В примере с поднятием проблема возникает, потому что класс Grammar имеет задачу отправки сообщения parse_details. Эта задача на самом деле является ответственностью RuleList, который знает больше деталей класса Rule, чем Grammar. На рис. 8 показана улучшенная структура, не использующая никакие классы-знакомые.

В примере с поднятием проблема возникла из-за того, что на класс Grammar была возложена задача отправки сообщения parse_details. На самом деле эта задача входит в ответственность класса RuleList, который знает о деталях класса Rule больше, чем Grammar. На рис. 8 показан улучшенный вариант, который не использует ни одного класса-знакомого. 

Перепроектирование вводит дополнительный метод. Если рассматривать классы списков как стабильные (что, например, справедливо в случае Smalltalk), то нет необходимости для перепроектирования и использование класса-знакомого является оправданным.


Формы для популярных языков

Для эффективного применения закона Деметры его необходимо адаптировать к конкретному языку. Ниже приведены формы, которые мы вывели для нескольких популярных объектно-ориентированных языков. Для C++ мы приводим строгую версию формы для классов; для остальных языков — форму закона для объектов. Этот выбор произволен, однако для статически типизированных языков C++ и Eiffel форма для классов наиболее полезна, поскольку ее соблюдение может проверяться модифицированным компилятором. Пользователям Eiffel не составит труда сформулировать форму для классов.

С++, строгая версия формы для классов. Во всех функциях-членах M класса C разрешено использовать только члены (функции и данные) следующих классов, а также их базовых классов: 

  • C;

  • классов данных-членов C;

  • классов аргументов M;

  • классов, функции-конструкторы которых вызываются в M;

  • классы глобальных переменных, используемых в M.

Common Lisp Object System, форма для объектов. Мы предполагаем, что пользователь CLOS может определить для каждой обобщенной функции количество аргументов выбора метода (не обязательно все требуемые) и что это число является частью интерфейса обобщенной функции. Аргумент выбора метода — это аргумент, используемый для определения применимых методов. 

Все вызовы функций внутри метода M должны использовать в качестве аргументов выбора метода только следующие объекты:

  • объекты-аргументы M;

  • непосредственные части аргументов выбора метода M;

  • объект, который либо создан непосредственно в M, либо является объектом из глобальной переменной.

Eiffel, форма для объектов. Во всех вызовах подпрограмм внутри подпрограммы M объект-сущность должен быть одним из следующих объектов: 

  • объектом-аргументом M

  • объектом-атрибутом класса, в котором определена M

  • объектом, созданным непосредственно в M.

Flavors, форма для объектов. В любом методе M, принадлежащем классу C, разрешается отправлять сообщения только следующим объектам:

  • объектам-аргументам M;

  • объектам-переменным экземпляра класса C;

  • объект, который либо создан непосредственно в M, либо является объектом из глобальной переменной. 

Smalltalk-80, форма для объектов. Во всех выражениях-сообщениях внутри метода M получатель должен быть одним из следующих объектов:

  • объектом-аргументом M, включая объекты в псевдопеременных Self и Super;

  • непосредственной частью Self; 

  • объектом, который либо создан непосредственно в M, либо является объектом из глобальной переменной.      


Стиль модульного программирования, поощряемый Законом Деметры, естественным образом приводит к созданию кода, который легче читать и сопровождать. Этот закон позволяет перепроектировать классы (включая их интерфейсы), сохраняя при этом бо́льшую часть существующего кода без изменений. Более того, эффективное смягчение последствий локальных изменений в программной системе может значительно уменьшить количество проблем, связанных с сопровождением программного обеспечения. Однако соблюдение закона сопряжено с издержками. Чем строже ограничения, накладываемые на интерфейс (что является углублением сокрытия), тем выше плата в виде увеличения количества методов, снижения скорости выполнения, роста числа аргументов в методах, а иногда и ухудшения читаемости кода. 

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

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

Мы продолжаем наши исследования в области модульного объектно-ориентированного программирования и убеждены, что закон ведет к созданию качественного программного обеспечения, если применять его вместе с другими общепринятыми стилистическими правилами. 

Благодарности

Ранняя версия этой статьи была опубликована в материалах конференции OOPSLA ’88: Object-Oriented Programming Systems, Languages, and Applications. В июньском номере журнала Computer за 1988 год в разделе Open Channel было размещено краткое изложение закона.  

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

Представители сообщества Common Lisp Object System (включая Дэниела Боброу, Ричарда Гэбриела, Джима Кемпфа, Грегора Кичалеса и Алана Снайдера) принимали участие в дискуссии и разработке CLOS-версии закона. Мы благодарим Маркку Саккинена за его ценную статью и полезные письма, касающиеся закона Деметры. Синди Браун и Митч Уонд убедили нас в необходимости использовать более наглядную нотацию, чем расширенная форма Бэкуса — Наура, и помогли в ее создании. Пол Стеклер и Игнасио Сильва-Лепе также внесли ряд усовершенствований в расширенную нотацию «Деметры».

Источники

1. Kaehler T., Patterson D. A Taste of Smalltalk. New York: W.W. Norton, 1986.

2. Snyder A. Inheritance and the Development of Encapsulated Software Systems // Shriver B., Wegner P. (eds.) Research Directions in Object-Oriented Programming. Cambridge: MIT Press, 1987. P. 147–164. 

3. Lieberherr K.J. Object-Oriented Programming with Class Dictionaries // Journal of Lisp and Symbolic Computation, vol. 1, № 2, 1988. P. 185–212.

4. Lieberherr K.J., Riel A.J. Demeter: A CASE Study of Software Growth Through Parameterized Classes // Journal of Object-Oriented Programming, August–September 1988. P. 8–22.

5. Lieberherr K.J., Holland I., Riel A.J. Object-Oriented Programming: An Objective Sense of Style // ACM SIGPLAN Notices, vol. 23, № 11, November 1988. P. 323–334.

6. Sakkinen M. Comments on the Law of Demeter and C++ // ACM SIGPLAN Notices, vol. 23, № 12, December 1988. P. 38–44.

7. Hewitt C., Baker H. Laws for Communicating Parallel Processes // Gilchrist B. (ed.) Information Processing 77, Proceedings of the IFIP Congress 77, Toronto, Canada, August 8–12, 1977. Amsterdam: North-Holland, 1977. P. 987–992.

8. Liskov B., Guttag J. Abstraction and Specification in Program Development. Cambridge: MIT Press, 1986.

9. Parnas D.L., Clements P.C., Weiss D.M. The Modular Structure of Complex Systems // IEEE Transactions on Software Engineering, vol. SE-11, № 3, March 1985. P. 259–266.

10. Heijenoort J.V. From Frege to Gödel. Cambridge: Harvard University Press, 1963.

Комментарии (0)