一.面向对象设计原则
在面向对象编程时,以面向对象的设计原则为指导,开发可扩展性和维护性高的程序,多个原则需要适当取舍,达到业务与技术的一个平衡.
二.具体原则
1.开闭原则
(1).定义:对修改关闭,对扩展开放
- 作用: 用抽象构建框架,用实现扩展细节
- 优点:提高软件系统的可复用性和可维护性(可扩展性)
- 实例:
以商城为例:
/**
* 商品接口
* @author shixinke
*/
public interface Product {
/**
* 获取商品ID
* @return
*/
Long getProductId();
/**
* 获取商品名称
* @return
*/
String getName();
/**
* 价格
* @return
*/
Double getPrice();
}
/**
* 书籍类商品
* @author shixinke
*/
public class Book implements Product {
/**
* 商品ID
*/
private Long productId;
/**
* 商品名称
*/
private String name;
/**
* 商品价格
*/
private Double price;
/**
* ISBN号
*/
private String isbn;
public Long getProductId() {
return productId;
}
public String getName() {
return name;
}
public Double getPrice() {
return price;
}
}
有这样一个需求:在双11或618等有活动,需要对商品进行打折出售,那么如何去获取这个价格呢?
第一种方式:修改Book类中的getPrice方法
/**
* 书籍类商品
* @author shixinke
*/
public class Book implements Product {
/**
* 商品ID
*/
private Long productId;
/**
* 商品名称
*/
private String name;
/**
* 商品价格
*/
private Double price;
/**
* ISBN号
*/
private String isbn;
public Book(Long productId, String name, Double price, String isbn) {
this.productId = productId;
this.name = name;
this.price = price;
this.isbn = isbn;
}
public Long getProductId() {
return productId;
}
public String getName() {
return name;
}
/**
* 比如打9折(修改获取价格的方法)
* @return
*/
public Double getPrice() {
return 0.9 * price;
}
}
第二种方式:新建一个子类继承Book类,重写getPrice方法
/**
* 活动书籍
* @author shixinke
*/
public class ActivityBook extends Book {
public ActivityBook(Long productId, String name, Double price, String isbn) {
super(productId, name, price, isbn);
}
/**
* 获取原价
* @return
*/
public Double getOriginPrice() {
return super.getPrice();
}
/**
* 获取活动价
* @return
*/
public Double getPrice() {
return 0.9 * super.getPrice();
}
}
/**
* 商品测试类
* @author shixinke
*/
public class ProductTest {
public static void main(String[] args) {
/**
* 平时
*/
Product book = new Book(1001L, "java设计模式", 125D, "97845124578");
/**
* 活动期间
*/
Product activityBook = new ActivityBook(1001L, "java设计模式", 125D, "97845124578");
ActivityBook bookObj = (ActivityBook) activityBook;
System.out.println("书籍名称:"+bookObj.getName() + " 书籍原价:" + bookObj.getOriginPrice() + " 书籍活动价:" + bookObj.getPrice());
}
}
第二种方式类图如下:
总结:
- 第一种方式是通过直接对原类进行修改,这样做是违背了”面向接口编程,而不是面向实现编程”的原则,如果又有一个新的活动,是另外一种折扣,则又需要修改原类,而且,几个活动无法并存
- 第二种方式通过扩展原类,重写父类的相关方法,没有修改原类,又实现了相应的功能,如果又有一个活动,直接再写一个扩展类,继承原类即可
2.依赖倒置原则
- 定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 表现:
- 抽象不应该依赖细节;细节应该依赖抽象
- 针对接口编程,不应该针对实现编程
- 优点:减少类之间的耦合,提高代码的可维护性和可读性,可降低因修改带来的风险
- 实例:
第一种:
/**
* 缓存数据源
* @author shixinke
*/
public class CacheDataSource {
public String get(Long userId) {
return "从缓存中获取数据";
}
}
/**
* 用户数据访问服务
* @author shixinke
*/
public class UserService {
/**
* 缓存数据源
*/
private CacheDataSource cacheDataSource;
public UserService(CacheDataSource cacheDataSource) {
this.cacheDataSource = cacheDataSource;
}
/**
* 获取数据
* @param userId
* @return
*/
public String getData(Long userId) {
return cacheDataSource.get(userId);
}
}
上面例子的缺点:
- 针对实现编程,灵活性差,如果将数据源改成数据库,则需要修改UserService类的代码
第二种:
(1)将数据源抽象为一个接口
/**
* 数据源接口
* @author shixinke
*
*/
public interface DataSource {
/**
* 获取数据
* @param userId
* @return
*/
String get(Long userId);
}
(2)不同的数据源实现这个接口
/**
* 缓存数据源
* @author shixinke
*/
public class CacheDataSource implements DataSource {
public String get(Long userId) {
return "从缓存中获取数据";
}
}
/**
* 数据库数据源
* @author shixinke
*/
public class DatabaseDataSource implements DataSource {
public String get(Long userId) {
return "从数据库中获取数据";
}
}
(3)userService中定义使用接口作为参数
/**
* 用户数据访问服务
* @author shixinke
*/
public class UserService {
/**
* 数据源
*/
private DataSource dataSource;
public UserService(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取数据
* @param userId
* @return
*/
public String getData(Long userId) {
return dataSource.get(userId);
}
}
(4)客户端控制使用哪个数据源
/**
* 用户数据测试类
* @author shixinke
*/
public class UserServiceTest {
public static void main(String[] args) {
DataSource cache = new CacheDataSource();
UserService userService = new UserService(cache);
System.out.println(userService.getData(1L));
/**
* 如果从其他数据源获取,只需要使用以下形式,而不用修改UserService类
*/
DataSource db = new DatabaseDataSource();
userService = new UserService(db);
System.out.println(userService.getData(1L));
}
}
以上类的UML关系:
3.单一职责原则
(1)定义:不要存在多于一个导致类变更的原因
(2)表现:一个类/接口/方法只负责一项职责
(3)优点:降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险
(4)实例:
订单支付
第一种:
/**
* 订单支付
* @author shixinke
*/
public class Payment {
/**
* 支付订单
* @param paymentType
* @param price
* @return
*/
public boolean pay(String paymentType, Double price) {
if ("alipay".equalsIgnoreCase(paymentType)) {
System.out.println("使用支付宝支付,支付金额为:"+price);
/**
* 支付宝支付逻辑
*/
return true;
} else if ("wepay".equalsIgnoreCase(paymentType)) {
System.out.println("使用微信支付,支付金额为:"+price);
/**
* 微信支付逻辑
*/
return true;
}
System.out.println("暂不支持其他支付方式");
return false;
}
}
缺点:如果添加一种新的支付方式,得修改支付类
第二种:
(1)定义一个支付接口
/**
* 订单支付
* @author shixinke
*/
public interface Payment {
/**
* 订单支付
* @param price
* @return
*/
boolean pay(Double price);
}
(2)根据不同的支付方式定义对应的支付类
/**
* 支付宝支付
* @author shixinke
*/
public class Alipay implements Payment {
public boolean pay(Double price) {
System.out.println("使用支付宝支付,支付金额为:"+price);
return true;
}
}
/**
* 微信支付
* @author shixinke
*/
public class Wepay implements Payment {
public boolean pay(Double price) {
System.out.println("使用微信支付,支付金额为:" + price);
return true;
}
}
(3)客户端确定使用哪种支付方式
/**
* 支付测试
* @author shixinke
*/
public class PaymentTest {
public static void main(String[] args) {
Payment payment = new Alipay();
payment.pay(38.9);
Payment wepay = new Wepay();
wepay.pay(38.9);
}
}
以上类的UML关系:
4.接口隔离原则
(1)定义:客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上
(2)表现:不要在一个接口里面放很多的方法
(3)优点:最小化接口,降低耦合,提高复用性
(4)实例:
第一种:不遵循接口隔离原则
定义一个大的接口:
/**
* 手机接口
* @author shixinke
*/
public interface Mobile {
/**
* 可以打电话
*/
void call();
/**
* 可以上网
*/
void internet();
}
只包含接口部分功能的实现类
/**
* 诺基亚手机(砸核桃的那种)
* @author shixinke
*/
public class NokiaMobile implements Mobile {
public void call() {
System.out.println("用诺基亚手机打电话");
}
public void internet() {
System.out.println("用诺基亚无法上网");
}
}
包含接口所有功能的实现类
/**
* 华为手机
* @author shixinke
*/
public class HuweiMobile implements Mobile {
public void call() {
System.out.println("用华为手机打电话");
}
public void internet() {
System.out.println("用华为手机上5G网网络");
}
}
第二种:遵循接口隔离原则的
将接口最小化定义,每个接口只实现一个功能
- 打电话的接口
/**
* 可以打电话的手机
* @author shixinke
*/
public interface CallableMobile {
/**
* 打电话
*/
void call();
}
- 上网的接口
/**
* 可以上网的手机
* @author shixinke
*/
public interface InternetMobile {
/**
* 上网
*/
void internet();
}
实现类选择对应的功能
- 诺基亚手机只实现打电话的功能
/**
* 诺基亚手机(它可以打电话,因此实现打电话的功能)
* @author shixinke
*/
public class NokiaMobile implements CallableMobile {
public void call() {
System.out.println("用诺基亚手机打电话");
}
}
- 华为手机实现打电话和上网两个功能
/**
* 华为手机(即能打电话,也能上网)
* @author shixinke
*/
public class HuaweiMobile implements CallableMobile, InternetMobile {
public void call() {
System.out.println("用华为手机打电话");
}
public void internet() {
System.out.println("用华为手机上5G网网络");
}
}
以上几个类对应的UML关系图:
5.迪米特原则(最少知道原则)
(1)定义:一个对象应该对其他对象保持最少的了解(也叫最少知道原则)
(2)表现:
- 尽量降低类与类之间的耦合(尽量少使用public的方法和变量)
- 只与成员变量、方法的入参及出参进行交流
(3)优点:降低类之间的耦合
(4)实例:
老板想知道当天的订单总数,而老板向运营人员获取这个数量,而此时运营系统没有查询订单数的功能(这个运营系统功能真不全),运营人员需要向开发求助,开发人员通过查询数据库来获取,将结果交给运营人员,运营人员再将结果报告给老板
不遵循迪米特法则
- 订单
/**
* 订单
* @author shixinke
*/
public class Order {
}
- 开发者
/**
* 开发者
* @author shixinke
*/
public class Developer {
public int getOrderCount() {
/**
* 查库
*/
List<Order> orders = new ArrayList<Order>(20);
for (int i = 0; i < 20; i++) {
orders.add(new Order());
}
return orders.size();
}
}
- 运营人员
/**
* 运营工作人员
* @author shixinke
*/
public class Operator {
/**
* 因为后台没有这个功能,所以运营需要开发查库才能获取数量
*/
public void getOrderCount(Developer developer) {
System.out.println("订单数:" + developer.getOrderCount());
}
}
- 老板
/**
* 老板
* @author shixinke
*/
public class Boss {
/**
* 问运营今天有多少订单数
* @param operator
*/
public void getOrderCount(Operator operator) {
Developer developer = new Developer();
operator.getOrderCount(developer);
}
}
缺点:
- boss类,需要为运营找一个开发,让开发来查询,而boss类只关心订单结果,不关心结果是怎么来的,因此,它不需要知道Developer类
遵循迪米特法则
- 订单
/**
* 订单
* @author shixinke
*/
public class Order {
}
- 开发者
/**
* 开发者
* @author shixinke
*/
public class Developer {
public int getOrderCount() {
/**
* 查库
*/
List<Order> orders = new ArrayList<Order>(20);
for (int i = 0; i < 20; i++) {
orders.add(new Order());
}
return orders.size();
}
}
- 运营人员
/**
* 运营工作人员
* @author shixinke
*/
public class Operator {
/**
* 因为后台没有这个功能,所以运营需要开发查库才能获取数量
*/
public void getOrderCount() {
Developer developer = new Developer();
System.out.println("订单数:" + developer.getOrderCount());
}
}
- 老板
/**
* 老板
* @author shixinke
*/
public class Boss {
/**
* 问运营今天有多少订单数
* @param operator
*/
public void getOrderCount(Operator operator) {
operator.getOrderCount();
}
}
注:老板只需要向运营人员要结果就行,其他细节他不关心,也不应该让他知道
以上类的UML关系图如下:
6.里氏替换原则
(1)定义:任何基类可以出现的地方,子类一定也可以出现
(2)表现:
- 子类可以扩展父类的功能,但不能改变父类原有的功能
(3)实例:
- 基类:
/**
* 动物类
* @author shixinke
*/
public class Animal {
public void eat() {
System.out.println("吃东西");
}
}
- 子类
/**
* 猴子
* @author shixinke
*/
public class Monkey extends Animal{
public void climb() {
System.out.println("猴子爬树");
}
}
- 使用
/**
* 测试类
* @author shixinke
*/
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
animal.eat();
/**
* 猴子
*/
Animal mon = new Monkey();
mon.eat();
}
}
在使用Animal类对象的时候,都可以使用Monkey这个子类的对象来进行替换
UML图如下:
7.组合复用原则
(1)定义:一个对象应该对其他对象保持最少的了解(也叫最少知道原则)
(2)表现:
- 尽量多用组合,少用继承
(3)实例:
以从数据源获取数据为例(数据源可能是Redis或者MySQL)
- 定义一个数据源接口:
/**
* 数据源
* @author shixinke
*/
public interface DataSource {
/**
* 获取数据
* @param id
* @return
*/
String get(String id);
}
- 定义Redis数据源
public class RedisDataSource implements DataSource {
public String get(String id) {
return "从Redis中获取数据";
}
}
- 定义MySQL数据源
public class MysqlDataSource implements DataSource {
public String get(String id) {
return "从MySQL中获取数据";
}
}
- 定义用户数据操作类
/**
* 用户数据操作类
* @author shixinke
*/
public class UserDao {
private DataSource dataSource;
public UserDao(DataSource dataSource) {
this.dataSource = dataSource;
}
public String getUserInfo(String id) {
return dataSource.get(id);
}
}
- 测试
/**
* 数据源测试
* @author shixinke
*/
public class DataSourceTest {
public static void main(String[] args) {
DataSource ds = new RedisDataSource();
UserDao userDao = new UserDao(ds);
System.out.println(userDao.getUserInfo("11"));
}
}
注:用户数据操作类UserDao不继承MysqlDataSource也不继承RedisDataSource,而是将DataSource的实例当成UserDao的一个实例变量,通过组合来关联两个类
以上类之间的关系: