Принципы объектно-ориентированного проектирования в Java

2 min


Введение

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

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

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

Некоторые из наиболее важных принципов проектирования в объектно-ориентированной парадигме перечислены в этой статье, но это далеко не полный список.

Принципы SRP, LSP, Open / Closed и DIP часто объединяются и называются SOLID принципы.

Не повторяй принцип (СУХОЙ)

Не повторяй себя (СУХОЙ) принцип является общим принципом в парадигмах программирования, но он особенно важен в ООП. По принципу:

Каждая часть знаний или логики должна иметь одно, однозначное представление в системе,

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

public class Animal {
    public void eatFood() {
        System.out.println("Eating food...");
    }
}

public class Cat extends Animal {
    public void meow() {
        System.out.println("Meow! *purrs*");
    }
}

public class Dog extends Animal {
    public void woof() {
        System.out.println("Woof! *wags tail*");
    }
}

Оба Cat и Dog Нужно есть пищу, но они говорят по-другому. Поскольку употребление пищи является для них общей функциональностью, мы можем абстрагировать ее в родительский класс, такой как Animal а затем попросите их расширить класс.

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

Cat cat = new Cat();
cat.eatFood();
cat.meow();

Dog dog = new Dog();
dog.eatFood();
dog.woof();

Выход будет:

Eating food...
Meow! *purrs*
Eating food...
Woof! *wags tail*

Всякий раз, когда есть константа, которая используется несколько раз, рекомендуется определить ее как общедоступную константу:

static final int GENERATION_SIZE = 5000;
static final int REPRODUCTION_SIZE = 200;
static final int MAX_ITERATIONS = 1000;
static final float MUTATION_SIZE = 0.1f;
static final int TOURNAMENT_SIZE = 40;

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

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

Примечание: Из-за соглашения об именах в Java они должны начинаться с прописных букв, разделенных подчеркиванием ("_").

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

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

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

Хорошая архитектура может амортизировать это, но проблема может возникнуть на практике, тем не менее.

Нарушения принципа СУХОЙ

Нарушения принципа СУХОГО часто называют Влажные решения, WET может быть сокращением для нескольких вещей:

  • Нам нравится печатать
  • Тратить время каждого
  • Пиши каждый раз
  • Пишите все дважды

Решения WET не всегда плохи, так как повторение иногда рекомендуется в классах, которые по своей сути отличаются, или для того, чтобы сделать код более читабельным, менее взаимозависимым и т. Д.

Держите это простым и глупым (KISS) принцип

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

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

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

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

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

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

Принцип единой ответственности (SRP) утверждает, что никогда не должно быть двух функциональных возможностей в одном классе. Иногда это перефразируется как:

«У класса должна быть только одна и только одна причина для изменения».

Где «причина для изменения» является обязанностью класса. Если есть несколько обязанностей, есть больше причин, чтобы изменить этот класс в какой-то момент.

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

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

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

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

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

Вместо этого мы определяем класс, такой как ProductService что бы получить продукт из базы данных, ProductController для обработки информации, а затем мы отображаем ее на уровне представления – либо на HTML-странице, либо в другом классе / графическом интерфейсе.

Открытый / Закрытый Принцип

Открыто закрыто принцип гласит, что классы или объекты и методы должны быть открыты для расширения, но закрыты для изменений.

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

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

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

Принцип замещения Лискова (LSP)

Согласно Принцип замещения Лискова (LSP), производные классы должны иметь возможность заменять свои базовые классы без изменения поведения вашего кода.

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

Распространенный способ, которым люди думают об объектных отношениях (которые иногда могут вводить в заблуждение), – это необходимость это отношения между классами.

Например:

  • Car это Vehicle
  • TeachingAssistaint это CollegeEmployee

Важно отметить, что эти отношения не идут в обоих направлениях. Дело в том, что Car это Vehicle может не означать, что Vehicle это Car – это может быть Motorcycle, Bicycle, Truck

Причина, по которой это может вводить в заблуждение, – распространенная ошибка, которую люди допускают, думая об этом на естественном языке. Например, если бы я спросил вас, если Square имеет "отношения" с RectangleВы можете автоматически сказать «да».

Ведь из геометрии мы знаем, что квадрат является особый случай прямоугольника. Но в зависимости от того, как реализованы ваши структуры, это может быть не так:

public class Rectangle {
    protected double a;
    protected double b;

    public Rectangle(double a, double b) {
        this.a = a;
        this.b = b;
    }

    public void setA(double a) {
        this.a = a;
    }

    public void setB(double b) {
        this.b = b;
    }

    public double calculateArea() {
        return a*b;
    }
}

Теперь давайте попробуем унаследовать это от нашего Square в том же пакете:

