Как правильно проектировать структуру классов в ООП (Java)?
Пишем приложение для приемки различной номенклатуры (оборудования) от поставщиков (мониторы, принтеры и др. оргтехника). Приемка осуществляется на основании документа, в котором есть N-ое кол-во строк. Каждая строка это отдельная номенклатура с указанием наименования, кода, цены, кол-во и тд. За каждой еденицей номенклатуры закреплен один инвентарный номер. Сейчас для описания этого мы используем следующие классы:
// Этот класс описывает документ class Doc < String docNum; // Номер документа Listrows; // Строки документа > // Этот класс описывает строку документа class DocRow < Nomen nomen; // Номенклатура int count; // Кол-во по документу int price; // Цена по документу Listinvents; // Список инвентарных номеров > // Этот класс описывает номенклатуру class Nomen < String code; // Код String name; // Наименование >// Этот класс описывает инвентарный номер class Invent < Nomen nomen; // Номенклатура String barcode; // Генерируемый штрих-код >

В итоге вложенность классов такая: Видно что класс Nomen есть на нескольких уровнях и как-бы протаскивается ниже. Вопрос в том на сколько правильно так делать? Каковы основные правила? Мы сделали так, потому что копии ссылок на экземпляр класса Nomen на разных уровнях бывают очень удобными. Например у нас есть метод для печати на принтере пачки инвентарных номеров print(List invents) . Печатая инвентарный номер Invent.barcode требуется выводить так же и наименование. Благодаря доступу к Invent.Nomen.name мы можем сделать это без особых проблем, без добавления дополнительной логики по поиску этого наименования. Класс Invent как-бы может сообщить свой штрих-код и тип номенклатуры что делает его достаточно самодостаточным. Более того, в будущем мы можем использовать класс Invent вообще вне класса DocRow и тогда уж точно нужно понимать к какому типу номенклатуры принадлежит данные инвентарный номер. Что подскажут коллеги с многолетним опытом? Спасибо.
Структуры и классы (введение в ООП)
Иногда мы хотим, чтобы наш объект имел несколько характеристик. Так, например, для характеристики автомобиля мы можем указать следующие параметры: цвет, марка, стоимость и т.д.
В языке C++ уже определена такая вещь как структура. Структура — это составной тип данных, который может содержать в себе несколько переменных-членов.
Создадим стуктуру, описанную нами выше:
struct car int price; char name[50]; >;
Как обращаться со стуктурой? Это достаточно просто — мы можем обращаться как с обычной переменной:
car Tesla; // инициализация структурной переменной car cars[50]; // также можем создавать массивы Tesla.price = 100; // обращаемся к члену стуктуры cout Tesla.price;
ООП
Использование структур неминуемо ведет нас в Объектно Ориентированное Программирование (ООП). ООП — это особый концептуальный подход к проектированию программ. Наиболее важными характеристиками ООП являются:
- Абстракция
- Инкапсуляция и сокрытие данных
- Полиморфизм
- Наследование
- Повторное использование кода
Класс — расширение С++ (относительно С), предназначенное для реализации средств. Чтобы упростить идентификацию классов, программисты прищли к соглашению, что классы должны именоваться с большой буквы. Рассмотрим объявление класса:
class Bank_account private: int amount; public: Bank_account() amount = 0; >; int Add(int number); int Remove(int number); void Show_money(); >;
Здесь мы создали Bank_account. Класс — это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью. В данном случае наш класс описывает счет в банке. Пусть методами данного класса будут Add (добавить некоторую сумму), Remove (снять некоторую сумму) и Show_money — показать баланс.
Объектом (экземпляром) класса будем называть отдельный представитель класса. Людей много, а счет один.
Новыми словами являются private и public. Любая программа, использующая объект определенного класса, может иметь доступ к членам из раздела public. Доступ к членам из раздела private программа может получить только через открытые функции-члены из раздела public, или по другому, из интерфейса. Этот подход называется сокрытием данных. Открытые функции-члены называют методами.
Рассмотрим члены private. Здесь мы описываем то, как хранятся наши данные. В данном случае у нас всего одна переменная — amount (количество средств). Доступ к этому члену происходит только через public.
Теперь рассмотрим члены public. Первый член данного раздела — конструктор этого класса. Что такое конструктор? Конструктор — это особая функция, инициализирующая значения переменных из private. В нашем конструкторе мы инициализировали amount = 0. При отсутствии конструктора он создастся автоматически.
Далее у нас перечислены методы класса. Согласно стандарту программирования C++ классы должны именоваться с большой буквы (Bank_account). Также все определения должны собираться в особый файл — заголовочный. Он имеет расширение .h
Пусть наш заголовочный файл будет называться bank.h. Тогда мы помещаем определение нашего класса в этот файл:
//bank.h #pragma once class Bank_account private: int amount; public: Bank_account(); int Add(int number); int Remove(int number); void Show_money(); >;
Теперь, если мы захотим использовать класс Bank_account нам нужно будет подключить заголовочный файл “bank.h”. Для определений создадим файл исходного кода def.cpp и поместим туда все определения методов из класса.
//def.cpp #include "bank.h" // обратите внимание, что здесь кавычки #include using namespace std; Bank_account::Bank_account() // конструктор amount = 0; > int Bank_account::Add(int number) amount += number; return amount; > int Bank_account::Remove(int number) amount -= number; return amount; > void Bank_account::Show_money() cout <"Current amount: " <amount <endl; >
Теперь в главном файле нам достаточно подключить “bank.h”. Как же нам теперь использовать данный класс? Это достаточно просто, создаем объект класса Kate (по легенде, Kate открыла счет в банке, и этот счет называется её имененм). Теперь используем методы класса, также как мы это делали с обычными функциями.
// main.cpp #include #include "bank.h" using namespace std; int main() Bank_account Kate; Kate.Add(20); Kate.Show_money(); Kate.Add(50); Kate.Show_money(); Kate.Remove(70); Kate.Show_money(); return 0; >
Данный подход, а именно разделение программы на несколько составляющих файлов называется раздельной компиляцией и очень полезен в промышленном программировании, где стандартизация и упорядоченность играют огромную роль.
Как вы видите, классы — это достаточно мощный инструмент в языках программирования. В данной статье были рассмотрены базовые понятия ООП, и теперь вы и сами в состоянии двигаться дальше.

Наследование — механизм объектно-ориентированного программирования (наряду с инкапсуляцией, полиморфизмом и абстракцией), позволяющий описать новый класс на основе уже существующего (родительского), при этом свойства и функциональность родительского класса заимствуются новым классом.
Класс, от которого произошло наследование, называется базовым, или родительским (англ. base class). Классы, которые произошли от базового, называются потомками, наследниками или производными классами (англ. derived class).
Класс-наследник реализует спецификацию уже существующего класса, что позволяет обращаться с объектами класса-наследника точно так же, как с объектами базового класса.
Абстрактный класс
В системе используются абстрактные классы. Абстрактный класс — это класс, описанный в программе и имеющий поля и методы, но не использующийся непосредственно ни для хранения данных, ни для создания объекта. От абстрактного класса можно наследоваться, создавая экземпляры дочерних классов со всеми полями и методами, объявленными в родителе. Например, абстрактным классом может быть базовый класс «сотрудник вуза», от которого наследуются классы «аспирант», «профессор» и т. д. Так как производные классы имеют общие поля и функции (например, поле «год рождения»), то эти члены класса могут быть описаны в базовом классе.
Способы наследования
В моделях возможно наследование одной сущности от другой. Возможны три способа наследования сущностей. Все способы реализованы в DataObject, поэтому могут использоваться в проекте.
Single Table Inheritance
Все классы-наследники хранятся в одной таблице
![]()
Самый быстрый способ хранения, всегда делается простой select.
![]()
Нужен механизм, чтобы отличать объекты базового класса от наследуемого.
![]()
Сложности с уникальностью, Nullable/NotNullable полями в наследниках.
Для отличия одного класса от другого используется поле дискриминатор типа. В таблице может быть очень много слабозаполненных колонок. Возможно, с этим могут быть проблемы.
Class Table Inheritance
По таблице на каждый тип (в том числе и абстрактный, если у него есть не абстрактные наследники)
![]()
Отсутствует дублирование данных, в каждой таблице только поля конкретного класса.
![]()
Минимальные изменения БД при изменении одного класса.
![]()
Для получения объекта-наследника нужно делать объединение JOIN по нескольким таблицам (для значений родительских полей).
![]()
Нужен механизм, позволяющий понять, сколько есть объектов определенного класса (без учета наследников).
Concrete Table Inheritance
По таблице на каждый конкретный (не абстрактный) класс.
Отличается от предыдущего тем, что не создаются таблицы для абстрактных классов. Таблицы классов содержат все поля, как собственные, так и наследуемые.
В примере: нет таблицы для Player, все остальные таблицы содержат name. Для выборки всех Player — нужны UNION .
![]()
Гораздо быстрее предыдущего варианта для глубокого наследования
![]()
Избыточное хранение данных. Проблемы с синхронизацией.
![]()
Для получения всех объектов класса с учетом наследников надо делать JOIN .
В проекте использованы на данный момент 2 и 3 способы наследования.
Преимущества наследования в учетных системах
Быстрое проектирование
Использование базовых классов позволяет быстро создавать новые документы и справочники, похожие на те, что уже есть в системе.
Объединение списков объектов
Использование базовых классов позволяет получать общие списки различных документов. Например, на основе базового документа «Входящие» мы можем получить журнал входящих документов, состоящий из списка всех его наследников
Легкая поддержка
Для того чтобы изменить маршрут, бизнес правило или какое-либо поведение группы документов, наследуемых от одного документа, достаточно внести изменения только в базовый документ.
Кроме легкости и понятности, в проекте также достигается минимальная избыточность программного кода.
Пример наследования в системе

