Курс PHP для начинающих - с нуля до джуниора за 2 месяца. Жми!
objects08 2

Методы проектирования

Курс PHP для начинающих
Внимание! Данный курс устарел!
Переходите к новому курсу "ООП в PHP: продвинутый курс".

Теперь, когда мы достаточно подробно рассмотрели механизм поддержки объектов в PHP, отойдем от деталей и выясним, почему лучше всего использовать те средства, с которыми мы познакомились.

Курс PHP для начинающих

Определение программного проекта

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

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

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

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

Курс PHP для начинающих

Объектно-ориентированное и процедурное программирование

Чем объектно-ориентированный проект отличается от более традиционного процедурного кода? Так и хочется сказать: "Главное отличие в том, что в объектно-ориентированном коде есть объекты". Но это неверно. В PHP часто бывает, что в процедурном коде используются объекты. С другой стороны, вы можете столкнуться с классами, в которых содержатся фрагменты процедурного кода. Наличие классов — еще не гарантия объектно-ориентированного проекта, даже в таком языке, как Java, который заставляет большинство работы выполнять внутри класса.

Коренное отличие между объектно-ориентированным и процедурным кодом заключается в том, как распределяется ответственность. Процедурный код имеет форму последовательности команд и вызовов функций. Управляющий код обычно несет ответственность за обработку различных ситуаций. Это управление "сверху вниз" может привести к дублированию и возникновению зависимостей в проекте. Объектно-ориентированный код пытается минимизировать эти зависимости путем передачи ответственности за управление задачами от клиентского кода объектам в системе.

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

Код PHP
function readParams( $source ) {
    $params = array();
    
	// Читаем текстовый параметр из файла $source
	
    return $params;
}

function writeParams( $params, $source ) {
    // Записываем текстовый параметр в файл $source
}

Функции readParams() нужно передать имя конфигурационного файла. Она пытается открыть его и читает каждую строку в поиске пар "ключ/значение". В ходе работы эта функция создает ассоциативный массив. И наконец, она возвращает этот массив управляющему коду. Функции writeParams() передается ассоциативный массив и имя конфигурационного файла. Она проходит в цикле по этому ассоциативному массиву, записывая каждую пару "ключ/значение" в файл. Вот пример клиентского кода, который работает с этими функциями:

Код PHP
$file = "./params.txt"; 
$array['key1'] = "val1";
$array['key2'] = "val2";
$array['key3'] = "val3";
writeParams( $array, $file );
$output = readParams( $file );
print_r( $output ); 

Этот код относительно компактный, и его легко сопровождать. При вызове функции writeParams() создается файл params.txt, в который записывается приведенная ниже информация:

key1:val1
key2:val2
key3:val3

Но вдруг нас проинформировали, что программа должна уметь работать с простыми конфигурационными файлами, заданными в формате XML, как показано ниже:

<params>
	<param>
		<key>key1</key>
		<val>val1</val>
    </param>
    <param>
		<key>key2</key>
		<val>val2</val>
    </param>
</params>

Если данный конфигурационный файл имеет расширение .xml, то для его обработки необходимо задействовать средства обработки XML, встроенные в PHP. И хотя такую задачу совсем нетрудно решить, существует угроза, что наш код станет намного труднее сопровождать. На этом этапе у нас есть два варианта. Мы можем проверить расширение файла в управляющем коде или сделать проверку внутри функций чтения и записи. Давайте пока остановимся на втором варианте:

Код PHP
function readParams( $source ) {
    $params = array();
	
	if ( preg_match( "/\.xml$/i", $source )) {
		//  Читаем параметры в формате XML из $source
    } 
	else {
		// Читаем текстовые параметры из $source
    }
	
    return $params;
}

function writeParams( $params, $source ) {
    if ( preg_match( "/\.xml$/i", $source )) {
		// Запишем параметры в формате XML в $source
    } 
	else {
		// Запишем текстовые параметры в $source
    }
}

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

Как видите, расширение .xml мы проверяли в каждой функции. Именно это повторение в итоге может стать причиной проблем. Если нас попросят включить в систему поддержку еще одного формата параметров, то мы должны будем помнить о том, что изменения нужно внести в обе функции: readParams() и writeParams().