public class Square extends Rectangle {
    public Square(double a) {
        super(a, a);
    }

    @Override
    public void setA(double a) {
        this.a = a;
        this.b = a;
    }

    @Override
    public void setB(double b) {
        this.a = b;
        this.b = b;
    }
}

Вы заметите, что сеттеры на самом деле устанавливают оба a а также b, Некоторые из вас уже могут догадаться о проблеме. Допустим, мы инициализировали наш Square и примененный полиморфизм, чтобы содержать его в пределах Rectangle переменная:

Rectangle rec = new Square(5);

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

rec.setA(6);
rec.setB(3);

Они получат совершенно неожиданное поведение, и может быть трудно проследить, в чем проблема.

Если они пытаются использовать rec.calculateArea() результат не будет 18 как они могли ожидать от прямоугольника со сторонами длины 6 а также 3,

Результат будет вместо 9 потому что их прямоугольник на самом деле квадрат и имеет две равные стороны – длины 3,

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

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

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

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

Например, Pizza интерфейс не должен требоваться для реализации addPepperoni() метод, потому что это не должно быть доступно для каждого типа пиццы. Ради этого урока давайте предположим, что у всех пицц есть соус, и их нужно запекать, и нет ни одного исключения.

Это когда мы можем определить интерфейс:

public interface Pizza {
    void addSauce();
    void bake();
}

А потом, давайте реализуем это через пару классов:

public class VegetarianPizza implements Pizza {
    public void addMushrooms() {System.out.println("Adding mushrooms");}

    @Override
    public void addSauce() {System.out.println("Adding sauce");}

    @Override
    public void bake() {System.out.println("Baking the vegetarian pizza");}
}

public class PepperoniPizza implements Pizza {
    public void addPepperoni() {System.out.println("Adding pepperoni");}

    @Override
    public void addSauce() {System.out.println("Adding sauce");}

    @Override
    public void bake() {System.out.println("Baking the pepperoni pizza");}
}

VegetarianPizza есть грибы, тогда как PepperoniPizza есть пепперони. Оба, конечно, нуждаются в соусе и должны быть испечены, который также определен в интерфейсе.

Если addMushrooms() или же addPepperoni() методы были расположены в интерфейсе, оба класса должны были бы реализовать их, даже если им не нужны оба, а только по одному.

Мы должны лишить интерфейсы всех, кроме абсолютно необходимых функций.

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

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

Еще одна важная вещь, которую заявляет DIP:

Абстракции не должны зависеть от деталей. Детали (конкретные реализации) должны зависеть от абстракций.

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

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

Принцип обращения зависимостей а также Инверсия контроля (IoC) используются взаимозаменяемо некоторыми людьми, хотя это технически неверно.

Инверсия зависимостей ведет нас к развязка используя внедрение зависимости через Инверсия Контейнера Контроля, Другое название IoC Containers вполне может быть Контейнеры для инъекций зависимостей, хотя старое имя остается вокруг.

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

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

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

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

Проблема здесь иллюстрируется следующим примером:

IMG

Spaceship а также Airplane расширить абстрактный класс FlyingVehicle, пока Car а также Truck простираться GroundVehicle, У каждого есть свои соответствующие методы, которые имеют смысл для типа транспортного средства, и мы, естественно, сгруппировали бы их вместе с абстракцией, думая о них в этих терминах.

Эта структура наследования основана на мышлении об объектах с точки зрения того, что они находятся вместо того, что они делать,

Проблема в том, что новые требования могут вывести из равновесия всю иерархию. В этом примере, что если ваш начальник влетел и сообщил вам, что клиент хочет летающий автомобиль сейчас? Если вы наследуете от FlyingVehicleвам придется реализовать drive() опять же, несмотря на то, что та же самая функциональность уже существует, тем самым нарушая принцип СУХОГО, и наоборот:

public class FlyingVehicle {
    public void fly() {}
    public void land() {}
}

public class GroundVehicle {
    public void drive() {}
}

public class FlyingCar extends FlyingVehicle {

    @Override
    public void fly() {}

    @Override
    public void land() {}

    public void drive() {}
}

public class FlyingCar2 extends GroundVehicle {

    @Override
    public void drive() {}

    public void fly() {}
    public void land() {}
}

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

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

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

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

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

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

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

В нашем примере транспортного средства мы могли бы просто реализовать интерфейсы Flyable а также Drivable вместо введения абстракции и наследования.

наш Airplane а также Spaceship мог бы реализовать Flyableнаш Car а также Truck мог бы реализовать Drivableи наш новый FlyingCar мог бы реализовать обе,

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

Заключение

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

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


0 Comments

Ваш e-mail не будет опубликован. Обязательные поля помечены *