Java → Как писать в стиле ООП?

В этой статье кратко расскажу как нужно писать в стиле ООП, зачем он нужен и в чем его особенность. 

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

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


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

 

Абстракция позволяют работать с объектами как с цельными вещами, не обращая внимания на их внутреннее устройство.  Например: 1) производитель компьютеров работает с частями компьютера. Ему не важно как он внутри устроен, из чего состоит. А важно как он работает. Для него процессор это цельный объект, который решает его определенную задачу. 2) Для пользователей не важен как устроен компьютер, из каких деталей он состоит, как они работают. Им важно как работает компьютер, потому что они работают с компьютером как с целым, неотделимым объектом. Тут два уровня абстракции. 

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

Ой, как много слоов. Щас будет код с примерами. 

Основные правила ООП:

1. Каждый класс должен решать только одну задачу и ничего больше.
2. Каждый метод класса должен выполнять только одну цельную операцию и ничего больше.
3. Название классов и методов должны говорить о том, для чего этот класс или метод или что они делают. Т.е. прочитал название и сразу понял что он делает. Названию классов должны быть в Существительной части речи (сущность), в название методов Глаголом (действие).
4. Если кода слишком много в одном классе или методе, значит этот класс или метод берет на себя слишком много ответственности, следует его разбить на части.  

Пример:

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

Мы знаем что в языках программирования нет типа данных, который умеет хранить числа записанные в виде обыкновенных дробей. Мы можем сохранить их только как десятичная дробь типа float в виде 0.343, но это не то что нам нужно. Поэтому, нам нужно создать свой тип данных, в котором мы будем хранить обыкновенные дроби. И назовем этот класс Fraction (Дробь). Так, как дробь состоит из двух составляющих: знаменатель и числитель, у класс будут два свойства целого типа.

public class Fraction {  //Дробь    
    public int denominator;  //Знаменатель
    public int numerator;  //Числитель
}

Теперь, чтобы создать Дробь 3/4 в программе, где мы будем использовать дроби, нам нужно написать:

Fraction frac1 = new Fraction();
frac1.numerator = 3;
frac1.denominator = 4;

Теперь у нас есть объект Fraction(Дробь), которая в себе хранит знаменатель(4) и числитель(3). Чтобы не писать три строчки кода для создания объекта, мы можем знаменатель и числитель указывать в конструкторе класса. Для этого нам нужно создать конструктор, который принимает два параметра:

public class Fraction {   //Дробь
    public int denominator;  //Знаменатель
    public int numerator;  //Числитель
    
    public Fraction(int numerator, int denominator){
        this.denominator = denominator;
        this.numerator = numerator;
    }
}

Теперь, создание дроби занимает всего одну команду:

Fraction frac1 = new Fraction(3, 4);

Если, мы напечатаем этот объект в System.out, то увидим что-то подобное:

Fraction@60e53b93

Файл FractionTest.java, использованный для печати:

public class FractionTest {
    public static void main(String[] args) {
        Fraction frac1 = new Fraction(3, 4);
        System.out.println(frac1);
    }
}

Java, при печати или при конвертации в String, вызывает метод toString() у любого объекта. Этот метод определен в классе Object, который является прародителем для всех классов. Поэтому метода toString() по умолчанию есть у всех классов, но он печатает название класса и адрес объекта в памяти. 

Если мы хотим чтобы наши объекты печатались на экран понятным образом, мы должны переопределить метод toString() в нашем классе:

public class Fraction {    //Дробь
    public int denominator;  //Знаменатель
    public int numerator;  //Числитель

    public Fraction(int numerator, int denominator){
        this.denominator = denominator;
        this.numerator = numerator;
    }

    public String toString(){
        return numerator + "/" + denominator;
    }
}

Теперь при печати наших объектов:

Fraction frac1 = new Fraction(3, 4);
System.out.println(frac1);

мы будем видеть понятную запись:

3/4

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

Начнем с операции суммирования. Чтобы просуммировать два дробных числа, нужно привести их к общему знаменателю. Общий знаменатель это Наименьшее общее кратное(НОК) двух знаменателей. А для нахождения НОК, нужно найди Наибольший общий делитель(НОД). В нашем случае не имеет значения как они находятся. Просто добавим два метода в наш класс Fraction:

