面向对象设计原则(OOD)
为什么需要设计原则
面向对象设计原则是前人从面向对象工程实践中总结出的一系列指导原则,对于我们开发有很强的指导性。能够帮助我们构建高可用性、高可修改性、高可扩展性的系统。
面向对象设计原则同时也是理解设计模式的基础。
设计原则与设计模式的关系
对于设计模式而言,设计原则是更高层次得指导思想,设计模式是基于这些原则提炼出得具体的实现方法。
所以要理解设计模式,就要先理解设计原则。
对于设计原则&设计模式的个人看法
现在互联网上存在很多观点
- 真实工作中设计模式根本就用不到
- 设计模式搞的项目复杂度很高,降低了可读性和可修改性
- 手上的项目深受设计模式荼毒
针对以上观点,笔者发表个人观点(主观)
设计原则是必须要了解的,设计原则能理的清晰且运用其实设计模式在会逐步的体现出来。
很多时候,我们来直接学习设计模式,会感觉很晦涩。因为我们缺少场景缺少案列。
关于复杂度的问题,过度设计确实会引入很高的复杂度,笔者认为这是一个经验性的问题。同时还有一个值得思考的点,若摒弃设计模式复杂度高的项目会变得简单吗?
关于项目中引入或者使用设计模式等内容的问题,是一个值得思考的点。
KISS原则(Keep It Simple, Stupid)是一种设计哲学,强调在设计过程中保持简洁性,避免不必要的复杂性。
KISS原则的全称是“Keep It Simple, Stupid”,字面意思是“保持简单,愚蠢”。虽然“愚蠢”在这里并不是字面上的愚蠢,而是指不考虑复杂性和不必要的细节,使设计或代码尽可能简单易懂。这个原则由14世纪英格兰的逻辑学家奥卡姆的威廉提出,被称为“奥卡姆剃刀原理”,即“如无必要,勿增实体”。
七种设计原则
开闭原则(The Open-Closed Principle ,OCP)
基本概念:软件实体应当对扩展开放,对修改关闭。开闭原则是面向对象设计原则的核心。
开闭原则是面向对象设计原则的核心,所有在后面的设计原则中可能会发现好像有开闭原则的影子,这个是正常的。
在笔者看来,剩下的几个原则更像是对开闭原则的补充。
展开解释开闭原则中的开闭:
-
对扩展开放:有新的需求或变化时,可以对现有的代码进行扩展,以适应新的变化。
-
对修改封闭:模块一旦设计完成,就可以完成其工作,当有新的变化时,不需要对已有代码进行修改。
反面教材
图形编辑器GraphicEditor中draw()方法正常情况下在开发完成后就不需要再次修改了。但是如果后面需要添加一个新的形状,如三角形,则需要对GraphicEditor中的draw方法进行修改,这明显违背了开闭原则。
package com.yiwyn.ocp;
public class OCPBadDemo {
public static void main(String[] args) {
GraphicEditor editor = new GraphicEditor();
// 画一个圆形
editor.draw("Circle");
}
// 图形编辑类
static class GraphicEditor {
// 画图方法
public void draw(String graphicType) {
if (graphicType.equals("Circle")) {
final OCPGoodDemo.Circle circle = new OCPGoodDemo.Circle();
circle.draw();
} else if (graphicType.equals("Rectangle")) {
final OCPGoodDemo.Rectangle rectangle = new OCPGoodDemo.Rectangle();
rectangle.draw();
}
}
}
// 圆形
static class Circle {
public void draw() {
System.out.println("画一个圆形");
}
}
// 矩形
static class Rectangle {
public void draw() {
System.out.println("画一个矩形");
}
}
}
改进方案
我们可以看到在改进方案中,使用了接口(Graphic)的形式对图形进行了统一,在画图中使用接口做为入参,而画图方法graphicEditor.draw() 无论后面添加什么类型的新图形,都无需修改,仅需要对Graphic接口进行新的实现即可。
package com.yiwyn.ocp;
public class OCPGoodDemo {
public static void main(String[] args) {
GraphicEditor editor = new GraphicEditor();
// 画一个圆形
editor.draw(new Circle());
}
// 图形编辑类
static class GraphicEditor {
// 画图方法
public void draw(Graphic graphic) {
graphic.draw();
}
}
// 图形接口
interface Graphic {
void draw();
}
// 圆形
static class Circle implements Graphic {
@Override
public void draw() {
System.out.println("画一个圆形");
}
}
// 矩形
static class Rectangle implements Graphic {
@Override
public void draw() {
System.out.println("画一个矩形");
}
}
}
依赖倒置原则(Dependency Inversion Principle ,DIP)
基本概念:要面向接口编程,不要面向实现编程。依赖倒置原则是实现开闭原则的重要途径之一
开闭原则强调扩展,依赖倒置原则强调模块之间的解耦。
开闭原则要求通过扩展来支持新功能,而不修改现有代码。
依赖倒置原则有通过依赖抽象,使得底层可以独立变化,而不影响高层模块
内容
- 高层不应该依赖低层,两者应该基于抽象
- 抽象不应该依赖细节,细节依赖抽象
反面教材
若pay行为有修改,则需要修改PayService,需要引入新的支付和对pay方法进行改造,违反了依赖倒置原则,高层直接调用到了底层实现。(也违反了开闭原则)
package com.yiwyn.dip;
public class DIPBadDemo {
public static void main(String[] args) {
PayService payService = new PayService();
payService.pay("AliPay");
}
/**
* 支付服务-高层
*/
static class PayService {
private final AliPay aliPay = new AliPay();
private final WxPay wxPay = new WxPay();
/**
* 这里payService层直接调用了底层逻辑,若底层修改则Service层也需要修改
*/
public void pay(String payChannel) {
if (payChannel.equals("AliPay")) {
aliPay.pay();
} else if (payChannel.equals("WxApy")) {
wxPay.pay();
}
}
}
/**
* 阿里支付-低层
*/
static class AliPay {
public void pay() {
System.out.println("阿里支付");
}
}
/**
* 微信支付-低层
*/
static class WxPay {
public void pay() {
System.out.println("微信支付");
}
}
}
改进方案
通过将Pay行为抽象,添加IPay接口,将service层和支付实现层进行隔离。同时引入AppConfig类,在改进后的方案中,业务真实调用的pay本质上由config配置读取实现,无论Pay实现进行如何修改,都不会影响到Service、DIPGoodDemo中的代码。
package com.yiwyn.ood.dip;
import java.util.HashMap;
import java.util.Map;
public class DIPGoodDemo {
public static void main(String[] args) {
PayService payService = new PayService();
payService.pay();
}
/**
* 支付服务-高层
*/
static class PayService {
/**
* 这里payService层直接调用了底层逻辑,若逻辑
*/
public void pay() {
IPay pay = AppConfig.getPay();
pay.pay();
}
}
/**
* 模拟系统配置
*/
static class AppConfig {
// 读取系统配置
private static final String payChannel = System.getProperty("payChannel", "AliPay");
// 支付信息实现
private static final Map<String, IPay> payMap = new HashMap<>();
static {
payMap.put("AliPay", new AliIPay());
payMap.put("WxPay", new WxIPay());
}
public static IPay getPay() {
return payMap.get(payChannel);
}
}
/**
* 支付接口
*/
interface IPay {
void pay();
}
/**
* 阿里支付-低层
*/
static class AliIPay implements IPay {
@Override
public void pay() {
System.out.println("阿里支付");
}
}
/**
* 微信支付-低层
*/
static class WxIPay implements IPay {
@Override
public void pay() {
System.out.println("微信支付");
}
}
}
单一职责原则(Single Responsibility Principle,SRP)
基本概念:顾名思义,一个对象(模块)只承担一项职责
优点:
-
降低类的复杂度,一个类仅承担一项责任,可以有效的提高内聚。
-
提高类的可读性。
-
提高类的可维护性
反面教材
代码中将用户相关职能和邮件服务职能进行了混合,将来如果涉及到邮箱相关逻辑改造,需要对UserService服务进行同步改造
class UserService {
public void register(RegisterInfo info) {
// 注册...
}
public void sendVerficationCode(String email) {
// 发送验证码逻辑
}
}
改进方案
为了保证UserService的职责单一,将邮件验证码发送逻辑抽成邮箱服务,UsreService调用邮箱服务。
// 用户服务
class UserService {
EmailService emailService = new EmailService();
public void register(registerInfo info) {
// 注册...
}
public void sendVerficationCode(String email) {
// 发送验证码
emailService.sendEmail(email);
}
}
// 邮箱服务
class EmailService extends UserService {
public void sendEmail(String email) {
// 发送验证码
}
}
里氏替换原则(Liskov Substitution Principle ,LSP)
基本概念:子类应该能够替换其父类,并且不会影响程序的正确性。子类必须遵循父类的行为约定(功能)
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法**重载(overload)**父类的方法时,方法的入参要父类的方法更宽松
- 当子类方法实现父类的方法时,方法的反参要比父类更加严格和相等。
为什么遵守里氏替换原则
- 继承关系给程序带来侵入性
- 保证程序升级后的兼容性
- 避免程序出错
反面教材
这个反面教材可能大家感觉没什么问题,SonCalculator修改了计算方法似乎问题不大,但是如果调用方这里在使用Calculator时,其实使用的是子类,但是却认为是父类的加法时,是否出现了大问题呢?如果按照开闭原则等来看待这个问题,这个时候calc方法已经失去了其原本的含义。
package com.yiwyn.ood.lsp;
public class LSPBadDemo {
public static void main(String[] args) {
Calculator calculator = new Calculator();
// 这里期望的加法
int calc = calculator.calc(1, 2);
System.out.println(calc); // 3
Calculator sonCalc = new SonCalculator();
int calc1 = sonCalc.calc(1, 2);
System.out.println(calc1); // -1
}
// 计算类
static class Calculator {
// 默认加法
public int calc(int a, int b) {
return a + b;
}
}
static class SonCalculator extends Calculator {
@Override
public int calc(int a, int b) {
return a - b;
}
}
}
改进方案
可以在子类中添加方法,保持父类方法的逻辑不变动,这样无论子类在任何位置被调用,都不会对调用者产生误导
package com.yiwyn.ood.lsp;
public class LSPGoodDemo {
public static void main(String[] args) {
LSPBadDemo.Calculator calculator = new LSPBadDemo.Calculator();
// 这里期望的加法
int calc = calculator.calc(1, 2);
System.out.println(calc); // 3
SonCalculator sonCalc = new SonCalculator();
int calc1 = sonCalc.calc(1, 2);
System.out.println(calc1); // 3
int i = sonCalc.calcSub(1, 2);
System.out.println(i); // -1
}
// 计算类
static class Calculator {
// 默认加法
public int calc(int a, int b) {
return a + b;
}
}
static class SonCalculator extends Calculator {
// 子类添加方法,不修改父类方法
public int calcSub(int a, int b) {
return a - b;
}
}
}
迪米特原则(最少知识原则)(Law of Demeter ,LoD)
基本概念:一个软件实体应当尽可能少地与其他实体发生相互作用,一个类对于其他类知道的越少越好
优点
- 降低耦合性,提高模块功能的独立性
// 书类
class Book {
private String title;
private double price;
Book(String title, double price) {
this.title = title;
this.price = price;
}
String getTitle() {
return title;
}
double getPrice() {
return price;
}
}
// 购物车类
class ShoppingCart {
private List<Book> books;
ShoppingCart() {
this.books = new ArrayList<>();
}
void addBook(Book book) {
books.add(book);
}
double calculateTotalPrice() {
double total = 0;
for (Book book : books) {
total += book.getPrice();
}
return total;
}
void displayCart() {
System.out.println("Books in the shopping cart:");
for (Book book : books) {
System.out.println("- " + book.getTitle() + " ($" + book.getPrice() + ")");
}
System.out.println("Total price: $" + calculateTotalPrice());
}
}
// 书店类
class BookStore {
private List<Book> availableBooks;
BookStore() {
// 初始化一些书
this.availableBooks = new ArrayList<>();
availableBooks.add(new Book("The Great Gatsby", 10.99));
availableBooks.add(new Book("1984", 8.99));
availableBooks.add(new Book("To Kill a Mockingbird", 12.99));
}
void displayAvailableBooks() {
System.out.println("Available books in the store:");
for (Book book : availableBooks) {
System.out.println("- " + book.getTitle() + " ($" + book.getPrice() + ")");
}
}
void addBookToCart(ShoppingCart cart, String title) {
for (Book book : availableBooks) {
if (book.getTitle().equals(title)) {
cart.addBook(book);
System.out.println("Added to cart: " + book.getTitle());
return;
}
}
System.out.println("Book not found: " + title);
}
}
// 主程序
public class Main {
public static void main(String[] args) {
// 创建书店和购物车
BookStore store = new BookStore();
ShoppingCart cart = new ShoppingCart();
// 显示书店中的书
store.displayAvailableBooks();
// 用户将书添加到购物车
store.addBookToCart(cart, "1984");
store.addBookToCart(cart, "The Great Gatsby");
// 显示购物车内容
cart.displayCart();
}
}
接口分隔原则(Interface Segregation Principle ,ISP)
基本概念:要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用
反面教材
Bird接口包含了太多行为,导致Sparrow和Ostrich不得不实现不相关的方法。
package com.yiwyn.ood.isp;
public class ISPBadDemo {
public static void main(String[] args) {
// 麻雀
Bird sparrow = new Sparrow();
sparrow.run();
// 鸵鸟
Bird ostrich = new Ostrich();
ostrich.fly();
}
/**
* 鸟类接口
*/
interface Bird {
// 飞
void fly();
// 跑
void run();
// 吃
void eat();
}
/**
* 麻雀
*/
static class Sparrow implements Bird {
@Override
public void fly() {
System.out.println("I'm flying");
}
@Override
public void run() {
throw new RuntimeException("I'm cannot run");
}
@Override
public void eat() {
System.out.println("I'm eating");
}
}
/**
* 鸵鸟
*/
static class Ostrich implements Bird {
@Override
public void fly() {
throw new RuntimeException("I'm cannot flying");
}
@Override
public void run() {
System.out.println("I'm run");
}
@Override
public void eat() {
System.out.println("I'm eating");
}
}
}
改进方案
将Bird接口拆分为多个专用接口,每个类只需实现相关的方法。
package com.yiwyn.ood.isp;
public class ISPBadDemo {
public static void main(String[] args) {
// 麻雀
Fly sparrow = new Sparrow();
sparrow.fly();
// 鸵鸟
Run ostrich = new Ostrich();
ostrich.run();
}
/**
* 鸟类接口
*/
interface Bird {
// 吃
void eat();
}
/**
* 会飞的
*/
interface Fly {
// 飞
void fly();
}
/**
* 会跑的
*/
interface Run {
// 跑
void run();
}
/**
* 麻雀
*/
static class Sparrow implements Bird, Fly {
@Override
public void fly() {
System.out.println("I'm flying");
}
@Override
public void eat() {
System.out.println("I'm eating");
}
}
static class Ostrich implements Bird, Run {
@Override
public void run() {
System.out.println("I'm run");
}
@Override
public void eat() {
System.out.println("I'm eating");
}
}
}
组合/聚合复用原则(Composite/Aggregate Reuse Principle ,CARP)
基本概念:尽量使用对象组合,而不是继承来达到复用的目的。在实际开发中,优先使用组合和聚合,避免过度使用继承,以降低代码的耦合性。
-
继承:表示类与类之间的“是一个”的关系(is-a)。子类继承父类的所有特性和行为,是一种强耦合关系。
// 父类 class Animal { void eat() { System.out.println("Animal is eating"); } } // 子类 class Dog extends Animal { void bark() { System.out.println("Dog is barking"); } } // 使用 Dog dog = new Dog(); dog.eat(); // 继承自 Animal dog.bark(); // Dog 自己的方法 -
组合:表示类与类之间的“有一个”的关系(has-a)。一个类通过包含其他类的实例来实现功能,是一种松耦合关系。
// 引擎类 class Engine { void start() { System.out.println("Engine started"); } } // 汽车类 class Car { private Engine engine; // Car 拥有一个 Engine Car() { this.engine = new Engine(); // Engine 的生命周期由 Car 管理 } void start() { engine.start(); System.out.println("Car started"); } } // 使用 Car car = new Car(); car.start(); -
聚合:表示类与类之间的“整体-部分”的关系(whole-part)。类似于组合,但更加松散。
// 书类 class Book { String title; Book(String title) { this.title = title; } void display() { System.out.println("Book: " + title); } } // 图书馆类 class Library { private List<Book> books; // 图书馆包含多本书 Library(List<Book> books) { this.books = books; // 书可以独立于图书馆存在 } void displayBooks() { System.out.println("Books in the library:"); for (Book book : books) { book.display(); } } } // 使用 public class Main { public static void main(String[] args) { // 创建几本书 Book book1 = new Book("The Great Gatsby"); Book book2 = new Book("1984"); Book book3 = new Book("To Kill a Mockingbird"); // 将书添加到列表中 List<Book> books = new ArrayList<>(); books.add(book1); books.add(book2); books.add(book3); // 创建图书馆 Library library = new Library(books); // 显示图书馆中的书 library.displayBooks(); // 书可以独立存在 System.out.println("\nA book outside the library:"); book1.display(); } }
反面教材
我们为各种颜色能源性质的车分别实现了不同的子类,会导致Car的变量非常多,且每个子类的复用性非常差
package com.yiwyn.ood.capr;
public class CAPRBadDemo {
public static void main(String[] args) {
Car car = new BlueElectronicCar();
car.carInfo();
}
static class Car {
public void carInfo() {
System.out.println("一个车");
}
}
static class RedCar extends Car {
@Override
public void carInfo() {
System.out.println("一个红色的车");
}
}
static class RedElectronicCar extends Car {
@Override
public void carInfo() {
System.out.println("一个红色的电车");
}
}
static class BlueElectronicCar extends Car {
@Override
public void carInfo() {
System.out.println("一个蓝色的电车");
}
}
}
改进方案
使用组合的形式对继承进行改进,由继承实现的差异,使用新的成员变量来进行控制,其中Color和Energy在构建不同组合时可以大幅度的复用
package com.yiwyn.ood.capr;
public class CAPRGoodDemo {
public static void main(String[] args) {
Car car = new Car(new RedColor(), new EleEnergy());
car.carInfo();
}
/**
* 车辆类
*/
static class Car {
private final Color color;
private final Energy energy;
public Car(Color color, Energy energy) {
this.color = color;
this.energy = energy;
}
public void carInfo() {
System.out.println("一个" + color.color() + energy.energy() + "车");
}
}
// 颜色信息
interface Color {
String color();
}
// 能源信息
interface Energy {
String energy();
}
// 电动
static class EleEnergy implements Energy {
@Override
public String energy() {
return "Ele";
}
}
// 红色
static class RedColor implements Color {
@Override
public String color() {
return "red";
}
}
}
感谢观看 !