А теперь давайте попробуем решить ту же задачу с помощью простых классов. Сначала создадим абстрактный базовый класс, который будет определять интерфейс для типа:

Код PHP
abstract class ParamHandler {
    protected $source;
    protected $params = array();

    function __construct( $source ) {
        $this->source = $source;
    }

    function addParam( $key, $val ) {
        $this->params[$key] = $val;
    }

    function getAllParams() {
        return $this->params;
    }

    static function getInstance( $filename ) {
        if ( preg_match( "/\.xml$/i", $filename )) {
            return new XmlParamHandler( $filename );
        }
        return new TextParamHandler( $filename );
    }

    abstract function write();
    abstract function read();
}

Я определил метод addParam(), чтобы позволить пользователю добавлять параметры к защищенному свойству $params, и метод getAllParams() — чтобы предоставить доступ к копии массива.

Я также создал статический метод getInstance(), который проверяет расширение файла и возвращает объект конкретного подкласса в соответствии с результатами проверки. Решающим является то, что мы определили два абстрактных метода, read() и write(), тем самым гарантировав, что любые подклассы будут поддерживать этот интерфейс.

А теперь давайте определим подклассы, снова опуская детали реализации с целью сохранения понятности кода:

Код PHP
class XmlParamHandler extends ParamHandler {

    function write() {
        // Запись в формате XML
		// массива параметров $this->params
    }

    function read() {
        // Чтение из XML-файла и
		// запись значений в массив $this->params

    } 
}

class TextParamHandler extends ParamHandler {

    function write() {
        // Запись в текстовый файл
		// массива параметров $this->params
    }

    function read() {
        // Чтение из текстового файла и 
		// запись значений в массив $this->params

    } 
}

Эти классы просто обеспечивают реализации методов read() и write(). Каждый класс будет считывать и записывать данные согласно соответствующему формату. В результате из клиентского кода можно будет записывать параметры конфигурации в текстовый и XML-файлы совершенно ясно и прозрачно, в зависимости от расширения имени файла:

Код PHP
$file = "./test.xml"; 
$test = ParamHandler::getInstance( $file );
$test->addParam("key1", "val1" );
$test->addParam("key2", "val2" );
$test->addParam("key3", "val3" );
$test->write();		// Запись файла в XML-формате

Итак, какие выводы можно сделать из этих двух подходов?

Ответственность

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

В объектно-ориентированной версии выбор формата файла делает статический метод getInstance(), который проверяет расширение файла только один раз и создает нужный подкласс. Клиентский код не несет ответственности за реализацию. Он использует предоставленный объект, не зная и даже не интересуясь, какому конкретному подклассу он принадлежит. Он только знает, что работает с объектом ParamHandler и что он поддерживает методы read() и write(). В то время как процедурный код занимается деталями, объектно-ориентированный код работает только с интерфейсом, не заботясь о деталях реализации. Поскольку ответственность за реализацию лежит на объектах, а не на клиентском коде, будет легко включить поддержку новых форматов явным и прозрачным способом.

Связность

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

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

С другой стороны, в процедурном примере связанные процедуры разделены. Код для работы с XML-форматом разбросан по нескольким функциям.

Тесная связь

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

Такой вид тесной связи можно увидеть в примере процедурного кода. Функции readParams() и writeParams() осуществляют одну и ту же проверку расширения файла, чтобы определить, как они должны работать с данными. Любое изменение в логике, которое осуществляется для одной функции, должно быть реализовано и для другой. Например, в случае добавления нового формата нужно было бы привести все функции в соответствие, чтобы новое расширение файла обрабатывалось в них одинаково. По мере добавления новых функций, обрабатывающих другие форматы файлов, эта проблема будет только усугубляться.

А в примере объектно-ориентированного кода классы отделяются один от другого и от клиентского кода. Если бы нам потребовалось добавить поддержку нового формата файла, то мы могли бы просто создать новый подкласс, изменив единственную проверку в статическом методе getInstance().

Ортогональность

