超级链接: Java常用设计模式的实例学习系列-绪论
参考:《HeadFirst设计模式》
本文以购物车支付
场景为例,对面向对象的六个原则进行理解。
本文中的代码是逐步重构的,如果本步骤的代码与上步骤的代码相同,则不再展示。
本文的主要目的是理解六个设计原则,所以对于需求是否合理和代码是否粗糙就请不要计较了。
完整代码可以参考github:https://github.com/hanchao5272/design-pattern
直接上代码
商品类:Goods
/**
* 商品
*
* @author hanchao
*/
#Setter
#Getter
#AllArgsConstructor
public class Goods {
/**
* 商品名称
*/
private String name;
/**
* 商品价格
*/
private Float price;
/**
* 折扣
*/
private Float discount;
}
购物车类:ShoppingCart
/**
* 购物车
*
* @author hanchao
*/
#Slf4j
#NoArgsConstructor
public class ShoppingCart {
/**
* 商品列表
*/
private List<Goods> goodsList = new ArrayList<>();
/**
* 添加商品
*/
public ShoppingCart addGoods(Goods goods) {
goodsList.add(goods);
return this;
}
/**
* 显示商品
*/
public ShoppingCart showGoods() {
goodsList.forEach(goods -> log.info("购物车中有:{},价格:{}.", goods.getName(), goods.getPrice() * goods.getDiscount()));
log.info("-------------------------------------");
log.info("购物车中获取总价格:{}元\n", this.totalCost());
return this;
}
/**
* 计算总价
*/
public float totalCost() {
return goodsList.stream().map(goods -> goods.getPrice() * goods.getDiscount()).reduce(Float::sum).orElse(0f);
}
/**
* 连接支付宝
*/
public void connect() {
log.info("开始进行支付宝支付:");
log.info("1.初始化支付宝客户端...");
log.info("2.设置请求参数...");
log.info("3.请求支付宝进行付款,并获取支付结果...");
log.info("-------------------------------------");
}
/**
* 支付(日志显示2位小数)
*/
public void pay() {
float money = this.totalCost();
connect();
log.info("4.通过支付宝支付了" + FormatUtil.format(money) + "元.");
}
}
**工具类:浮点型保留2位小数:FormatUtil **
/**
* 格式化工具类
*
* @author hanchao
*/
public class FormatUtil {
/**
* 浮点数显示两位小数
*/
public static String format(float number) {
return new DecimalFormat("0.00").format(Objects.isNull(number) ? 0 : number);
}
}
测试代码
public static void main(String[] args) {
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.addGoods(new Goods("一双球鞋", 3500f, 1f))
.addGoods(new Goods("一件外套", 2800.00f, 0.80f))
.showGoods()
.pay();
}
测试结果:
2019-07-18 14:19:03,784 INFO - 购物车中有:一双球鞋,价格:3500.0.
2019-07-18 14:19:03,786 INFO - 购物车中有:一件外套,价格:2240.0.
2019-07-18 14:19:03,786 INFO - -------------------------------------
2019-07-18 14:19:03,793 INFO - 购物车中获取总价格:5740.0元
2019-07-18 14:19:03,793 INFO - 开始进行支付宝支付:
2019-07-18 14:19:03,793 INFO - 1.初始化支付宝客户端...
2019-07-18 14:19:03,793 INFO - 2.设置请求参数...
2019-07-18 14:19:03,793 INFO - 3.请求支付宝进行付款,并获取支付结果...
2019-07-18 14:19:03,793 INFO - -------------------------------------
2019-07-18 14:19:03,794 INFO - 4.通过支付宝支付了5740.00元.
##2. 六个原则
###2.1. 原则一:单一职责原则
单一职责原则:
问题
回顾章节1.3.
的ShoppingCart类
,发现它违背了单一职责原则:购物逻辑与支付逻辑放在了同一个类中。
随着需求的不断发展,支付逻辑与购物逻辑都在不断增多,代码越来越复杂,这种设计毫无扩展性与灵活性。
解决
将支付宝相关逻辑抽离出来,放到单独的类中处理。
支付类:AliPayClient
/**
* 支付宝类
*
* @author hanchao
*/
#Slf4j
public class AliPayClient {
/**
* 连接支付宝
*/
private void connect() {
log.info("开始进行支付宝支付:");
log.info("1.初始化支付宝客户端...");
log.info("2.设置请求参数...");
log.info("3.请求支付宝进行付款,并获取支付结果...");
log.info("-------------------------------------");
}
/**
* 支付(日志显示2位小数)
*/
public void pay(float money) {
connect();
log.info("4.通过支付宝支付了" + FormatUtil.format(money) + "元.");
}
}
购物车类:ShoppingCart
/**
* 购物车
*
* @author hanchao
*/
#Slf4j
#NoArgsConstructor
public class ShoppingCart {
//....
/**
* 通过支付宝支付
*/
public void pay() {
new AliPayClient().pay(totalCost());
}
}
总结:
经过上述优化,AliPayClient只负责支付相关逻辑,ShoppingCart只负责购物相关逻辑。当一方需求发生变化时,只需要修改一个类,不会影响另一个类。
开闭原则
抽象
指的是interface
或者abstract class
。抽象
的过程,实质上是在概括归纳总结它的本质。抽象
,还能够统一规范实现类的需要实现的方法。类本身
的处理。问题
回顾章节2.2.
的代码,发现它违背了开闭原则:支付客户端和购物车都是具体实现类。
解决
将支付客户端和购物车抽象化。
支付宝接口:IAliPayClient
/**
* 支付宝的抽象类
*
* @author hanchao
*/
public interface IAliPayClient {
/**
* 连接
*/
void connect();
/**
* 支付
*/
void pay(Float money);
}
支付宝实现类:AliPayClient
/**
* 支付宝
*
* @author hanchao
*/
#Slf4j
public class AliPayClient implements IAliPayClient {
/**
* 连接
*/
@Override
public void connect() {
//...
}
/**
* 支付
*/
@Override
public void pay(Float money) {
//...
}
}
购物车接口:IShoppingCart
/**
* 购物车的抽象类
*
* @author hanchao
*/
public interface IShoppingCart {
/**
* 添加商品
*/
IShoppingCart addGoods(Goods goods);
/**
* 显示商品
*/
IShoppingCart showGoods();
/**
* 计算总价
*/
Float totalCost();
/**
* 通过支付宝支付
*/
void pay();
}
购物车实现类:
/**
* 普通购物车
*
* @author hanchao
*/
#Slf4j
#NoArgsConstructor
public class ShoppingCart implements IShoppingCart {
//....
/**
* 添加商品
*/
@Override
public IShoppingCart addGoods(Goods goods) {
//...
}
/**
* 显示商品
*/
@Override
public IShoppingCart showGoods() {
//...
}
/**
* 计算总价
*/
@Override
public Float totalCost() {
//...
}
/**
* 通过支付宝支付
*/
@Override
public void pay() {
IAliPayClient aliPayClient = new AliPayClient();
aliPayClient.pay(totalCost());
}
}
测试代码
public static void main(String[] args) {
//注意这里是接口
IShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.addGoods(new Goods("一双球鞋", 3500f, 1f))
.addGoods(new Goods("一件外套", 2800.00f, 0.80f))
.showGoods()
//设置微信支付方式
.pay();
}
总结:
经过上述优化,如果需求发生变化,比方说新出现了一种超级购物车实现SupperShoppingCart
,这样在ShoppingDemo
类中,只需要修改IShoppingCart shoppingCart = new SupperShoppingCart();
即可,无需修改后续的添加商品,显示购物车内容和结算等代码逻辑。
依赖倒置原则
依赖
关系的处理。问题
回顾章节2.2.
的ShoppingCart#pay()
方法,发现它违背了违背依赖倒置原则:当新增微信支付时,因为写死了用支付宝支付,所以还是要修改支付代码。
解决
将支付宝支付和微信支付抽象成更高层次的支付接口;将购物车类对支付类的依赖设置为可替换的。
支付接口:PayClient
/**
* 支付的抽象类
*
* @author hanchao
*/
public interface PayClient {
/**
* 连接
*/
void connect();
/**
* 支付
*/
void pay(Float money);
}
支付实现类:支付宝:AliPayClient
/**
* 支付宝
*
* @author hanchao
*/
#Slf4j
public class AliPayClient implements PayClient {
//...
}
支付实现类:微信:WeChatPayClient
/**
* 微信支付
*
* @author hanchao
*/
#Slf4j
public class WeChatPayClient implements PayClient {
/**
* 连接
*/
@Override
public void connect() {
//do nothing
}
/**
* 支付
*/
@Override
public void pay(Float money) {
log.info("开始进行微信支付:");
log.info("1.设置请求参数...");
log.info("2.发送HTTP请求微信付款地址进行支付...");
log.info("3.进行微信支付回调,并获取支付结果...");
log.info("-------------------------------------");
log.info("4.通过微信支付了" + money + "元.");
}
}
购物车接口:IShoppingCart
/**
* 购物车的抽象类
*
* @author hanchao
*/
public interface IShoppingCart {
IShoppingCart setPayClient(PayClient payClient);
//....
}
购物车实现类:ShoppingCart
/**
* 购物车
*
* @author hanchao
*/
#Slf4j
#NoArgsConstructor
public class ShoppingCart implements IShoppingCart {
//....
/**
* 支付方式
*/
private PayClient payClient = new AliPayClient();
@Override
public IShoppingCart setPayClient(PayClient payClient) {
this.payClient = payClient;
return this;
}
//....
/**
* 通过支付宝支付
*/
@Override
public void pay() {
payClient.pay(totalCost());
}
}
测试代码
public static void main(String[] args) {
IShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.addGoods(new Goods("一双球鞋", 3500f, 1f))
.addGoods(new Goods("一件外套", 2800.00f, 0.80f))
.showGoods()
//设置微信支付方式,而无需修改ShoppingCart
.setPayClient(new WeChatPayClient())
.pay();
}
总结
经过上述优化,即使需求发生变化,比如新增一种支付方式银联支付UnionPayClient
,则无需修改ShoppingCart的代码,只需调用shoppingCart..setPayClient(new UnionPayClient())
即可。
里式替换原则
问题
回顾章节2.3.
的WeChatPayClient#pay()
方法,发现它违背了里式替换原则:微信支付作为支付抽象类的子类,并不能完全实现父类规定的保留2位小数功能。
解决
修改子类型的重载方法,完全实现父类型的功能。
支付实现类:微信:WeChatPayClient
/**
* 微信支付
*
* @author hanchao
*/
#Slf4j
public class WeChatPayClient implements PayClient {
//...
/**
* 支付
*/
@Override
public void pay(Float money) {
//...
log.info("4.通过微信支付了" + FormatUtil.format(money) + "元.");
}
}
总结
经过上述优化,微信支付完全实现了父类规定的接口功能,这样能够避免问题的出现。
P.s.上面的示例比较low,将就着看吧。
接口隔离原则
问题
回顾章节2.3.
的WeChatPayClient
类,发现它违背了接口隔离原则:PayClient的接口有两个方法,但是connect()对于微信支付是无用的。
解决
拆解大接口,形成小接口。
支付接口:PayClient
/**
* 支付的抽象类
*
* @author hanchao
*/
public interface PayClient {
/**
* 支付
*/
void pay(float money);
}
可连接接口:Connectable
/**
* 可连接的
*
* @author hanchao
*/
public interface Connectable {
/**
* 连接
*/
void connect();
}
支付实现类:支付宝:AliPayClient
/**
* 支付宝
*
* @author hanchao
*/
#Slf4j
public class AliPayClient implements PayClient, Connectable {
//...
}
支付实现类:微信:WeChatPayClient
/**
* 微信支付
*
* @author hanchao
*/
#Slf4j
public class WeChatPayClient implements PayClient {
/**
* 支付
*/
@Override
public void pay(float money) {
log.info("开始进行微信支付:");
log.info("1.设置请求参数...");
log.info("2.发送HTTP请求微信付款地址进行支付...");
log.info("3.进行微信支付回调,并获取支付结果...");
log.info("-------------------------------------");
log.info("4.通过微信支付了" + FormatUtil.format(money) + "元.");
}
}
总结
经过上述优化,微信支付只需要实现PayClient
即可,无需去重写不需要的方法。
迪米特原则
需求变化
商品的价格计算不仅仅有普通折扣,还有会员折上折
问题
回顾章节1.3.
的ShoppingCart#showGoods()和ShoppingCart#totalCost()
方法,发现它违背了迪米特原则:购物车不应该关心商品的计算逻辑。
这种设计导致购物车过多参与价格的计算,耦合性太高,当价格计算规则发生变化时,这些方法都需要修改。
解决
购物车不应该知道商品价格的计算规则,他只要能够通过某个方法直接获取商品的最终价格接口。
商品类:Goods
/**
* 商品
*
* @author hanchao
*/
#Setter
#Getter
#AllArgsConstructor
public class Goods {
/**
* 商品名称
*/
private String name;
/**
* 商品价格
*/
private float price;
/**
* 折扣
*/
private float discount;
/**
* 会员折扣
*/
private float vipDiscount;
/**
* 计算最终价格(把价格计算放在自己的类中,不然别的类知道)
*/
public float getFinalPrice() {
return price * discount * vipDiscount;
}
}
购物车实现类:ShoppingCart
/**
* 购物车
*
* 最后一个原则:开闭原则:对扩展开放,对修改关闭。前面做的事情。
*
* @author hanchao
*/
#Slf4j
#NoArgsConstructor
public class ShoppingCart implements IShoppingCart {
//...
/**
* 显示商品
*/
@Override
public IShoppingCart showGoods() {
goodsList.forEach(goods -> log.info("购物车中有:{},价格:{}.", goods.getName(), goods.getFinalPrice()));
log.info("-------------------------------------");
log.info("购物车中获取总价格:{}元\n", this.totalCost());
return this;
}
/**
* 计算总价
*/
@Override
public float totalCost() {
return goodsList.stream().map(Goods::getFinalPrice).reduce(Float::sum).orElse(0f);
}
//...
}
总结
经过上述优化,当商品的计算方式再次发生变化,其实对购物车类并无影响。
面向对象的六个设计原则的总体目的:降低耦合性,提高系统可维护性、可扩展性、灵活性、容错性等。
面向对象的六个设计原则没有先后顺序,也没有主次,本文只是为了叙述方便所以采取了文中的引入顺序。
在实际开发中很难用到全部的六个原则,要集合实际情况量力而行,不要过分追求设计原则,而忘了最初目的,造成舍本逐末。
在软件开发过程中,唯一不变的就是变化,所以我们只能尽量做到代码的优化,但是却不能保证代码永远满足需求。
参考: