Объектно-ориентированное программирование принесло в разработку ПО новые подходы к проектированию приложений. В частности, ООП позволило программистам комбинировать сущности, объединённые некоей общей целью или функционалом, в отдельных классах, рассчитанных на решение самостоятельных задач и независимых от других частей приложения. Однако само по себе применение ООП не означает, что разработчик застрахован от возможности создания непонятного, запутанного кода, который тяжело поддерживать. Роберт Мартин, для того, чтобы помочь всем желающим разрабатывать качественные ООП-приложения, разработал пять принципов объектно-ориентированного программирования и проектирования, говоря о которых, с подачи Майкла Фэзерса, используют акроним SOLID.
Материал, перевод которого мы сегодня публикуем, посвящён основам SOLID и предназначен для начинающих разработчиков.
class Animal {
constructor(name: string){ }
getAnimalName() { }
saveAnimal(a: Animal) { }
}
Animal
, представленный здесь, описывает какое-то животное. Этот класс нарушает принцип единственной ответственности. Как именно нарушается этот принцип?saveAnimal
и манипулируя свойствами объекта в конструкторе и в методе getAnimalName
.Animal
:class Animal {
constructor(name: string){ }
getAnimalName() { }
}
class AnimalDB {
getAnimal(a: Animal) { }
saveAnimal(a: Animal) { }
}
Animal
.class Animal {
constructor(name: string){ }
getAnimalName() { }
}
Animal
, и узнать о том, какие звуки они издают. Представим, что мы решаем эту задачу с помощью функции AnimalSounds
://...
const animals: Array<Animal> = [
new Animal('lion'),
new Animal('mouse')
];
function AnimalSound(a: Array<Animal>) {
for(int i = 0; i <= a.length; i++) {
if(a[i].name == 'lion')
return 'roar';
if(a[i].name == 'mouse')
return 'squeak';
}
}
AnimalSound(animals);
AnimalSound
не соответствует принципу открытости-закрытости, так как, например, при появлении новых видов животных, нам, для того, чтобы с её помощью можно было бы узнавать звуки, издаваемые ими, придётся её изменить.//...
const animals: Array<Animal> = [
new Animal('lion'),
new Animal('mouse'),
new Animal('snake')
]
//...
AnimalSound
://...
function AnimalSound(a: Array<Animal>) {
for(int i = 0; i <= a.length; i++) {
if(a[i].name == 'lion')
return 'roar';
if(a[i].name == 'mouse')
return 'squeak';
if(a[i].name == 'snake')
return 'hiss';
}
}
AnimalSound(animals);
if
.AnimalSound
в соответствие с принципом открытости-закрытости? Например — так:class Animal {
makeSound();
//...
}
class Lion extends Animal {
makeSound() {
return 'roar';
}
}
class Squirrel extends Animal {
makeSound() {
return 'squeak';
}
}
class Snake extends Animal {
makeSound() {
return 'hiss';
}
}
//...
function AnimalSound(a: Array<Animal>) {
for(int i = 0; i <= a.length; i++) {
a[i].makeSound();
}
}
AnimalSound(animals);
Animal
теперь есть виртуальный метод makeSound
. При таком подходе нужно, чтобы классы, предназначенные для описания конкретных животных, расширяли бы класс Animal
и реализовывали бы этот метод.makeSound
, а при переборе массива с животными в функции AnimalSound
достаточно будет вызвать этот метод для каждого элемента массива.AnimalSound
менять не придётся. Мы привели её в соответствие с принципом открытости-закрытости.class Discount {
giveDiscount() {
return this.price * 0.2
}
}
fav
) клиентам даётся скидка в 20%, а VIP-клиентам (vip
) — удвоенная скидка, то есть — 40%. Для того, чтобы реализовать эту логику, было решено модифицировать класс следующим образом:class Discount {
giveDiscount() {
if(this.customer == 'fav') {
return this.price * 0.2;
}
if(this.customer == 'vip') {
return this.price * 0.4;
}
}
}
Discount
. В этом новом классе мы и реализуем новый механизм:class VIPDiscount: Discount {
getDiscount() {
return super.getDiscount() * 2;
}
}
class SuperVIPDiscount: VIPDiscount {
getDiscount() {
return super.getDiscount() * 2;
}
}
Animal
. Напишем функцию, предназначенную для возврата информации о количествах конечностей животного.//...
function AnimalLegCount(a: Array<Animal>) {
for(int i = 0; i <= a.length; i++) {
if(typeof a[i] == Lion)
return LionLegCount(a[i]);
if(typeof a[i] == Mouse)
return MouseLegCount(a[i]);
if(typeof a[i] == Snake)
return SnakeLegCount(a[i]);
}
}
AnimalLegCount(animals);
//...
class Pigeon extends Animal {
}
const animals[]: Array<Animal> = [
//...,
new Pigeon();
]
function AnimalLegCount(a: Array<Animal>) {
for(int i = 0; i <= a.length; i++) {
if(typeof a[i] == Lion)
return LionLegCount(a[i]);
if(typeof a[i] == Mouse)
return MouseLegCount(a[i]);
if(typeof a[i] == Snake)
return SnakeLegCount(a[i]);
if(typeof a[i] == Pigeon)
return PigeonLegCount(a[i]);
}
}
AnimalLegCount(animals);
Animal
в нашем случае) должны также принимать и возвращать значения, типами которых являются его подклассы (Pigeon
).AnimalLegCount
:function AnimalLegCount(a: Array<Animal>) {
for(let i = 0; i <= a.length; i++) {
a[i].LegCount();
}
}
AnimalLegCount(animals);
LegCount
. Всё, что она знает о типах — это то, что обрабатываемые ей объекты должны принадлежать классу Animal
или его подклассам.Animal
должен появиться метод LegCount
:class Animal {
//...
LegCount();
}
//...
class Lion extends Animal{
//...
LegCount() {
//...
}
}
//...
LegCount
для экземпляра класса Lion
производится вызов метода, реализованного в этом классе, и возвращается именно то, что можно ожидать от вызова подобного метода.AnimalLegCount
не нужно знать о том, объект какого именно подкласса класса Animal
она обрабатывает для того, чтобы узнать сведения о количестве конечностей у животного, представленного этим объектом. Функция просто вызывает метод LegCount
класса Animal
, так как подклассы этого класса должны реализовывать этот метод для того, чтобы их можно было бы использовать вместо него, не нарушая правильность работы программы.Shape
:interface Shape {
drawCircle();
drawSquare();
drawRectangle();
}
drawCircle
), квадратов (drawSquare
) и прямоугольников (drawRectangle
). В результате классы, реализующие этот интерфейс и представляющие отдельные геометрические фигуры, такие, как круг (Circle), квадрат (Square) и прямоугольник (Rectangle), должны содержать реализацию всех этих методов. Выглядит это так:class Circle implements Shape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}
class Square implements Shape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}
class Rectangle implements Shape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}
Rectangle
, представляющий прямоугольник, реализует методы (drawCircle
и drawSquare
), которые ему совершенно не нужны. То же самое можно заметить и при анализе кода двух других классов.Shape
ещё один метод, drawTriangle
, предназначенный для рисования треугольников:interface Shape {
drawCircle();
drawSquare();
drawRectangle();
drawTriangle();
}
drawTriangle
. В противном случае возникнет ошибка.Shape
из нашего примера. Клиенты (у нас это классы Circle
, Square
и Rectangle
) не должны реализовывать методы, которые им не нужно использовать. Кроме того, этот принцип указывает на то, что интерфейс должен решать лишь какую-то одну задачу (в этом он похож на принцип единственной ответственности), поэтому всё, что выходит за рамки этой задачи, должно быть вынесено в другой интерфейс или интерфейсы.Shape
решает задачи, для решения которых необходимо создать отдельные интерфейсы. Следуя этой идее, переработаем код, создав отдельные интерфейсы для решения различных узкоспециализированных задач:interface Shape {
draw();
}
interface ICircle {
drawCircle();
}
interface ISquare {
drawSquare();
}
interface IRectangle {
drawRectangle();
}
interface ITriangle {
drawTriangle();
}
class Circle implements ICircle {
drawCircle() {
//...
}
}
class Square implements ISquare {
drawSquare() {
//...
}
}
class Rectangle implements IRectangle {
drawRectangle() {
//...
}
}
class Triangle implements ITriangle {
drawTriangle() {
//...
}
}
class CustomShape implements Shape {
draw(){
//...
}
}
ICircle
используется лишь для рисования кругов, равно как и другие специализированные интерфейсы — для рисования других фигур. Интерфейс Shape
может применяться в качестве универсального интерфейса.class XMLHttpService extends XMLHttpRequestService {}
class Http {
constructor(private xmlhttpService: XMLHttpService) { }
get(url: string , options: any) {
this.xmlhttpService.request(url,'GET');
}
post() {
this.xmlhttpService.request(url,'POST');
}
//...
}
Http
представляет собой высокоуровневый компонент, а XMLHttpService
— низкоуровневый. Такая архитектура нарушает пункт A принципа инверсии зависимостей: «Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций».Http
вынужденно зависит от класса XMLHttpService
. Если мы решим изменить механизм, используемый классом Http
для взаимодействия с сетью — скажем, это будет Node.js-сервис или, например, сервис-заглушка, применяемый для целей тестирования, нам придётся отредактировать все экземпляры класса Http
, изменив соответствующий код. Это нарушает принцип открытости-закрытости. Http
не должен знать о том, что именно используется для организации сетевого соединения. Поэтому мы создадим интерфейс Connection
:interface Connection {
request(url: string, opts:any);
}
Connection
содержит описание метода request
и мы передаём классу Http
аргумент типа Connection
:class Http {
constructor(private httpConnection: Connection) { }
get(url: string , options: any) {
this.httpConnection.request(url,'GET');
}
post() {
this.httpConnection.request(url,'POST');
}
//...
}
Http
может пользоваться тем, что ему передали, не заботясь о том, что скрывается за интерфейсом Connection
.XMLHttpService
таким образом, чтобы он реализовывал этот интерфейс:class XMLHttpService implements Connection {
const xhr = new XMLHttpRequest();
//...
request(url: string, opts:any) {
xhr.open();
xhr.send();
}
}
Connection
и подходящих для использования в классе Http
для организации обмена данными по сети:class NodeHttpService implements Connection {
request(url: string, opts:any) {
//...
}
}
class MockHttpService implements Connection {
request(url: string, opts:any) {
//...
}
}
Http
(высокоуровневый модуль) зависит от интерфейса Connection
(абстракция). Классы XMLHttpService
, NodeHttpService
и MockHttpService
(низкоуровневые модули) также зависят от интерфейса Connection
.XMLHttpService
, NodeHttpService
и MockHttpService
могут служить заменой базовому типу Connection
.К сожалению, не доступен сервер mySQL