Потрясающее сочетание компонентов с четко определенными обязанностями наряду с независимостью от более широкого контекста иногда называют ортогональностью (orthogonality), как, например, в книге Эндрю Ханта (Andrew Hunt) и Дэвида Томаса (David Thomas) The Pragmatic Programmer (Addison-Wesley Professional, 1999). Как утверждают авторы, ортогональность способствует повторному использованию кода, поскольку готовые компоненты можно включать в новые системы, не делая никакой их специальной настройки. Такие компоненты должны иметь четко определенные входные и выходные данные, независимые от какого-либо более широкого контекста.

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

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

Выбор классов

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

При моделировании реальной ситуации это может показаться простым. Обычно объектно-ориентированные системы — это программное представление реальных вещей, поскольку часто используются классы Person, Invoice и Shop. Отсюда можно предположить, что определение классов — это вопрос нахождения некоторых объектов в системе и придания им функциональности с помощью методов. Это неплохая отправная точка, но тут таятся некоторые опасности. Если рассматривать класс как существительное, как подлежащее для любого количества глаголов, то окажется, что он будет все больше расширяться, потому что в ходе разработки и внесения изменений потребуется, чтобы класс выполнял все больше и больше операций.

Давайте рассмотрим пример с классом ShopProduct, который мы создавали в предыдущих статьях. Наша система предназначена для того, чтобы продавать товары покупателям, поэтому определение класса ShopProduct - это очевидное решение. Но будет ли это решение единственным? Для доступа к данным о товаре мы предоставляем методы getTitle() и getPrice(). Если нас попросят обеспечить механизм для вывода краткой информации о товаре для счетов-фактур и уведомлений о доставке товаров, то, наверное, имеет смысл определить метод write(). Когда клиент попросит нас обеспечить механизм выдачи краткой информации в различных форматах, мы снова обратимся к нашему классу и надлежащим образом создадим методы writeXML() и writeXHTML() в дополнение к методу write(). Либо добавим в код метода write() условные операторы, чтобы выводить данные в различных форматах в соответствии со значением входного аргумента.

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

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

Например, обязанность класса ShopProduct — хранить информацию о товаре. И если мы добавляем методы для вывода данных в различных форматах, то тем самым вводим новую сферу ответственности (т.е. обязанностей) — отображение информации о продукте. Как вы видели в статье "Расширенные возможности использования объектов", на самом деле на основе этих отдельных обязанностей мы определили не один, а два класса. Класс ShopProduct остался отвечать за хранение информации о товарах, a ShopProductWriter берет на себя ответственность за отображение этой информации. А уточнение этих обязанностей осуществляется с помощью отдельных подклассов - XmlShopProductWriter и TextShopProductWriter.

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

Полиморфизм

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

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

Код PHP
function readParams( $source ) {
    $params = array();
	
	if ( preg_match( "/\.xml$/i", $source )) {
		//  Читаем параметры в формате XML из $source
    } 
	else {
		// Читаем текстовые параметры из $source
    }
	
    return $params;
}

function writeParams( $params, $source ) {
    if ( preg_match( "/\.xml$/i", $source )) {
		// Запишем параметры в формате XML в $source
    } 
	else {
		// Запишем текстовые параметры в $source
    }
}

В каждой части напрашивается выделить один из подклассов, которые мы в конце концов и создаем: XmlParamHandler и TextParamHandler. В этих классах реализуются методы write() и read() абстрактного базового класса ParamHandler. Важно отметить, что полиморфизм не отрицает использование условных операторов. Обычно в методах наподобие ParamHandler::getInstance() с помощью операторов switch или if определяется, какие объекты нужно возвращать. Но это ведет к сосредоточению кода с условными операторами в одном месте.

Как мы видели, в PHP 5 программиста вынуждают создавать интерфейсы, определяемые абстрактными классами. Это удобно, потому что мы можем быть уверенными, что конкретный дочерний класс будет поддерживать в точности те же сигнатуры методов, что были определены в абстрактном родительском классе. В них будут включены все уточнения типов классов и элементы контроля доступа. Поэтому в клиентском коде можно рассматривать все дочерние классы общего суперкласса как взаимозаменяемые (если в нем используются только функции, определенные в родительском классе). Но у этого правила есть важное исключение; пока что не существует способа ограничить тип, возвращаемый методом.

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

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

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

