Принципы Объектно-ориентированного-программирования

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

 

1. Абстракция

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

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

Например, можно совершенно точно предсказать погоду на завтра в определенном месте, но расчеты по такой модели продлятся трое суток даже на самом мощном компьютере. Зачем нужна модель, опаздывающая на два дня? Ну а точность модели, используемой синоптиками, мы все знаем сами. Зато расчеты по этой модели занимают всего несколько часов.

Описание каждой модели производится в виде одного или нескольких классов (classes). Класс можно считать проектом, слепком, чертежом, по которому затем будут создаваться конкретные объекты. Класс содержит описание переменных и констант, характеризующих объект. Они называются полями класса (class fields). Процедуры, описывающие поведение объекта, называются методами класса (class methods). Внутри класса можно описать и вложенные классы (nested classes) и вложенные интерфейсы. Поля, методы и вложенные классы первого уровня являются членами класса (class members). Разные школы объектно-ориентированного программирования предлагают разные термины, мы используем терминологию, принятую в технологии Java.

Вот набросок описания автомобиля:

class Automobile{

int maxVelocity;  // Поле, содержащее наибольшую скорость автомобиля 

int speed;        // Поле, содержащее текущую скорость автомобиля 

int weight;       // Поле, содержащее вес автомобиля 

                  // Прочие поля…

void moveTo(int x, int у){   // Метод, моделирующий перемещение

                             // автомобиля. Параметры х и у — не поля

int а = 1;   // Локальная переменная — не поле 

             // Тело метода. Здесь описывается закон 

             // перемещения автомобиля в точку (х, у) 

}

// Прочие методы. . . 

}

Знатокам Pascal

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

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

Automobile Iada2110, fordScorpio, oka;

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

lada2110 = new Automobile(); 

fordScorpio = new Automobile(); 

oka = new Automobile();

На третьем этапе происходит инициализация объектов, задаются начальные значения. Этот этап, как правило, совмещается со вторым, именно для этого в операции new повторяется имя класса со скобками Automobile () . Это так называемый конструктор (constructor) класса, но о нем поговорим попозже.

Поскольку имена полей, методов и вложенных классов у всех объектов одинаковы, они заданы в описании класса, их надо уточнять именем ссылки на объект:

lada2110.maxVelocity = 150; 

fordScorpio.maxVelocity = 180;

oka.maxVelocity = 350;    // Почему бы и нет? 

oka.moveTo(35, 120);

Напомним, что текстовая строка в кавычках понимается в Java как объект класса String . Поэтому можно написать

int strlen = «Это объект класса String».length();

Объект «строка» выполняет метод length() , один из методов своего класса string , подсчитывающий число символов в строке. В результате получаем значение strlen , равное 24. Подобная странная запись встречается в программах на Java на каждом шагу.

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

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

 

2. Иерархия

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

class Pet{     // Здесь описываем общие свойства всех домашних любимцев 

Master person; // Хозяин животного 

int weight, age, eatTimel];             // Вес, возраст, время кормления 

int eat(int food, int drink, int time){ // Процесс кормления

                                        // Начальные действия…

if (time == eatTimefi]) person.getFood(food, drink);

               // Метод потребления пищи 

}

void voice();  // Звуки, издаваемые животным 

               // Прочее… 

}

Затем создаем классы, описывающие более конкретные объекты, связывая их с общим классом:

class Cat extends Pet{  // Описываются свойства, присущие только кошкам:

int mouseCatched;       // число пойманных мышей

void toMouse();         // процесс ловли мышей

                        // Прочие свойства 

class Dog extends Pet{  // Свойства собак:

void preserve();        // охранять 

}

Заметьте, что мы не повторяем общие свойства, описанные в классе Pet . Они наследуются автоматически. Мы можем определить объект класса Dog и использовать в нем все свойства класса Pet так, как будто они описаны в классе Dog :

Dog tuzik = new Dog(), sharik = new Dog();

После этого определения можно будет написать

tuzik.age = 3;

int p = sharik.eat (30, 10, 12);

А классификацию продолжить так:

class Pointer extends Dog{ … } // Свойства породы Пойнтер 

class Setter extends Dog{ … }  // Свойства сеттеров

Заметьте, что на каждом следующем уровне иерархии в класс добавляются новые свойства, но ни одно свойство не пропадает. Поэтому и употребляется слово extends — «расширяет» и говорят, что класс Dog — расширение (extension) класса Pet . С другой стороны, количество объектов при этом уменьшается: собак меньше, чем всех домашних животных. Поэтому часто говорят, что класс Dog — подкласс (subclass) класса Pet , а класс Pet — суперкласс (superclass) или надкласс класса Dog .

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

В этой терминологии говорят о наследовании (inheritance) классов, в нашем примере класс Dog наследует класс Pet .

Мы еще не определили счастливого владельца нашего домашнего зоопарка. Опишем его в классе Master. Делаем набросок:

class Master{ // Хозяин животного

String name;  // Фамилия, имя

              // Другие сведения

void getFood(int food, int drink); // Кормление

