Технологии программирования
Лекция 5. Абстрактный класс. Анонимный класс. Принципы SOLID.
Содержание
Абстрактный класс
Абстрактный класс в Java является базовым классом, который может быть использован для создания объектов, но не может быть непосредственно инстанцирован. Он используется для обеспечения общей функциональности и структуры для подклассов, которые могут быть реализованы по-разному.
Например, абстрактный класс может представлять собой животное. У всех животных есть общие характеристики, такие как способность двигаться, есть и издавать звуки, но каждое животное также имеет свои уникальные особенности, такие как особенности поведения или внешний вид.
Вот пример абстрактного класса “Animal” и двух его подклассов “Dog” и “Cat”:
import java.util.ArrayList;
import java.util.List;
public abstract class Animal {
private String name;
private List<String> favoriteFoods;
public Animal(String name, List<String> favoriteFoods) {
this.name = name;
this.favoriteFoods = new ArrayList<>(favoriteFoods);
}
public String getName() {
return name;
}
public List<String> getFavoriteFoods() {
return favoriteFoods;
}
public abstract void makeNoise();
}
class Dog extends Animal {
public Dog(String name, List<String> favoriteFoods) {
super(name, favoriteFoods);
}
@Override
public void makeNoise() {
System.out.println(" Гав!");
}
}
class Cat extends Animal {
public Cat(String name, List<String> favoriteFoods) {
super(name, favoriteFoods);
}
@Override
public void makeNoise() {
System.out.println(" Мяу!");
}
}
public class Main {
public static void main(String[] args) {
Dog dog1 = new Dog("Рекс", Arrays.asList("говядина", "кости"));
Cat cat1 = new Cat("Мурка", Arrays.asList("молоко", "рыба"));
dog1.makeNoise(); // Гав!
cat1.makeNoise(); // Мяу!
}
}
Абстрактные классы и интерфейсы оба имеют свою роль в объектно-ориентированном программировании.
Абстрактные классы могут содержать реализацию методов, которые могут быть переопределены в подклассах. Это позволяет контролировать, как базовые методы будут реализованы в производных классах. Кроме того, абстрактные классы могут иметь и чисто абстрактные методы, которые должны быть реализованы в подклассах без предоставления реализации в базовом классе.
Интерфейсы, с другой стороны, определяют набор абстрактных методов, которые должны быть реализованы без предоставления реализации. Они используются для определения общих функциональных возможностей без привязки к конкретной реализации.
Выбор между абстрактным классом и интерфейсом зависит от конкретной задачи. Если требуется общая структура для производных классов с определенными методами, которые должны быть переопределены, то абстрактный класс обычно является лучшим выбором. Если же нужно определить набор методов без их реализации, то интерфейс будет более подходящим.
Анонимный класс
Анонимный класс - это класс, который не имеет имени и определяется прямо внутри выражения. Анонимные классы обычно используются для реализации интерфейсов без необходимости создавать новый именованный класс.
Например, у вас может быть интерфейс, который представляет животное:
interface Animal {
String makeNoise();
}
Вы можете создать анонимный класс для этого интерфейса, чтобы представить конкретное животное:
class Main {
public static void main(String[] args) {
Animal tiger = new Animal() {
@Override
public String makeNoise() {
return "Roar!";
}
};
tiger.makeNoise(); // Roar!
}
}
Ключевое слово final
- Ключевое слово final, применяемое к классу, означает, что класс является окончательным и не может быть унаследован.
- Если ключевое слово final используется для метода, это означает, что метод является окончательным и не может быть переопределен в подклассе.
- Применение final к полю гарантирует, что поле может быть инициализировано только один раз и не может изменяться после инициализации.
Принципы Solid
SOLID - это аббревиатура пяти основных принципов объектно-ориентированного программирования, которые помогают разработчикам создавать более понятные, надежные и тестируемые программные системы.
- Single Responsibility Principle - принцип единственной ответственности, который означает, что каждый модуль или класс должен иметь только одну ответственность. То есть, он должен решать только одну задачу или вызывать только одно изменение в системе.
- Open/Closed Principle - принцип открытости/закрытости, который гласит, что программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации. То есть, вы должны добавлять новую функциональность, не изменяя существующий код, а только расширяя его.
- Liskov Substitution Principle - принцип подстановки Барбары Лисков, который утверждает, что объекты в программе должны быть заменяемы на их подтипы без изменения корректности программы.
- Interface Segregation Principle - принцип разделения интерфейсов, который предполагает, что система должна содержать только такие интерфейсы, которые достаточны для определения желаемого поведения всех клиентов.
- Dependency Inversion Principle - принцип инверсии зависимостей, который говорит о том, что зависимости внутри системы должны быть установлены от абстракций к конкретным реализациям, а не наоборот. Это помогает уменьшить связанность между компонентами системы и улучшает ее модульность.
Single Responsibility Principle
Принцип единственной ответственности (Single Responsibility Principle, SRP) гласит, что каждый компонент системы (класс, модуль, функция) должен иметь одну и только одну ответственность. Это означает, что компонент должен выполнять только один аспект задачи и не должен быть связан с другими аспектами.
class Example {
int sumNumbers(int a, int b) {
System.out.println(a + " " + b);
sendToDb(a, b);
return a + b;
}
}
Здесь нарушается принцип единой ответственности, тк наш метод не только вычисляет сумму чисел, но и отправляет их в базу данных и печатает в консоль
Open/Closed Principle
Принцип открытости/закрытости в программировании гласит, что классы должны быть открыты для добавления новой функциональности и закрыты для изменения существующей. Это означает, что вы можете добавлять новые методы и свойства к классу, не изменяя его исходный код, просто наследуя его и добавляя свою функциональность. Однако, если вам нужно изменить уже существующую функциональность класса, лучше создать новый класс, который наследует старый и изменяет нужные методы. Это позволяет сохранять стабильность кода и избегать ошибок.
Предположим, у нас есть класс “Автомобиль” и мы хотим добавить ему функциональность “поездки по бездорожью”. Мы можем попытаться добавить соответствующий метод в класс “Автомобиль”, однако, вскоре может возникнуть необходимость добавить ещё какие-то специфичные для внедорожника функции, затем ещё, и в итоге мы просто “замусорим” исходный класс “Автомобиль” ненужными функциями, которые будут применимы только к узкому классу машин - внедорожникам. Гораздо правильнее было бы сначала создать подкласс “Внедорожник” наследующий функциональность “Автомобиля”, а затем уже добавлять ему нужные специфические функции.
public class Car{
public void drive() {
System.out.println("driving a car");
}
public static void main(String[] args) {
Car car = new Car();
car.drive();
}
}
public class OffRoadCar extends Car{
@Override
public void drive() {
super.drive();
System.out.println("driving an offroad car");
}
}
Liskov Substitution Principle
Liskov’s Substitution Principle (LSP) требует, чтобы объекты в подтипе могли безопасно использоваться везде, где ожидается тип объекта родительского класса. В Java это обычно означает, что подкласс должен удовлетворять всем требованиям, предъявляемым к его суперклассу.
Рассмотрим два класса - “Круг” и “Эллипс”, которые представляют геометрические фигуры. Класс “Круг” наследует методы и свойства базового класса “Фигура” - метод getArea(), который вычисляет площадь фигуры и метод move(int dx, int dy), который перемещает фигуру на заданное количество пикселей в указанном направлении.
class Figure {
public void move(int dx, int dy) {
System.out.println(“Перемещаю фигуру”);
}
public abstract double getArea();
}
class Circle extends Figure {
@Override
public double getArea() {
return Math.PI * radius * radius;
}
private double radius;
Circle(double radius) {
this.radius = radius;
}
}
// Класс Эллипс расширяет класс Фигура, но не реализует метод getArea(),
// предполагая, что он будет возвращать площадь круга
class Ellipse extends Figure {
Ellipse(double width, double height) {
double radius = Math.min(width, height) / 2;
// Предполагаем, что эллипс - это круг с радиусом, равным половине малой оси
System.out.println(radius);
}
}
В этом примере мы нарушили принцип LSP, так как класс Эллипс не может быть использован вместо класса Круг в контексте метода getArea(). Это связано с тем, что метод getArea() для эллипса должен вычислять площадь по формуле Math.PI * width * height, а не Math.PI * радиус * радиус, как это делает метод для класса Круг.
Таким образом, использование Эллипса вместо Круга в контексте метода getArea() приведет к неожиданным результатам.
Interface Segregation Principle
Interface Segregation Principle (Принцип разделения интерфейсов) гласит, что интерфейс должен содержать только те методы, которые are required (требуются) для использования клиентами. Объединение слишком разнородных функций в одном интерфейсе может сделать его сложным для использования и привести к нарушению других принципов, таких как Single Responsibility Principle.
Пример нарушения ISP на Java может выглядеть следующим образом:
У вас есть класс Bird (Птица) со методом fly() (летать). Теперь вы создаете класс Penguin (Пингвин), который тоже является птицей, но не может летать из-за особенностей своего строения. Если вы предоставите метод fly() в интерфейсе Penguin, то это будет являться нарушением ISP.
public interface Bird {
void fly();
}
public class Penguin implements Bird {
@Override
public void fly() {
System.out.println("A penguin can't fly.");
}
}
Чтобы избежать этого, вы можете создать отдельный интерфейс Flyable (Летающая) с методом fly(), который будет реализован только в тех классах, которые могут летать. Таким образом, интерфейс Penguin не будет содержать метод fly(), а интерфейс Bird будет его содержать.
Dependency Inversion Principle
Зависимости внутри системы строятся на основе абстракций. Модули верхнего уровня не зависят от модулей нижнего уровня. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Например:
public class DependencyException {
// Dependency Inversion Principle Принцип инверсии зависимостей
static class Book {
String name;
Printer printer;
}
static abstract class Printer {
abstract void print(Book book);
}
static class RealPrinter extends Printer {
@Override
void print(Book book) {
System.out.println("print on paper");
}
}
static class ConsolePrinter extends Printer {
void print(Book book) {
System.out.println(book.name);
}
}
}
Класс книга не зависит от реализации RealPrinter и ConsolePrinter, а зависит от абстракции Printer.
Полезные ссылки:
Статья про SOLID - примеры, которые мы рассматривали на лекции частично отсюда