Наследяване и композиция (Is-a срещу Има-връзка) в Java

1. Общ преглед

Наследяването и композицията - заедно с абстракция, капсулиране и полиморфизъм - са крайъгълните камъни на обектно-ориентираното програмиране (ООП).

В този урок ще разгледаме основите на наследяването и състава и ще се съсредоточим силно върху откриването на разликите между двата типа взаимоотношения.

2. Основи на наследството

Наследяването е мощен, но прекалено използван и злоупотребен механизъм.

Просто казано, с наследяване, базовият клас (известен още като основен тип) дефинира състоянието и поведението, общо за даден тип и позволява подкласовете (известни също като подтипове) да предоставят специализирани версии на това състояние и поведение.

За да имаме ясна представа за това как да работим с наследяване, нека създадем наивен пример: базов клас Person, който дефинира общите полета и методи за даден човек, докато подкласовете Сервитьорка и Актриса предоставят допълнителни, фино изпълнени методи.

Ето го класът Person :

public class Person { private final String name; // other fields, standard constructors, getters }

И това са подкласовете:

public class Waitress extends Person { public String serveStarter(String starter) { return "Serving a " + starter; } // additional methods/constructors } 
public class Actress extends Person { public String readScript(String movie) { return "Reading the script of " + movie; } // additional methods/constructors }

В допълнение, нека създадем единичен тест, за да проверим, че екземплярите на класовете сервитьорка и актриса също са екземпляри на Person , като по този начин показваме, че условието „is-a“ е изпълнено на ниво тип:

@Test public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() { assertThat(new Waitress("Mary", "[email protected]", 22)) .isInstanceOf(Person.class); } @Test public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() { assertThat(new Actress("Susan", "[email protected]", 30)) .isInstanceOf(Person.class); }

Тук е важно да подчертаем семантичния аспект на наследяването . Освен повторното използване на изпълнението на класа Person , ние създадохме добре дефинирана връзка „is-a“ между базовия тип Person и подтиповете сервитьорка и актриса . Сервитьорките и актрисите всъщност са хора.

Това може да ни накара да попитаме: в кои случаи на употреба наследяването е правилният подход?

Ако подтиповете отговарят на условието „is-a“ и осигуряват главно адитивна функционалност по-надолу по йерархията на класовете, тогава наследяването е пътят.

Разбира се, заместването на метода е разрешено, докато заменените методи запазват основния тип / подтип заменяемост, насърчаван от Принципа на заместване на Лисков.

Освен това трябва да имаме предвид, че подтиповете наследяват API на базовия тип , което в някои случаи може да е прекалено или просто нежелано.

В противен случай вместо това трябва да използваме състав.

3. Наследяване в дизайнерските модели

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

3.1. Моделът на супертипа на слоя

В този случай използваме наследство, за да преместим общ код в основен клас (супертип) на база на слой .

Ето основно изпълнение на този модел в домейн слоя:

public class Entity { protected long id; // setters } 
public class User extends Entity { // additional fields and methods } 

Можем да приложим същия подход към останалите слоеве в системата, като слоевете за обслужване и постоянство.

3.2. Шаблонът на метода на шаблона

В шаблона на метода на шаблона можем да използваме основен клас, за да дефинираме инвариантните части на алгоритъм и след това да внедрим вариантните части в подкласовете :

public abstract class ComputerBuilder { public final Computer buildComputer() { addProcessor(); addMemory(); } public abstract void addProcessor(); public abstract void addMemory(); } 
public class StandardComputerBuilder extends ComputerBuilder { @Override public void addProcessor() { // method implementation } @Override public void addMemory() { // method implementation } }

4. Основи на композицията

Съставът е друг механизъм, предоставен от ООП за повторно използване на изпълнението.

Накратко, композицията ни позволява да моделираме обекти, които са съставени от други обекти , като по този начин дефинира връзката „има-а“ между тях.

Освен това композицията е най-силната форма на асоцииране , което означава, че обектът (ите), които съставят или се съдържат от един обект, също се унищожават, когато този обект бъде унищожен .

За да разберем по-добре как работи композицията, нека предположим, че трябва да работим с обекти, които представляват компютри .

Компютърът е съставен от различни части, включително микропроцесора, паметта, звуковата карта и т.н., така че можем да моделираме както компютъра, така и всяка негова част като отделни класове.

Ето как може да изглежда една проста реализация на класа Computer :

public class Computer { private Processor processor; private Memory memory; private SoundCard soundCard; // standard getters/setters/constructors public Optional getSoundCard() { return Optional.ofNullable(soundCard); } }

Следните класове моделират микропроцесор, паметта и звуковата карта (интерфейсите са пропуснати за краткост):

public class StandardProcessor implements Processor { private String model; // standard getters/setters }
public class StandardMemory implements Memory { private String brand; private String size; // standard constructors, getters, toString } 
public class StandardSoundCard implements SoundCard { private String brand; // standard constructors, getters, toString } 

Лесно е да се разберат мотивациите, които стоят зад изместването на композицията върху наследяването. Във всеки сценарий, при който е възможно да се установи семантично коректна връзка „има-има“ между даден клас и други, композицията е правилният избор.

In the above example, Computer meets the “has-a” condition with the classes that model its parts.

It's also worth noting that in this case, the containing Computer object has ownership of the contained objects if and only if the objects can't be reused within another Computer object. If they can, we'd be using aggregation, rather than composition, where ownership isn't implied.

5. Composition Without Abstraction

Alternatively, we could've defined the composition relationship by hard-coding the dependencies of the Computer class, instead of declaring them in the constructor:

public class Computer { private StandardProcessor processor = new StandardProcessor("Intel I3"); private StandardMemory memory = new StandardMemory("Kingston", "1TB"); // additional fields / methods }

Разбира се, това би било твърд, плътно свързан дизайн, тъй като бихме направили компютъра силно зависим от конкретни изпълнения на процесор и памет .

Не бихме се възползвали от нивото на абстракция, осигурено от интерфейси и инжектиране на зависимост.

С първоначалния дизайн, базиран на интерфейси, получаваме слабо свързан дизайн, който също е по-лесен за тестване.

6. Заключение

В тази статия научихме основите на наследяването и композицията в Java и проучихме задълбочено разликите между двата типа взаимоотношения („is-a“ срещу „has-a“).

Както винаги, всички примерни кодове, показани в този урок, са достъпни в GitHub.