В качестве примера рассмотрим наследование Справочников. Откроем Справочник Пользователи и роли и посмотрим его структуру через меню Действия -> Структура:
Также откроем структуру справочника Роли и увидим, что он является наследником справочника Пользователи и роли:
Аналогично можно убедиться в том, что Справочник Пользователи — наследник справочника Пользователи и роли:
Проектирование Классов и Интерфейсов (Перевод статьи)

Независимо от того, какой язык программирования вы используете (и Java здесь не исключение), следование правильным принципам проектирования — является ключевым фактором к написанию чистого, понятного и поддающегося проверке кода; а также создавать его долгоживущим, легко поддерживающим решения проблем. В этой части урока мы собираемся обсудить фундаментальные строительные блоки, которые предоставляет язык Java, и ввести пару принципов проектирования, стремясь помочь вам сделать лучшие дизайн-решений. Точнее, мы собираемся обсудить интерфейсы и интерфейсы с использованием методов по умолчанию (новая функция Java 8), абстрактные и окончательные (final) классы, неизменяемые классы, наследование, композицию и пересмотреть правила видимости (или доступности), которых мы кратко коснулись в 1 части урока «How to create and destroy objects».
2. ИНТЕРФЕЙСЫ
В объектно-ориентированном программировании, понятие интерфейсов формирует основы развития контрактов (прим. переводчика: «Контракт» в ООП — набор четко определенных условий, регулирующих отношения между классом-сервером и его клиентами). В двух словах, интерфейсы определяют набор методов (контрактов) и каждый класс, который требует поддержки этого специфичного интерфейса, должен обеспечить реализацию этих методов: довольно простая, но мощная идея. Многие языки программирования имеют интерфейсы в той или иной форме, но Java в особенности обеспечивает поддержку языка для этого. Давайте взглянем на простое интерфейсное определение в Java.
package com.javacodegeeks.advanced.design; public interface SimpleInterface
Во фрагменте выше, интерфейс, который мы назвали SimpleInterface , объявляет только один метод с именем performAction . Главные отличия интерфейсов по отношению к классам — то, что интерфейсы очерчивают, какой должен быть контакт (объявляют метод), но не обеспечивают их реализацию. Однако, интерфейсы в Java могут быть сложнее: они могут включать в себя вложенные интерфейсы, классы, подсчеты, аннотации и константы. Например:
package com.javacodegeeks.advanced.design; public interface InterfaceWithDefinitions < String CONSTANT = "CONSTANT"; enum InnerEnum < E1, E2; >class InnerClass < >interface InnerInterface < void performInnerAction(); >void performAction(); >
В этом более сложном примере, есть несколько ограничений, которые интерфейсы безоговорочно налагают относительно вложенных конструкций и объявлений метода, и к выполнению этого принуждает компилятор Java. Прежде всего, даже если это не было объявлено явно, каждое объявление метода в интерфейсе является публичным (public) (и может быть только публичным). Таким образом следующие объявления методов эквивалентны:
public void performAction(); void performAction();
Стоит упомянуть, что каждый отдельный метод в интерфейсе неявно объявлен абстрактным и даже эти объявления метода эквивалентны:
public abstract void performAction(); public void performAction(); void performAction();
Что касается объявленных полей констант, дополнительно к тому что они являются публичными, они также неявно статические и помечены, как final. Поэтому следующие объявления также эквивалентны:
String CONSTANT = "CONSTANT"; public static final String CONSTANT = "CONSTANT";
И, наконец, вложенные классы, интерфейсы или подсчеты, кроме того, что являются публичными, также неявно объявлены как static. К примеру, данные объявления также эквивалентны:
class InnerClass < >static class InnerClass
Стиль, который вы выберете — это ваше личное предпочтение, однако знание этих простых свойств интерфейсов может спасти вас от ненужного ввода.
3. Интерфейс-маркер
Интерфейс маркеры — это особый вид интерфейса, у которого нет методов или других вложенных конструкций. Как это определяет библиотека Java:
public interface Cloneable
Интерфейс-маркеры — не контракты по сути, но отчасти полезная техника, чтобы «прикрепить» или «связать» некоторую специфическую черту с классом. К примеру, относительно Cloneable, класс помечен как доступный для клонирования, однако, способ, которым это можно или следует реализовать, — не является частью интерфейса. Еще один очень известный и широко используемый пример интерфейс-маркера — это Serializable :
public interface Serializable
Этот интерфейс помечает класс, как пригодный для преобразования в последовательную форму (сериализацию) и десериализацию (deserialization), и снова, это не указывает способ, как это можно или следует реализовывать. Интерфейс-маркеры занимают свое место в объектно-ориентированном программировании, хотя они не удовлетворяют главную цель интерфейса, чтобы быть контрактом.
4. ФУНКЦИОНАЛЬНЫЕ ИНТЕРФЕЙСЫ, МЕТОДЫ ПО УМОЛЧАНИЮ И СТАТИЧЕСКИЕ МЕТОДЫ
С выпусков Java 8, интерфейсы получили новые очень интересные возможности: статические методы, методы по умолчанию и автоматическое преобразование из лямбд (функциональные интерфейсы). В разделе интерфейсы, мы подчеркивали тот факт, что интерфейсы в Java могут только объявлять методы, но не обеспечивают их реализацию. С методом по умолчанию, все иначе: интерфейс может отметить метод ключевым словом default и обеспечить реализацию для него. Например:
package com.javacodegeeks.advanced.design; public interface InterfaceWithDefaultMethods < void performAction(); default void performDefaulAction() < // Implementation here >>
Будучи на уровне экземпляра, методы по умолчанию могли быть переопределены каждой реализацией интерфейса, но теперь интерфейсы также могут включать статические методы, например: package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods < static void createAction() < // Implementation here >>
Можно сказать, что предоставление реализации в интерфейсе наносит поражение целому замыслу контрактного программирования . Но есть много причин, почему эти функции были введены в язык Java и независимо от того, насколько они полезны или сбивающие с толку, они есть там для вас и вашего пользования. Функциональные интерфейсы это совсем другая история и они опробованы, как очень полезные дополнения к языку. В основном, функциональный интерфейс — это интерфейс всего лишь с одним абстрактным методом, объявленным в нем. Runnable интерфейс из стандартной библиотеки — это очень хороший пример этой концепции.
@FunctionalInterface public interface Runnable
Компилятор Java по-разному обрабатывает функциональные интерфейсы и может превратить лямбда-функцию в реализацию функционального интерфейса, где это имеет смысл. Давайте рассмотрим следующее описание функции:
public void runMe( final Runnable r )
Для вызова этой функции в Java 7 и ниже должна предоставляться реализация интерфейса Runnable (например используя анонимные классы), но в Java 8 достаточно передать реализацию метода run() используя синтаксис лямбды:
runMe( () -> System.out.println( "Run!" ) );
Кроме того, аннотация @FunctionalInterface (аннотации будут раскрыты в деталях в 5 части учебника) намекает, что компилятор может проверить, содержит ли интерфейс только один абстрактный метод, поэтому любые изменения, внесенные в интерфейсе в будущем не будет нарушать это предположение.
5. АБСТРАКТНЫЕ КЛАССЫ
Еще одна интересная концепция, поддерживаемая языком Java, — понятие абстрактных классов. Абстрактные классы отчасти похожи на интерфейсы в Java 7 и очень близки интерфейсу с методом по умолчанию в Java 8. В отличие от обычных классов, нельзя создавать экземпляры абстрактного классы, но он может быть подклассом (обратитесь к разделу «Наследование» для получения более подробной информации). Что еще более важно, абстрактные классы могут содержать абстрактные методы: особый вид методов без реализации, так же, как и интерфейс. Например:
package com.javacodegeeks.advanced.design; public abstract class SimpleAbstractClass < public void performAction() < // Implementation here >public abstract void performAnotherAction(); >
В этом примере, класс SimpleAbstractClass объявлен как абстрактный и содержит один объявленный абстрактный метод. Абстрактные классы очень полезны, большинство или даже некоторые части деталей реализации могут совместно использоваться с многими подклассами. Как бы там ни было, они по-прежнему оставляют дверь приоткрытой и позволяют настроить поведение присущее каждому из подкласса с помощью абстрактных методов. Стоит упомянуть, в отличие от интерфейсов, которые могут содержать только публичные объявления, абстрактные классы могут использовать всю мощь правил доступности, чтоб управлять видимостью абстрактного метода.
6. НЕИЗМЕНЯЕМЫЕ КЛАССЫ
Неизменяемость становится все более и более важной в разработке программного обеспечения в наше время. Подъем многоядерных систем вызвало много вопросов, связанных с совместным использованием данных и параллелизмом. Но одна проблема определенно возникла: небольшое (или даже отсутствие) изменяемого состояния приводит к лучшей расширяемости (масштабируемости) и более простому рассуждению о системе. К сожалению, язык Java не обеспечивает достойную поддержку классовой неизменности. Однако, пользуясь комбинацией техник, становится возможно проектировать классы, которые неизменны. Прежде всего, все поля класса должны быть окончательные (помечены как final). Это хорошее начало, но не дает гарантий.
package com.javacodegeeks.advanced.design; import java.util.Collection; public class ImmutableClass < private final long id; private final String[] arrayOfStrings; private final CollectioncollectionOfString; >
Во вторых, следите за правильной инициализацией: если поле является ссылкой на коллекцию или массив, не назначайте те поля непосредственно из аргументов конструктора, вместе этого делайте копии. Это будет гарантировать, что состоянии коллекции или массива не будет изменено за пределами.
public ImmutableClass( final long id, final String[] arrayOfStrings, final Collection collectionOfString) < this.id = id; this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length ); this.collectionOfString = new ArrayList<>( collectionOfString ); >
И наконец, обеспечение надлежащего доступа (гетеры). Для коллекций, неизменяемый вид должен быть предоставлен в виде обертки Collections.unmodifiableXxx : С массивами единственный способ обеспечить настоящую неизменяемость – это предоставить копию вместо возвращения ссылки на массив. Это может быть неприемлемо с практической точки зрения, так как это очень зависит от размера массива и может возложить огромное давление на сборщика мусора.
public String[] getArrayOfStrings()
Даже этот маленький пример дает хорошую идею, что неизменяемость еще не гражданин первого класса в Java. Все может быть усложнено, если неизменяемый класс имеет поле, ссылающийся на объект другого класса. Те классы должны быть также неизменны, однако нет никакого способа это обеспечить. Есть несколько достойных анализаторов исходного кода в Java, как FindBugs и PMD, которые могут существенно помочь, проверяя ваш код и указывая на общие недостатки программирования Java. Эти инструменты — большие друзья любого разработчика Java.
7. АНОНИМНЫЕ КЛАССЫ
В предварительной Java 8 era, анонимные классы были единственным способом обеспечить оперативное определение классов и немедленное создание экземпляра. Целью анонимных классов было уменьшить шаблон и обеспечить краткий и легкий путь представления классов как запись. Давайте взглянем на типичный старомодный путь породить новый поток в Java:
package com.javacodegeeks.advanced.design; public class AnonymousClass < public static void main( String[] args ) < new Thread( // Example of creating anonymous class which implements // Runnable interface new Runnable() < @Override public void run() < // Implementation here >> ).start(); > >
В этом примере реализация Runnable interface предоставляется сразу как анонимный класс. Хотя есть некоторые ограничения, связанные с анонимными классами, основные недостатки их использования — весьма подробный синтаксис конструкций, которым обязывает Java как язык. Даже просто анонимный класс, который ничего не делает, требует по меньшей мере 5 линий кода каждый раз при записи.
new Runnable() < @Override public void run() < >>
К счастью, с Java 8, лямбдой и функциональными интерфейсами все эти стереотипы скоро уйдут, наконец написание кода Java будет выглядеть по настоящему кратко.
package com.javacodegeeks.advanced.design; public class AnonymousClass < public static void main( String[] args ) < new Thread( () -> < /* Implementation here */ >).start(); > >
8. ВИДИМОСТЬ

Мы уже немного говорили о правилах видимости и доступности в Java в части 1 учебника. В этой части мы собираемся вернуться к этой теме снова, но в контексте создания подклассов. Видимость различных уровней разрешает или запрещает классам просматривать другие классы или интерфейсы (например, если они находятся в разных пакетах или вложены друг в друга) или подклассам видеть и получать доступ к методам, конструкторам и полям их родителей. В следующем разделе, наследование, мы увидим это в действии.
9. НАСЛЕДОВАНИЕ
Наследование — одно из ключевых понятий объектно-ориентированного программирования, выступающее в качестве основы построения класса связей. В сочетании с видимостью и правилами доступности, наследование позволяет проектировать классы иерархии, с возможностью расширения и поддерживания. На понятийном уровне, наследование в Java реализуется с помощью создание подклассов и ключевого слова extends, вместе с родительским классом. Подкласс наследует все публичные и защищенные элементы родительского класса. Кроме того, подкласс наследует package-private элементы родительского класса, если оба (подкласс и класс) находятся в одном пакете. При этом, очень важно, независимо от того, что вы пытаетесь спроектировать, придерживаться минимального набора метода, которые класс выставляет публично или для его подклассов. Например, давай те рассмотрим класс Parent и его подкласс Child , чтобы продемонстрировать разницу в уровнях видимости и их эффекты.
package com.javacodegeeks.advanced.design; public class Parent < // Everyone can see it public static final String CONSTANT = "Constant"; // No one can access it private String privateField; // Only subclasses can access it protected String protectedField; // No one can see it private class PrivateClass < >// Only visible to subclasses protected interface ProtectedInterface < >// Everyone can call it public void publicAction() < >// Only subclass can call it protected void protectedAction() < >// No one can call it private void privateAction() < >// Only subclasses in the same package can call it void packageAction() < >>
package com.javacodegeeks.advanced.design; // Resides in the same package as parent class public class Child extends Parent implements Parent.ProtectedInterface < @Override protected void protectedAction() < // Calls parent's method implementation super.protectedAction(); >@Override void packageAction() < // Do nothing, no call to parent's method implementation >public void childAction() < this.protectedField = "value"; >>
Наследование — очень большая тема сама по себе, с большим количеством тонких деталей, характерных для Java. Однако, есть несколько правил, которым легко следовать, и которые могут очень помочь сохранить краткость в классовой иерархии. В Java, каждый подкласс может переопределять любые унаследованные методы его родителя, если он не был объявлен как окончательный (final). Однако, нет специального синтаксиса или ключевого слова, чтобы пометить метод, как переопределенный, что может привести к путанице. Вот почему была введена аннотация @Override: всякий раз , когда ваша цель – переопределить наследуемый метод, пожалуйста, используйте аннотацию @Override, чтобы кратко обозначить это. Другая дилемма, с которой Java разработчики постоянно сталкиваются в проектирование — это построение классов иерархии (с конкретными или абстрактными классами) в сравнении с реализацией интерфейсов. Настоятельно рекомендуем отдавать предпочтение интерфейсам по отношению к классам или абстрактным классам, где это возможно. Интерфейсы более легкие, их проще тестировать и поддерживать, плюс ко всему, они минимизируют побочные эффект изменений реализации. Многие продвинутые техники программирования, такие как создание прокси (proxy) классов в стандартной библиотеке Java, в большей степени полагаются на интерфейсы.
10. МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ
В отличие от С++ и некоторых других языков, Java не поддерживает множественное наследование: в Java каждый класс может иметь только одного прямого родителя (с классом Object в вершине иерархии). Однако, класс может реализовывать несколько интерфейсов, и, таким образом, стекование (stacking) интерфейсов — единственный способ достигнуть (или имитировать) множественное наследование в Java.
package com.javacodegeeks.advanced.design; public class MultipleInterfaces implements Runnable, AutoCloseable < @Override public void run() < // Some implementation here >@Override public void close() throws Exception < // Some implementation here >>
Реализация нескольких интерфейсов на самом деле довольно мощная, но часто необходимость снова и снова использовать реализацию приводит к глубокой классовой иерархии, как способ преодолеть отсутствие поддержки множественного наследования в Java.
public class A implements Runnable < @Override public void run() < // Some implementation here >>
// Class B wants to inherit the implementation of run() method from class A. public class B extends A implements AutoCloseable < @Override public void close() throws Exception < // Some implementation here >>
// Class C wants to inherit the implementation of run() method from class A // and the implementation of close() method from class B. public class C extends B implements Readable < @Override public int read(java.nio.CharBuffer cb) throws IOException < // Some implementation here >>
И так далее.. Недавний выпуск Java 8 несколько решает проблему с внедрением методов по умолчанию. Из-за методов по умолчанию, интерфейсы фактические стали предоставлять не только контракт, но и реализацию. Следовательно, классы, которые реализуют эте интерфейсы, также автоматически унаследуют эти реализованные методы. Например:
package com.javacodegeeks.advanced.design; public interface DefaultMethods extends Runnable, AutoCloseable < @Override default void run() < // Some implementation here >@Override default void close() throws Exception < // Some implementation here >> // Class C inherits the implementation of run() and close() methods from the // DefaultMethods interface. public class C implements DefaultMethods, Readable < @Override public int read(java.nio.CharBuffer cb) throws IOException < // Some implementation here >>
Имейте в виду, что множественное наследование очень мощный, но и в тоже время опасный инструмент. Хорошо известную проблему «Ромб смерти» часто называют основным дефектом реализации множественного наследования, поэтому разработчики вынуждены проектировать классы иерархии весьма тщательно. К сожалению, интерфейсы Java 8 с методами по умолчанию также становится жертвой этих дефектов.
interface A < default void performAction() < >> interface B extends A < @Override default void performAction() < >> interface C extends A < @Override default void performAction() < >>
Например, следующий фрагмент кода не удастся скомпилировать:
// E is not compilable unless it overrides performAction() as well interface E extends B, C
На данный момент, справедливо сказать, что Java как язык всегда пытался избежать угловых случаем объектно-ориентированного программирования, но, так как язык развивается, некоторые из тех случаем стали внезапно появляется.
11. НАСЛЕДОВАНИЕ И КОМПОЗИЦИЯ
К счастью, наследование не единственный путь спроектировать ваш класс. Другой альтернативой, которая, по мнению многих разработчиков, является намного лучшей, чем наследование, — является композиция. Идея очень проста: вместо создания иерархии классов, их нужно компоновать из других классов. Давайте посмотрим на этот пример:
// E is not compilable unless it overrides performAction() as well interface E extends B, C
Класс Vehicle состоит из двигателя (engine) и колес (плюс многие другие части, которые оставлены в стороне для простоты). Однако, можно сказать, что класс Vehicle так же является машиной (engine), так что может быть спроектирована с использованием наследования.
public class Vehicle extends Engine < private Wheels[] wheels; // . >
- Проектирование более гибкое;
- Модель более стабильная, так как изменения не распространяются через классовую иерархию;
- Класс и его композиция слабо связаны по сравнению с композицией, которая плотно связывает родителя и его подкласс.
- Логический ход мысли в классе проще, так как все его зависимости включены в нем же, в одном месте.
12. ИНКАПСУЛЯЦИЯ.
Понятие инкапсуляции в объектно-ориентированном программировании заключается в скрытие всех деталей реализации (как режим работы, внутренние методы и т.д.) от внешнего мира. Преимущества от инкапсуляции в удобстве сопровождения и легкости изменений. Внутренняя реализация класса скрывается, работа с данными класса происходит исключительно через публичные методы класса (реальная проблема, если вы разрабатываете библиотеку или фреймфорки (структуры), использованную многими людьми). Инкапсуляция в Java достигается с помощью правил видимости и доступности. В Java, считается, что лучше всего никогда не выставлять поля напрямую, только посредством гетеров (getters) и сетеров (setters) (если поля не помечены как окончательные (final)). Например:
package com.javacodegeeks.advanced.design; public class Encapsulation < private final String email; private String address; public Encapsulation( final String email ) < this.email = email; >public String getAddress() < return address; >public void setAddress(String address) < this.address = address; >public String getEmail() < return email; >>
Этот пример напоминает то, что называется JavaBeans в языке Java: стандартные классы Java, написаны соответственно набору соглашений, один из которых дает доступ к полям только с помощью гетер и сеттер методов. Как мы уже подчеркивали в разделе наследования, пожалуйста, всегда придерживайте минимальному контракту публичности в классе, используя принципы инкапсуляции. Все, что не должно быть публичным — должно стать приватным (или protected/ package private, зависит от проблемы, что вы решаете). В долгосрочной перспективе это вам окупится, давая вам свободу в проектировании без внесения критических изменений (или, по крайней мере, минимизируют их).
13. ОКОНЧАТЕЛЬНЫЕ КЛАССЫ И МЕТОДЫ
В Java, есть способ предотвратить возможность класса стать подклассом от другого класса: другой класс должен быть объявлен как окончательный (final).
package com.javacodegeeks.advanced.design; public final class FinalClass
Это же ключевое слово final в объявление метода предотвращает возможность переопределения метода в подклассах.
package com.javacodegeeks.advanced.design; public class FinalMethod < public final void performAction() < >>
Нет общих правил, чтобы решить должен класс или методы быть окончательными или нет. Окончательные классы и методы ограничивают расширяемость и очень сложно думать наперед, должен или не должен класс быть унаследованным, или должен или не должен метод быть переопределен в будущем. Это особенно важно для разработчиков библиотеки, поскольку решения проектирования подобны этому могли бы существенно ограничить применимость библиотеки. Стандартная библиотека Java имеет несколько примеров окончательный классов, причем наиболее известный — это класс String. На ранней стадии, было принято данное решение, чтобы предотвратить любые попытки разработчиков появиться с собственным, «лучшим» решением реализации string.
14. ЧТО ДАЛЬШЕ
В этой части урока мы рассмотрели концепции объектно-ориентированного программирования в Java. Мы также кратко прошлись по контрактному программированию , затронули некоторые функциональные понятия и увидели, как язык развивался с течением времени. В следующей части урока мы собираемся встретиться с generics и как они меняют способ приближения типовой безопасности в программировании.