private int nod(int a,int b){
    return b == 0 ? a : nod(b,a % b);
}

private int nok(int a,int b){
    return a / nod(a,b) * b;
}

Эти методы закрыты(private) потому что они будут использованы только внутри нашего класса. Теперь напишем сам метод суммирования двух чисел:

public Fraction add(Fraction other){
    Fraction result = new Fraction(1, 1);
    return result;
}

Этот метод будет называться add() - потому что добавляет одной дроби другую. Принимает один параметр типа Fraction - это дробь, которая добавляется в текущую дробь. Метод возвращает тип Fraction, потому что результат суммирования двух дробей тоже дробь. Пока метод создает единичную дробь 1/1 и  возвращает ее. 

Если у двух дробей одинаковый знаменатель, то просто суммируем их числители и получаем результирующую дробь. Реализуем этот случай сначала.  Меняем метод add():

public Fraction add(Fraction other){
    Fraction result = new Fraction(1, 1);
    if(this.denominator == other.denominator){ //если знаменатели одинаковые
        result.denominator = this.denominator;
        result.numerator = this.numerator + other.numerator;
    }
    return result;
}

Помните, что this - это ссылка на текущий объект? А параметр other в данном случае второй объект. 

Теперь, попробуем просуммировать две дроби с одинаковым знеменателем:

public class FractionTest {
    public static void main(String[] args) {
        Fraction frac1 = new Fraction(1, 5);  // 1/5
        Fraction frac2 = new Fraction(3, 5);  // 3/5
        Fraction result = frac1.add(frac2);
        System.out.println(result);      // Напечатает: 4/5
    }
}

И получим на экране:

4/5

Теперь, реализуем случай суммирования с неодинаковыми знаменателями. Изменим метод add() так:

public Fraction add(Fraction other) {
    Fraction result = new Fraction(1, 1);
    if (this.denominator == other.denominator) { //если знаменатели одинаковые
        result.denominator = this.denominator;
        result.numerator = this.numerator + other.numerator;
    }else{
        int nok = nok(this.denominator, other.denominator);
        result.denominator = nok;
        result.numerator = this.numerator * (nok/this.denominator) + other.numerator * (nok/other.denominator);
    }
    return result;
}

Теперь, попробуем просуммировать две дроби с разными знаменателями:

public class FractionTest {
    public static void main(String[] args) {
        Fraction frac1 = new Fraction(3, 15);  // 3/15
        Fraction frac2 = new Fraction(4, 18);  // 4/18
        Fraction result = frac1.add(frac2);
        System.out.println(result);     //  38/90
    }
}

Напечатает:

38/90

тоже самое математически:

Теперь добавим метод умножения дробей в класс Fraction:

public Fraction multiply(Fraction other) {
    Fraction result = new Fraction(1, 1);
    result.denominator = this.denominator * other.denominator;
    result.numerator = this.numerator * other.numerator;
    return result;
}

и попробуем умножить две дроби:

public class FractionTest {
    public static void main(String[] args) {
        Fraction frac1 = new Fraction(3, 4);   // 3/4
        Fraction frac2 = new Fraction(1, 5);   // 1/5
        Fraction result = frac1.multiply(frac2);
        System.out.println(result);         // 3/20
    }
}

Напечатает:

3/20

Результат 1.

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

Другой вариант:

Мы также можем сделать немного по другому: Выделить все операции над дробями в отдельный класс, чтобы наш класс Fraction только хранил дробь, а другой класс умел выполнять операции над ними. Назовем второй класс FractionCalculator. Теперь, чтобы суммировать две дроби нам нужно сначала создать объект калькулятора дробей, потом передавать ему дроби, чтобы он выполнял операции над ними . Этот вариант можно посмотреть здесь

Ввод/Вывод данных

Если вы внимательно читали, то должны заметить что внутри класса Fraction не было ввода и вывода данных. Потому, что ни класс Fraction, ни FractionCalculator не должны заниматься вводом и выводом данных. Пусть этим занимается другой класс (Ввод и вывод в Java). А наши классы будут выполнять только то, что требуется от них: хранить дроби и выполнять над ними арифметические операции. 

Надеюсь пример был понятным. Пишите вопросы в чате или в телеграм. Скоро добавлю комментарии. 

2227 10
Alisher Alikulov