Введение

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

Что изменилось?

Я считаю, что есть 3 различных этапа обучения программированию: изучение основ (например, циклов, ветвей и т. д.), решение простых задач (например, упражнения для начинающих по алгоритмам, смоделированные задачи реального мира) и программирование проектов (> 1 kLoC, работа в команда). Недавно я перешел со второго на третий этап обучения и постепенно осознал важность принципов и шаблонов в разработке программного обеспечения. Когда вы пишете свое собственное небольшое решение, не работая ни с кем, вы более или менее понимаете свой код, когда читаете его (поскольку вы тот, кто его написал!). Однако при работе в команде, где есть другие люди, которые обязаны читать ваш код, велика вероятность того, что он будет не так читаем, если вы не будете следовать принципам и шаблонам. Поэтому, если вы находитесь на переходном этапе между вторым и третьим этапом изучения программирования, читайте дальше! (Хотя, если вы находитесь на других этапах и хотели бы узнать больше о том, как работают принципы, вам можно и продолжить!)

Я уже некоторое время проболтался, особо не касаясь основной темы этой статьи. Так что же такое принципы S.O.L.I.D? Это аббревиатура для 5 различных принципов, которым полезно следовать в своих инженерных проектах. Прежде чем я продолжу, в этой статье предполагается, что у вас есть хотя бы приличное понимание объектно-ориентированного программирования (ООП).

S — принцип единой ответственности

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

class Library {
	getNumberOfBooks();
	borrowBook(Book book);
	returnBook(Book book);
	librarianWages(); // Can be abstracted out!
	hireLibrarian(Librarian librarian) // Can be abstracted out!
}

Как мы видим из этого примера класса Library, может быть легко поместить все, что связано с библиотекой, внутрь этого класса. Однако у этого конкретного класса теперь есть две обязанности — управление книгами и библиотекари. Класс Library должен сосредоточиться только на своих книгах, в то время как вопросы библиотекаря могут быть абстрагированы вместо этого в класс LibraryHumanResources!

O — открытый/закрытый принцип

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

interface Dog {
	bark();
	guideHuman(); // Wrong!
}
interface GuideDog extends Dog {
	guideHuman(); // Correct!
}

В приведенном выше примере у нас есть класс Dog и класс GuideDog. Если мы хотим реализовать метод guideHuman(), мы должны создать новый класс GuideDog, который расширяется от Dog, и реализовать в нем указанный метод. Реализация guideHuman() в классе Dog является чрезмерным обобщением ответственности Dog.

L — принцип замены Лисков

Названный в честь ученого-компьютерщика, разработавшего этот принцип, Барбары Лисков, этот принцип может быть немного сложным для понимания. Этот принцип является продолжением предыдущего принципа (принцип открытости/закрытости), и его формальное определение выглядит следующим образом:

Пусть Φ(x) — доказуемое свойство объектов x типа T. Тогда Φ(y) должно быть истинным для объектов y типа S, где S — подтип T.

Я уверен, что это достаточно просто для вас, чтобы понять! Итак, давайте перейдем к следующему принципу…

Хорошо, я шучу. Но если разбить его на термины ООП, этот принцип гласит, что родительские классы должны заменяться его производными классами без нарушения работы приложения. Один из самых известных примеров — отношения между прямоугольником и квадратом. Квадрат является прямоугольником, верно? Либо это…

class Rectangle {
	int height = 1;
	int width = 1;
	Rectangle() {}
	void setHeight(int h) {
		this.height = h;
	}
	void setWidth(int w) {
		this.width = w;
	}
}
class Square extends {
	int height = 1;
	int width = 1;
	Square() {}
	
	@Override
	void setHeight(int h) {
		this.height = h;
		this.width = h;
	}
	
	@Override
	void setWidth(int w) {
		this.width = w;
		this.height = w;
	}
}
class Main {
	Rectangle [] rectangles = new Rectangle[3];
	for (int i = 1; i <= 3; i++) {
		Square s = new Square();
		s.setHeight(i);
	}
}

Приведенный выше код определенно работает, но вы уверены, что хотите, чтобы он работал именно так?

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

LSP может быть очень абстрактным для понимания, однако вот две меры предосторожности, основанные на этом принципе, которые вы должны по крайней мере убрать:

  1. Тщательно подумайте, прежде чем внедрять отношения «является». Некоторые объекты могут выглядеть так, как будто они образуют эту связь, но подумайте, чем они отличаются, и должны ли они вместо этого быть двумя отдельными типами.
  2. Если вы расширяетесь от родительского класса, но вам нужно переопределить один из методов родительского класса, вы, вероятно, нарушаете LSP.

I — Принцип разделения интерфейсов

Интернет-провайдер утверждает, что ни один клиент не должен зависеть от методов, которые он не использует. Глядя на название этого принципа, вы можете предположить, что он поощряет разделение интерфейсов на более мелкие. Так как же это помогает в определении этого принципа? Давайте смоделируем пример платформы электронной коммерции с поставщиками и потребителями.

interface ECommercePlatform {
	login(Credentials);
	sellItems(Items items);
	abstract buyItems(Items items);
}
class SupplierPlatform extends ECommercePlatform {
	login(Credentials credentials) {
		// Some implementation
	};
	sellItems(Items items) {
		// Some implementation
	};
	buyItems(Items items) {
		throw Exception("Suppliers can't buy items!");
	};
}
class ConsumerPlatform extends ECommercePlatform {
	login(Credentials credentials) {
		// Some implementation
	};
	buyItems(Items items) {
		// Some implementation
	};
	sellItems(Items items) {
		throw Exception("Consumers can't list items!");
	};
}

Оба SupplierPlatform и ConsumerPlatform зависят от ECommercePlatform, однако оба также зависят от методов, которые не используют - buyItems() и sellItems() соответственно. Отсюда определенно лучше разделить интерфейс ECommercePlatform, например. ECommerceSupplierPlatform и ECommerceConsumerPlatform , где buyItems() и sellItems() могут быть реализованы вместо этого соответственно.

D — Принцип инверсии зависимостей

DIP определяется Робертом С. Мартином следующим образом:

  1. Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

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

Вывод

Я рассмотрел 5 принципов S.O.L.I.D и надеюсь, что вы лучше поняли не только их определение, но и то, как они улучшили наш код. Большинство принципов в программной инженерии так или иначе связаны между собой, хотя не все из них применимы постоянно. Самое главное, как разработчик, вы должны знать, когда применять эти принципы и как их применять. По мере того, как мы приобретаем больше опыта, то, как мы оцениваем наш уровень технических навыков, зависит не от количества кода, который мы пишем, а от того, как мы его пишем. А теперь я желаю вам всего наилучшего в вашем путешествии по программированию!