              // Прочее 

}

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

 

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

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

В англоязычной литературе подобное обращение описывается словом message. Это понятие неудачно переведено на русский язык ни к чему не обязывающим словом «сообщение». Лучше было бы использовать слово «послание», «поручение» или даже «распоряжение». Но термин «сообщение» устоялся и нам придется его применять. Почему же не используется словосочетание «вызов метода», ведь говорят: «Вызов процедуры»? Потому что между этими понятиями есть, по крайней мере, триЪтличия.

  • Сообщение идет к конкретному объекту, знающему метод решения задачи, в примере этот объект — текущее значение переменной person . У каждого объекта свое текущее состояние, свои значения полей класса, и это может повлиять на выполнение метода.
  • Способ выполнения поручения, содержащегося в сообщении, зависит от объекта, которому оно послано. Один хозяин поставит миску с «Chappi», другой бросит кость, третий выгонит собаку на улицу. Это интересное свойство называется полиморфизмом (polymorphism) и будет обсуждаться ниже.
  • Обращение к методу произойдет только на этапе выполнения программы, компилятор ничего не знает про метод. Это называется «поздним связыванием» в противовес «раннему связыванию», при котором процедура присоединяется к программе на этапе компоновки.

Итак, объект sharik , выполняя свой метод eat () , посылает сообщение объекту, ссылка на который содержится в переменной person, с просьбой выдать ему определенное количество еды и питья. Сообщение записано в строке person.getFood(food, drink) .

Этим сообщением заключается контракт (contract) между объектами, суть которого в том, что объект sharik берет на себя ответственность (responsibility) задать правильные параметры в сообщении, а объект — текущее значение person — возлагает на себя ответственность применить метод кормления getFood() , каким бы он ни был.

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

 

4. Модульность

Этот принцип утверждает — каждый класс должен составлять отдельный модуль. Члены класса, к которым не планируется обращение извне, должны быть инкапсулированы.

В языке Java инкапсуляция достигается добавлением модификатора private к описанию члена класса. Например:

private int mouseCatched;

private String name;

private void preserve();

Эти члены классов становятся закрытыми, ими могут пользоваться только экземпляры того же самого класса, например, tuzik может дать поручение

sharik.preserve().

А если в классе Master мы напишем

private void getFood(int food, int drink);

то метод getFood() не будет найден, и несчастный sharik не сможет получить пищу. ,

В противоположность закрытости мы можем объявить некоторые члены класса открытыми, записав вместо слова private модификатор public , например:

public void getFood(int food, int drink);

К таким членам может обратиться любой объект любого класса.

Знатокам C++

В языке Java словами private, public и protected отмечается каждый член класса в отдельности.

Принцип модульности предписывает открывать члены класса только в случае необходимости. Вспомните надпись: «Нормальное положение шлагбаума — закрытое».

Если же надо обратиться к полю класса, то рекомендуется включить в класс специальные методы доступа (access methods), отдельно для чтения этого поля (get method) и для записи в это поле (set method). Имена методов доступа рекомендуется начинать со слов get и set , добавляя к этим словам имя поля. Для JavaBeans эти рекомендации возведены в ранг закона.

В нашем примере класса Master методы доступа к полю Name в самом простом виде могут выглядеть так:

public String getName(){

return name; 

public void setName(String newName)

{

name = newName; 

}

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

Кроме методов доступа рекомендуется создавать проверочные is-методы, возвращающие логическое значение true или false . Например, в класс Master можно включить метод, проверяющий, задано ли имя хозяина:

public boolean isEmpty(){

return name == null ? true : false; 

}

и использовать этот метод для проверки при доступе к полю Name , например:

if (masterOl.isEmpty()) masterOl.setName(«Иванов»);

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

Напротив, члены класса должны активно взаимодействовать друг с другом, как говорят, иметь тесную функциональную связность (high cohestion). Для этого в класс следует включать все методы, описывающие поведение моделируемого объекта, и только такие методы, ничего лишнего. Одно из правил достижения сильной функциональной связности, введенное Карлом Ли-берхером (Karl J. Lieberherr), получило название закон Деметра. Закон гласит: «в методе т() класса А следует использовать только методы класса А, методы классов, к которым принадлежат аргументы метода т(), и методы классов, экземпляры которых создаются внутри метода m ().

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

Будут ли закрытые члены класса доступны его наследникам? Если в классе Pet написано

private Master person;

то можно ли использовать sharik.person ? Разумеется, нет. Ведь в противном случае каждый, интересующийся закрытыми полями класса А , может расширить его классом B , и просмотреть закрытые поля класса А через экземпляры класса B .

Когда надо разрешить доступ наследникам класса, но нежелательно открывать его всему миру, тогда в Java используется защищенный (protected) доступ, отмечаемый модификатором protected , например, объект sharik может обратиться к полю person родительского класса pet , если в классе Pet это поле описано так:

protected Master person;

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

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

Но, говоря о принципах ООП, я не могу удержаться от того, чтобы не напомнить основной принцип всякого программирования.

 

5. Принцип KISS

Самый основной, базовый и самый великий принцип программирования — принцип KISS — не нуждается в разъяснений и переводе: «Keep It Simple, Stupid!»