Главная страница
Навигация по странице:

  • 2. OCP: принцип открытости/закрытости

  • 3. LSP: принцип подстановки Лисков

  • 4. ISP: принцип разделения интерфейса

  • 5. DIP: принцип инверсии зависимостей

  • Внедрение конструктора

  • Шаг 1: начальный пример

  • Шаг 2: разделение поведений

  • Объектно-ориентированный подход. Объектно_ориентированный_подход. Объектно ориентированный подход Мэтт Вайсфельд 5е международное издание ббк 32. 973. 2018


    Скачать 5.43 Mb.
    НазваниеОбъектно ориентированный подход Мэтт Вайсфельд 5е международное издание ббк 32. 973. 2018
    АнкорОбъектно-ориентированный подход
    Дата31.03.2023
    Размер5.43 Mb.
    Формат файлаpdf
    Имя файлаОбъектно_ориентированный_подход.pdf
    ТипДокументы
    #1028905
    страница24 из 25
    1   ...   17   18   19   20   21   22   23   24   25
    ") ;
    System.out.printin("Total of all areas = " + areas);
    System.out.printin("") ;
    }
    }
    Теперь с помощью недавно написанного класса мы можем добавить функцио- нальность для вывода в HTML без воздействия на код для вычисления пло- щади:
    public class TestShape {
    public static void main(String args[]) {
    System.out.printin("Hello World!");
    Circle circle = new Circle(1);
    Shape[] shapeArray = new Shape[1];
    shapeArray[0] = circle;
    CalculateAreas ca = new CalculateAreas(shapeArray) ;
    CalculateAreas sum = new CalculateAreas(shapeArray) ;
    OutputAreasoAreas = new OutputAreas(sum.sumAreas() ) ;
    oAreas.console(); // output to console oAreas.HTML() ; // output to HTML
    }
    }
    Суть здесь заключается в том, что теперь можно послать вывод в различных направлениях в зависимости от необходимости. Если нужно добавить возмож- ность другого способа вывода, например JSON, можно привнести ее в класс
    OutputAreas без необходимости внесения изменений в класс
    CalculateAreas
    В результате можно перераспределить класс
    CalculateAreas без какого-либо затрагивания других классов.
    2. OCP: принцип открытости/закрытости
    Принцип открытости/закрытости гласит, что можно расширить поведение класса без внесения изменений.
    Обратим снова внимание на пример с фигурами. В приведенном ниже коде есть класс
    ShapeCalculator
    , который берет объект
    Rectangle
    , рассчитывает площадь этого объекта и возвращает значения. Это простое приложение, но оно работа- ет только с прямоугольниками.

    Глава.12..Принципы.объектно-ориентированного.проектирования
    240
    class Rectangle{
    protected double length;
    protected double width;
    public Rectangle(double 1, double w) {
    length = 1;
    width = w;
    };
    }
    class CalculateAreas {
    private double area;
    public double calcArea(Rectangle r) {
    area = r.length * r.width;
    return area;
    }
    }
    public class OpenClosed {
    public static void main(String args[]) {
    System.out.printin("Hello World");
    Rectangle r = new Rectangle(1,2);
    CalculateAreas ca = new CalculateAreas ();
    System.out.printin("Area = "+ ca.calcArea(r));
    }
    }
    То, что это приложение работает только в случае с прямоугольниками, приводит к ограничению, которое наглядно объясняет принцип открытости/закрытости: если мы хотим добавить класс
    Circle к классу
    CalculateArea
    (изменить то, что он выполняет), нам нужно внести изменения в сам модуль. Очевидно, что это вступает в противоречие с принципом открытости/закрытости, который гласит, что мы не должны вносить изменения в модуль для изменения того, что он вы- полняет.
    Чтобы соответствовать принципу открытости/закрытости, можно вернуться к уже проверенному примеру с фигурами, где создается абстрактный класс
    Shape а непосредственно фигуры наследуют от класса
    Shape
    , у которого есть абстракт- ный метод getArea()
    На данный момент можно добавлять столь много разных классов, сколько тре- буется, без необходимости внесения изменений непосредственно в класс
    Shape
    (например, класс
    Circle
    ). Сейчас можно сказать, что класс
    Shape закрыт.

    241
    Принципы.объектно-ориентированной.разработки.SOLID. .
    Код ниже обеспечивает реализацию решения для прямоугольников и кругов и позволяет создавать неограниченное количество фигур:
    abstract class Shape {
    public abstract double getArea() ;
    }
    class Rectangle extends Shape
    {
    protected double length;
    protected double width;
    public Rectangle(double 1, double w) {
    length = 1;
    width = w;
    };
    public double getArea() {
    return length*width;
    }
    }
    class Circle extends Shape
    {
    protected double radius;
    public Circle(double r) {
    radius = r;
    };
    public double getArea() {
    return radius*radius*3.14;
    }
    }
    class CalculateAreas {
    private double area;
    public double calcArea(Shape s) {
    area = s.getArea();
    return area;
    }
    }
    public class OpenClosed {
    public static void main(String args[]) {
    System.out.printiIn("Hello World") ;
    CalculateAreas ca = new CalculateAreas() ;
    Rectangle r = new Rectangle(1,2);
    System.out.printIn("Area = " + ca.calcArea(r));
    Circle c = new Circle(3);

    Глава.12..Принципы.объектно-ориентированного.проектирования
    242
    System.out.printIn("Area = " + ca.calcArea(c));
    }
    }
    Стоит заметить, что при такой реализации в метод
    CalculateAreas()
    не должны вноситься изменения при создании нового экземпляра класса
    Shape
    Можно масштабировать код, не переживая о существовании предыдущего кода.
    Принцип открытости/закрытости заключается в том, что следует расширять код с помощью подклассов так, чтобы изначальный класс не требовал правок.
    Однако само понятие «расширение» выступает противоречивым в некоторых обсуждениях, касающихся принципов SOLID. Развернуто говоря, если мы от- даем предпочтение композиции, а не наследованию, как это влияет на принцип открытости/закрытости?
    При соответствии одному из принципов SOLID код может удовлетворять кри- териям других принципов SOLID. Например, при проектировании в соответ- ствии с принципом открытости/закрытости код может подходить требованиям принципа единственной ответственности.
    3. LSP: принцип подстановки Лисков
    Согласно принципу подстановки Лисков, проектирование должно предусма- тривать возможность замены любого экземпляра родительского класса экзем- пляром одного из дочерних классов. Если родительский класс может выполнять какую-либо задачу, дочерний класс тоже должен мочь.
    Рассмотрим некоторый код, который на первый взгляд корректен, тем не менее нарушает принцип подстановки Лисков. В коде, приведенном ниже, присут- ствует типовой абстрактный класс
    Shape
    . Класс
    Rectangle
    , в свою очередь, на- следует атрибуты от класса
    Shape и переопределяет его абстрактный метод calcArea()
    . Класс
    Square
    , в свою очередь, наследует от
    Rectangle abstract class Shape{
    protected double area;
    public abstract double calcArea();
    }
    class Rectangle extends Shape{
    private double length;
    private double width;
    public Rectangle(double 1, double w) {
    length = 1;
    width = w;
    }
    public double calcArea() {

    243
    Принципы.объектно-ориентированной.разработки.SOLID. .
    area = length*width;
    return (area) ;
    };
    }
    class Square extends Rectangle{
    public Square(double s) {
    super(s, Ss);
    }
    }
    public class LiskovSubstitution {
    public static void main(String args[]) {
    System.out.printIn("Hello World") ;
    Rectangle r = new Rectangle(1,2);
    System.out.printin("Area = " + r.calcArea());
    Square s = new Square(2) ;
    System.out.printin("Area = " + s.calcArea());
    }
    }
    Пока что все хорошо: прямоугольник является экземпляром фигуры, поэтому ничего не вызывает беспокойства, поскольку квадрат является экземпляром прямоугольника, — и снова все правильно, правда?
    Теперь зададим философский вопрос: а квадрат — это все-таки прямоугольник?
    Многие ответят утвердительно. Хотя и можно допустить, что квадрат — это частный случай прямоугольника, но его свойства будут отличаться. Прямо- угольник является параллелограммом (противоположные стороны одинаковы), как и квадрат. В то же время квадрат еще и является ромбом (все стороны оди- наковы), в то время как прямоугольник — нет. Поэтому различия есть.
    Когда дело доходит до объектно-ориентированного проектирования, проблема не в геометрии. Проблема состоит в том, как именно мы создаем прямоуголь- ники и квадраты. Вот конструктор для класса
    Rectangle
    :
    public Rectangle(double 1, double w) {
    length = 1;
    width = w;
    }
    Очевидно, конструктор требует два параметра. Однако конструктору для клас- са
    Square требуется только один, несмотря даже на то, что родительский класс,
    Rectangle
    , требует два.

    Глава.12..Принципы.объектно-ориентированного.проектирования
    244
    class Square extends Rectangle{
    public Square(double s) {
    super(s, Ss);
    }
    В действительности функционал для вычисления площади немного различен в случае каждого из этих двух классов. То есть класс
    Square как бы имитирует
    Rectangle
    , передавая конструктору один и тот же параметр дважды. Может казаться, что такой обходной прием вполне годится, но на самом деле он может ввести в заблуждение разработчиков, сопровождающих код, что вполне чрева- то подводными камнями при сопровождении в дальнейшем. По меньшей мере это неувязка и, наверное, сомнительное дизайнерское решение. Когда один конструктор вызывает другой, неплохо взять паузу и пересмотреть конструк- цию — возможно, дочерний класс построен ненадлежащим образом.
    Как же найти выход из этой ситуации? Попросту говоря, нельзя осуществить подстановку класса
    Square вместо
    Rectangle
    . Таким образом,
    Square не должен быть дочерним классом
    Rectangle
    . Они должны быть отдельными классами.
    abstract class Shape {
    protected double area;
    public abstract double calcArea();
    }
    class Rectangle extends Shape {
    private double length;
    private double width;
    public Rectangle(double 1, double w) {
    length = 1;
    width = w;
    }
    public double calcArea() {
    area = length*width;
    return (area);
    };
    }
    class Square extends Shape {
    private double side;
    public Square(double s) {
    side = s;
    }
    public double calcArea() {
    area = side*side;
    return (area);
    };

    245
    Принципы.объектно-ориентированной.разработки.SOLID. .
    }
    public class LiskovSubstitution {
    public static void main(String args[]) {
    System.out.printIn("Hello World") ;
    Rectangle r = new Rectangle(1,2);
    System.out.printIn("Area = " + r.calcArea());
    Square s = new Square(2) ;
    System.out.printIn("Area = " + s.calcArea());
    }
    }
    4. ISP: принцип разделения интерфейса
    Принцип разделения интерфейсов гласит о том, что лучше создавать много не- больших интерфейсов, чем несколько больших.
    В этом примере мы создаем единственный интерфейс, который включает в себя несколько поведений для класса
    Mammal
    , а именно eat()
    и makeNoise()
    :
    interface IMammal {
    public void eat();
    public void makeNoise() ;
    }
    class Dog implements IMammal {
    public void eat() {
    System.out.printIn("Dog is eating");
    }
    public void makeNoise() {
    System.out.printIn("Dog is making noise");
    }
    }
    public class MyClass {
    public static void main(String args[]) {
    System.out.printIn("Hello World");
    Dog fido = new Dog();
    fido.eat();
    fido.makeNoise()
    }
    }
    Вместо создания единственного интерфейса для класса
    Mammal нужно создать раздельные интерфейсы для всех поведений:

    Глава.12..Принципы.объектно-ориентированного.проектирования
    246
    interface IEat {
    public void eat();
    }
    interface IMakeNoise {
    public void makeNoise() ;
    }
    class Dog implements IEat, IMakeNoise {
    public void eat() {
    System.out.printIn("Dog is eating");
    }
    public void makeNoise() {
    System.out.printIn("Dog is making noise");
    }
    }
    public class MyClass {
    public static void main(String args[]) {
    System.out.printIn("Hello World") ;
    Dog fido = new Dog();
    fido.eat();
    fido.makeNoise();
    }
    }
    Мы отделяем поведения от класса
    Mammal
    . Получается, что вместо создания единственного класса
    Mammal посредством наследования (точнее, интерфейсов) мы переходим к проектированию, основанному на композиции, подобно стра- тегии, которой придерживались в предыдущей главе.
    В нескольких словах, с таким подходом мы можем создавать экземпляры клас- са
    Mammal с помощью композиции, а не быть вынужденными использовать по- ведения, которые заложены в единственный класс
    Mammal
    . Например, предпо- ложим, что открыто млекопитающее, которое не принимает пищу, а вместо этого поглощает питательные вещества через кожу. Если мы произведем на- следование от класса
    Mammal
    , содержащего поведение eat()
    , для нового млеко- питающего это поведение будет излишним. При этом если все поведения будут заложены в отдельные одиночные интерфейсы, получится построить класс каждого млекопитающего в точности так, как задумано.
    5. DIP: принцип инверсии зависимостей
    Принцип инверсии зависимостей предполагает, что код должен зависеть от абстрактных классов. Часто может казаться, что термины «инверсия зависимо- стей» и «внедрение зависимостей» взаимозаменяемы, однако это ключевые термины, которые нужно ясно понимать при обсуждении этого принципа. Сей- час постараемся их объяснить:

    247
    Принципы.объектно-ориентированной.разработки.SOLID. .
    ‰
    Инверсия зависимости — принцип инвертирования зависимостей.
    ‰
    Внедрение зависимостей — акт инвертирования зависимостей.
    ‰
    Внедрение конструктора — осуществление внедрения зависимостей с по- мощью конструктора.
    ‰
    Внедрение параметра — выполнение внедрения зависимостей через параметр метода, например сеттера.
    Цель инверсии зависимостей в том, чтобы зависимость была с чем-то абстракт- ным, а не конкретным.
    Хотя в какой-то момент, очевидно, придется создать что-то конкретное, мы по- стараемся создать конкретный объект (используя ключевое слово new
    ) вверх по цепочке как можно дальше, как, например, в методе main()
    . Пожалуй, чтобы обдумать все это, лучше вернуться к главе 8 «Фреймворки и повторное исполь- зование: проектирование с применением интерфейсов и абстрактных классов», где обсуждается загрузка классов во время выполнения, а также к главе 9 «Соз- дание объектов и объектно-ориентированное проектирование», где речь идет о снижении связанности и создании небольших классов с ограниченными от- ветственностями.
    Одной из целей принципа инверсии зависимостей является выбор объектов во время выполнения, а не во время компиляции. (Можно изменить поведение программы во время выполнения). Можно даже писать новые классы без на- добности перекомпиляции уже существующих (собственно, можно писать новые классы и внедрять их).
    Много оснований для споров приведено в главе 11 «Избегание зависимостей и тесно связанных классов». Рекомендуем опираться на нее по мере рассмотре- ния принципа инверсии зависимостей.
    Шаг 1: начальный пример
    В этом примере мы в очередной раз вернемся к одному из классических при- меров в объектно-ориентированном проектировании, сопровождавшему нас по всей книге, — классу
    Mammal
    , наряду с классами
    Dog и
    Cat
    , которые от него на- следуют. Класс
    Mammal абстрактен и содержит лишь метод makeNoise()
    abstract class Mammal
    {
    public abstract String makeNoise();
    }
    Подклассы, например
    Cat
    , используют наследование для заимствования пове- дения класса
    Mammal
    , makeNoise()
    :

    Глава.12..Принципы.объектно-ориентированного.проектирования
    248
    class Cat extends Mammal
    {
    public String makeNoise()
    {
    return "Meow";
    }
    }
    Затем основное приложение создает экземпляр объекта и вызывает метод makeNoise()
    :
    Mammal cat = new Cat();;
    System.out.printin("Cat says " + cat.makeNoise());
    Полное приложение для первого шага представлено в следующем коде:
    public class TestMammal {
    public static void main(String args[]) {
    System.out.print]n("Hello World\n") ;
    Mammal cat = new Cat();;
    Mammal dog = new Dog();
    System.out.printIn("Cat says " + cat.makeNoise());
    System.out.printIn("Dog says " + dog.makeNoise());
    }
    }
    abstract class Mammal
    {
    public abstract String makeNoise();
    }
    class Cat extends Mammal
    {
    public String makeNoise()
    {
    return "Meow" ;
    }
    }
    class Dog extends Mammal
    {
    public String makeNoise()
    {
    return "Bark";
    }
    }
    Шаг 2: разделение поведений
    У кода, приведенного выше, есть один потенциально серьезный недостаток: он связывает классы млекопитающих и поведения (
    MakingNoise
    ). В отделении по-

    249
    Принципы.объектно-ориентированной.разработки.SOLID. .
    ведений млекопитающих от самих классов млекопитающих может заключаться значительное преимущество. Поэтому мы создаем класс
    MakingNoise
    , который могут использовать как млекопитающие, так и не млекопитающие.
    При такой модели классы
    Cat
    ,
    Dog и
    Bird могут расширить класс
    MakeNoise и создать свое «звуковое» поведение в зависимости от своих потребностей, на- пример, как в следующем фрагменте кода класса
    Cat
    :
    abstract class MakingNoise
    {
    public abstract String makeNoise() ;
    }
    class CatNoise extends MakingNoise
    {
    public String makeNoise()
    {
    return "Meow";
    }
    }
    При разделении поведения
    MakingNoise и класса
    Cat можно применить класс
    CatNoise вместо нагромождения кода в самом классе
    Cat
    , как показано в следу- ющем фрагменте кода:
    abstract class Mammal
    {
    public abstract String makeNoise();
    }
    class Cat extends Mammal
    {
    CatNoise behavior = new CatNoise();
    public String makeNoise()
    {
    return behavior.makeNoise() ;
    }
    }
    Далее приведено полное приложение для второго шага:
    public class TestMammal {
    public static void main(String args[]) {
    System.out.printIn("Hello World\n") ;
    Mammal cat = new Cat();;
    Mammal dog = new Dog();
    System.out.printIn("Cat says " + cat.makeNoise());
    System.out.printIn("Dog says " + dog.makeNoise());
    }

    Глава.12..Принципы.объектно-ориентированного.проектирования
    250
    }
    abstract class MakingNoise
    {
    public abstract String makeNoise() ;
    }
    class CatNoise extends MakingNoise
    {
    public String makeNoise()
    {
    return "Meow";
    }
    }
    class DogNoise extends MakingNoise
    {
    public String makeNoise()
    {
    return "Bark";
    }
    }
    abstract class Mammal
    {
    public abstract String makeNoise() ;
    }
    class Cat extends Mammal
    {
    CatNoise behavior = new CatNoise();
    public String makeNoise()
    {
    return behavior.makeNoise() ;
    }
    }
    class Dog extends Mammal
    {
    DogNoise behavior = new DogNoise();
    public String makeNoise()
    {
    return behavior.makeNoise() ;
    }
    }
    Проблема заключается в том, что хотя мы и провели отделение главной части кода, мы до сих пор не достигли нашей цели — инверсии зависимостей, посколь- ку класс
    Cat до сих пор создает экземпляр поведения издания звуков классом
    Cat
    CatNoise behavior = new CatNoise();
    Класс
    Cat связан с низкоуровневым модулем
    CatNoise
    . Другими словами, нуж- но допускать связывание класса
    Cat не с классом
    CatNoise
    , а с абстрактным

    1   ...   17   18   19   20   21   22   23   24   25


    написать администратору сайта