10 принципов ООП, о которых стоит знать каждому программисту

Обложка поста

Перевод статьи «10 OOP Design Principles Every Programmer Should Know»

Многим опытным разработчикам, вероятно, знакома методология объектно-ориентированного программирования (ООП). Кроме известных её принципов (абстракция, инкапсуляция, полиморфизм, наследование и т. д.) существуют и другие — менее известные, но не менее важные и полезные для реализации. Некоторые из них собраны в специальный блок и известны по акрониму SOLID. Эта статья расскажет об этих и других существующих принципах объектно-ориентированной разработки и о том, какие преимущества они предлагают.

Принцип единственной ответственности (SRP)

Соответствует букве S акронима SOLID. Согласно этому принципу, не должно быть более одной причины для изменения класса, или класс должен всегда обрабатывать одну функциональность.

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

Курс по теме From 0 to 1: Design Patterns — 24 That Matter.

Принцип открытости/закрытости (OCP)

Соответствует букве O акронима SOLID. Принцип можно выразить так: «Классы, методы или функции должны быть открыты для расширения (добавления новой функциональности) и закрыты для модификации». Такой подход запрещает кому-либо изменять уже опробованный и протестированный код, а значит, он не ломается. В этом и состоит основное преимущество такого подхода.

Ниже приведён пример кода на Java, который нарушает этот принцип:

public class Shape {
	protected int type=0;
	
	public Shape() {
		// Конструктор
	}
	
	public int getType() {
		return type;
	}
}

public class Circle extends Shape {
	
	public Circle() {
		// Конструктор
		super.type=2;
	}
}

public class Rectangle extends Shape {
	
	public Rectangle() {
		// Конструктор
		super.type=1;
	}
}

public class GraphicEditor{
	public GraphicEditor() {
		// Конструктор
	}
	
	public void drawCircle(Circle c){
		System.out.println("Circle");
	}
	
	public void drawRectangle(Rectangle r){
		System.out.println("Rectangle");
	}
	
	public void drawShape(Shape s) {
		if(s.getType() == 1){
			this.drawRectangle((Rectangle) s);
		}
		else if(s.getType() == 2){
			this.drawCircle((Circle) s);
		}
	}
}

А вот пример после рефакторинга. Теперь соблюдается принцип открытости/закрытости: при добавлении новой реализации Shape не нужно менять код GraphicEditor.

public class Shape {
	protected int type=0;
	
	public Shape() {
		// Конструктор
	}
	
	public int getType() {
		return type;
	}
	
	public void draw(){
		// Метод для отображения фигуры
	}
}

public class Circle extends Shape {
	
	public Circle() {
		// Конструктор
		super.type=2;
	}
	
	public void draw(){
	    // Конкретная реализация для Circle
	}
	
}

public class Rectangle extends Shape {
	
	public Rectangle() {
		// Конструктор
		super.type=1;
	}
	
	public void draw(){
	    // Конкретная реализация для Rectangle
	}
	
}

public class GraphicEditor{
	public GraphicEditor() {
		// Конструктор
	}
	
	public void drawShape(Shape s) {
		s.draw();
	}
}

Курс по теме SOLID Principles of Object-Oriented Design and Architecture.

Принцип подстановки Барбары Лисков (LSP)

Соответствует букве L акронима SOLID. Согласно этому принципу подтипы должны быть заменяемыми для супертипа. Другими словами, методы или функции, работающие с суперклассом, должны иметь возможность без проблем работать также и с его подклассами.

LSP тесно связан с принципом единственной ответственности и принципом разделения интерфейса.

Если класс реализует больше функциональности, чем подкласс, то последний может не поддерживать некоторые функции и тем самым нарушает данный принцип.

Ниже приведён пример такого кода на Java:

class Rectangle {
	
	protected int width = 0;
	protected int height = 0;
	
	public Rectangle() {
	}
	
	public int getWidth() {
		return width;
	}
	
	public int getHeight() {
		return height;
	}
	
	public void setWidth(int width) {
		this.width = width;
	}
	
	public void setHeight(int height) {
		this.height = height;
	}
	
	public int getArea(){
	    return this.width * this.height;
	}
	
}

class Square extends Rectangle {
	
	public Square() {
	}
	
	public void setWidth(int width) {
		this.width = width;
		this.height = width;
	}
	
	public void setHeight(int height) {
		this.height = height;
		this.width = height;
	}
	
}


public class Main
{
    static void resize(Rectangle r, int new_width, int new_height){
        r.setWidth(new_width);
        r.setHeight(new_height); 
    }

	public static void main(String[] args) {
		Rectangle r = new Rectangle();
		Square s = new Square();
		resize(r,2,3);
		resize(s,2,3);
		System.out.println(r.getArea());
		System.out.println(s.getArea());
	}
}