Инкапсуляция (encapsulation) — это просто сокрытие данных и функциональности от клиентского кода. И опять-таки, это ключевое понятие объектно-ориентированного программирования. На самом простейшем уровне мы инкапсулируем данные, объявляя свойства как private или protected. Скрывая свойство от клиентского кода, мы вводим в действие интерфейс и тем самым предотвращаем случайное повреждение данных объекта.

Полиморфизм иллюстрирует другой вид инкапсуляции. Размещая различные реализации за общим интерфейсом, мы скрываем работающий в их основе механизм от клиентского кода. Это означает, что любые изменения, внесенные за этим интерфейсом, являются прозрачными для более широкой системы. Мы можем добавлять новые классы или менять код в классе, что не приведет к возникновению ошибок. Значение имеет только интерфейс, а не механизм, работающий в его основе. Чем более независимы эти механизмы, тем меньше вероятности, что внесенные изменения или поправки будут иметь "эффект домино" для ваших проектов.

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

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

Конечно, код должен быть внимательно проверен, потому что конфиденциальность не была строго определена. Но, что интересно, ошибки случались редко, потому что структура и стиль кода довольно четко определяли, какие свойства трогать нельзя. Кроме того, даже в PHP 5 мы можем нарушить правила и выяснить точный подтип объекта, который мы используем в контексте замены классов, просто используя оператор instanceof:

Код PHP
function writeWithProducts (ShopProduct $product) {
	if ($product instanceof CDProduct) {
		// Обработка данных CDProduct
	}
	else {
		// Обработка других BookProduct
	}
}

Для выполнения подобных действий должна быть серьезная причина, поскольку в целом это вносит определенный элемент сумбура. Запрашивая конкретный подтип, как в данном примере, мы устанавливаем жесткую зависимость. Если же специфика подтипа скрыта полиморфизмом, то можно совершенно безболезненно изменить иерархию наследования класса ShopProduct, не опасаясь негативных последствий. Но в нашем коде этому положен предел. Теперь, если нам нужно усовершенствовать классы CDProduct и BookProduct, мы должны помнить о возможности нежелательных побочных эффектов в методе writeWithProducts().

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

Мысли вслух

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

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

Чтобы сделать упор на интерфейсе, размышляйте в терминах абстрактных базовых классов, а не конкретных дочерних классов. Например, в нашем коде извлечения параметров, интерфейс — это самый важный аспект проектирования. Нам нужен тип, который считывает и записывает пары "имя/значение". Именно эта обязанность важна для типа, а не реальный носитель, на котором будут сохраняться данные, или способы их хранения и извлечения. Мы проектируем систему вокруг абстрактного класса ParamHandler и добавляем только конкретные стратегии для того, чтобы в дальнейшем в реальном приложении можно было считывать и записывать параметры. Таким образом, мы встраиваем в нашу систему полиморфизм и инкапсуляцию с самого начала. Такая структура уже тяготеет к использованию замены классов.

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

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

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

Дублирование кода

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

Класс, который слишком много "знал"

Это может быть очень утомительно — пересылать параметры туда-сюда от одного метода к другому. Почему бы не упростить себе жизнь путем использования глобальной переменной? С помощью глобальной переменной каждый может получить доступ к данным.

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

На все руки мастер

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

Оставить "слишком работящий класс" без изменений — значит создать определенные проблемы в случае создания подклассов. Какую обязанность вы расширяете с помощью подкласса? Что будете делать, если вам понадобится подкласс для более чем одной обязанности? Очень вероятно, что у вас получится слишком много подклассов или слишком сильная зависимость от условного кода.

Условные операторы

Конечно, у вас будут серьезные причины для использования в коде операторов if и switch. Но иногда наличие подобных структур является сигналом к полиморфизму.

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

Рефлексия
Диаграммы UML

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

Результаты поиска по запросу

Система Orphus