Объектно-ориентированный подход. Объектно_ориентированный_подход. Объектно ориентированный подход Мэтт Вайсфельд 5е международное издание ббк 32. 973. 2018
Скачать 5.43 Mb.
|
") ; 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 , а с абстрактным |