Функция resize() провоцирует неявную ошибку при работе с экземпляром класса Square, потому что позволяет устанавливать отличные друг от друга значения ширины и высоты. Согласно принципу LSP, функции, использующие ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом. Поэтому для корректной работы функция resize() должна проверять, является ли передаваемый объект экземпляром класса Square, и в этом случае не позволять установить разные значения ширины и высоты. Отсюда идёт нарушение принципа.

Курс по теме SOLID Principles of Object-Oriented Design.

Принцип разделения интерфейса (ISP)

Соответствует букве I акронима SOLID. Этот принцип подразумевает, что интерфейс, который не используется, не должен быть реализован.

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

Написание интерфейса — сложная задача. Когда он готов, вы не сможете изменить его, не нарушив всю реализацию.

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

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

Принцип инверсии зависимостей (DIP)

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

Ниже приведён пример кода Java, который нарушает принцип инверсии зависимости:

public class EventLogWriter {
	
	public EventLogWriter() {
		// Конструктор
	}
	
	public write(String message) {
		// Конструктор
	}
}

public class AppManager {
	
	EventLogWriter writer = null;
	
	public AppManager() {
		// Конструктор
	}
	
	void notify(String message) {
		
		if (writer == null) {
			
			writer = new EventLogWriter();
		}
		
		writer.write(message);
	}
}

Пример демонстрирует, что AppManager зависит от EventLogWriter. Если вам нужно использовать другой способ уведомления клиента (например push-уведомления, SMS или электронную почту), необходимо изменить класс AppManager.

interface IWritable {
    public void write(String message);
}

class EventLogWriter implements IWritable {
	
	public EventLogWriter() {
	}
	
	public void write(String message) {
		// Конкретная реализация 
	}
}

class AppManager {
	
	IWritable writer = null;
	
	public AppManager(IWritable writer) {
	    this.writer = writer;
	}
	
	void notify(String message) {
		this.writer.write(message);
	}
}

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

Курс по теме Using SOLID Principles to Write Better Code — A Crash Course.

Теперь перейдём к принципам, которые не входят в пятёрку SOLID, но не менее важны.

DRY (Don’t Repeat Yourself)

Переводится как «не повторяйся» и буквально означает, что нужно уходить от дублирующего кода и по возможности использовать абстракцию для общих вещей.

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

Но важно не злоупотреблять этим принципом. Например, один и тот же код не подойдёт для проверки OrderId и SSN. Их форматы могут не совпадать, и на выходе функция выдаст некорректный результат. В качестве решения можно предусмотреть в методе проверку форматов для подобных наборов чисел.

Курс по теме Basics of Software Architecture & Design Patterns для Java на Udemy.

Инкапсуляция изменяющегося кода

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

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

Воспользуйтесь алгоритмом, по которому переменные и методы по умолчанию имеют спецификатор private. Затем шаг за шагом увеличиваете доступ при необходимости (с private на protected, с protected на public).

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

Курс по теме Design Pattern Library.

Композиция вместо наследования

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

Композиция позволяет изменять поведение класса прямо во время выполнения через установку его свойств. Реализуя интерфейсы, вы, таким образом, используете полиморфизм, который обеспечивает более гибкую реализацию.

«Effective Java» Джошуа Блоха также советует отдавать предпочтение композиции вместо наследования. Если вы всё ещё не уверены, вы также можете посмотреть здесь, чтобы узнать, почему композиция лучше, чем наследование для повторного использования кода и его функциональности.

Курс по теме Object-Oriented Programming in Java.

Программирование для интерфейса

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

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

Имеется в виду

List numbers= getNumbers();

вместо

ArrayList numbers = getNumbers();

Это также рекомендовано во многих книгах по Java, в том числе в Effective Java и Head First design pattern.

Ниже приведён пример для интерфейса в Java:

package tool;
import java.util.Arrays;
import java.util.List;
import java.util.Stream.Collectors;

/**
	Программа для демонстрации написания интерфейса
	*/
	
public class Hello {
	
	public static void main(String args[]) {
	
	// Использование интерфейса в качестве типа переменных
	List rawMessage = Arrays.asList("one", "two", "three");
	List allcaps = toCapitalCase(rawMessage);
	System.out.println(allcaps);
	
	}

/**
	Используем интерфейс в качестве типа аргумента и возвращаем тип
	*/
	public static List toCapitalCase(List messages) {
		
		return messages.stream()
						.map(String::toUpperCase)
						.collect(Collectors.toList());
	}
}

Курс по теме  Refactoring to Design Patterns».

Принцип делегирования

Не делайте всё самостоятельно, делегируйте это в соответствующий класс. Классическим примером этого принципа являются методы equals() и hashCode() в Java. Если нужно сравнить два объекта, это действие поручается соответствующему классу вместо клиентского.

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

Курс по теме  5 Free Object-Oriented Programming Online Courses for Programmers.

Заключение

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

Курс по теме Шаблоны проектирования.

Хинт для программистов: если зарегистрироваться на соревнования Huawei Honor Cup, бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании. Перейти к регистрации.