场景

1、开闭原则(Open-Closed Principle,OCP)

是指一个软件实体(如类、模块和函数)应该对外扩展开放,对修改关闭。所谓的关闭,也正是对扩展和修改两个行为的一个原则。

它强调的是用抽象构建框架,用实现扩展细节,可以提高软件系统的可复用性和可维护性。

开闭原则是面向对象设计的最基本原则,例如版本更新,可以实现尽量不修改源代码的前提下增加新功能。

2、依赖倒置原则(Dependence Inversion Principle, DIP)

是指设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。

抽象不应该依赖细节,细节应该依赖抽象。可以减少类与类之间的耦合性,提高系统的稳定性,提高代码可读性和可维护性,

降低修改程序的风险。

3、单一职责原则(Simple Responsibility Pinciple,SRP)

是指不要存在多于一个导致变更的原因。如果一个类负责两个职责,

修改其中一个,则可能导致另一个出现问题。所以将多个职责用多个类进行解耦。

4、接口隔离原则(Interface Segregation Principle,ISP)

是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。

①一个类对另一个类的依赖应该建立在最小的接口之上。

②建立单一接口,不要建立庞大臃肿的接口。

③尽量细化接口,接口中的方法尽量少(适度,不是越少越好)

接口隔离原则符合高内聚低耦合的设计思想,可以使类具有很好的可读性、可扩展性。

5、迪米特原则(Law of Demeter LoD)

是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合度。

迪米特原则主要强调:只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称为成员朋友类,

而出现在方法体内部的类不属于朋友类,也就是说陌生的类不要以局部变量的形式出现在类的内部。

6、里氏替换原则(Liskov Substitution Principle,LSP)

是指如果对每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都替换为O2时,程序P

的行为没有发生变化,那么类型T2是类型T1的子类型。

可以理解为一个软件实体如果适用于一个父类,那么一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,

子类对象能够替换父类对象,而程序逻辑不变。

或者说子类可以扩展父类的功能,但不能改变父类原有的功能:

①子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。

②子类可以增加自己特有的方法。

③当子类的方法重载父类的方法时,方法的前置条件(方法的输入/入参)要比父类方法的输入参数更宽松。

④当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(方法的输出/返回值)要比父类更严格或与父类一样。

7、合成复用原则(Composite/Aggregate Reuse Principle,CARP)

是指尽量使用对象组合/聚合而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,

一个类的变化对其他类造成的影响相对较少。

继承叫做白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合称为黑箱复用,我们是无法获取到类以外的对象的实现细节的。

虽然我们要根据具体的业务场景来做代码设计,但也需要遵循OOP模型。

注:

博客:
​​霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主​​ 关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。

实现

开闭原则示例

1、以商城商品为例,新建一个商品接口Goods

package com.ruoyi.demo.designPrinc.ocp;

/**
* 商品接口
*/
public interface IGoods {
Integer getId();
String getName();
Double getPrice();
}

2、商品有很多种类,这里新建一个电脑的类Computer

package com.ruoyi.demo.designPrinc.ocp;

public class Computer implements IGoods{

private Integer Id;
private String name;
private Double price;

public Computer(Integer id, String name, Double price) {
Id = id;
this.name = name;
this.price = price;
}

@Override
public Integer getId() {
return this.Id;
}

@Override
public String getName() {
return this.name;
}

@Override
public Double getPrice() {
return this.price;
}
}

3、现在要给电脑做活动,价格打88折。

如果修改Computer中的getPrice()方法,则存在一定风险,可能会影响另外调用获取价格的地方。既要不修改原有代码,又

要实现价格优惠这个功能,可以写一个处理优惠逻辑的类ComputerDiscounts

package com.ruoyi.demo.designPrinc.ocp;

/**
* 要给电脑类做活动,搞优惠。如果修改Computer的getPrice方法,则存在一定的风险,可能影响其他地方的调用结果
*/
public class ComputerDiscounts extends Computer{
public ComputerDiscounts(Integer id, String name, Double price) {
super(id, name, price);
}

public Double getOrginPrice(){
return super.getPrice();
}

public Double getPrice(){
return super.getPrice()*0.88;
}
}

依赖倒置原则示例

1、还是以商品类为例,先创建学生类

package com.ruoyi.demo.designPrinc.dip;

public class Student {

public void buyComputer(){
System.out.println("学生购买了电脑");
}

public void buyBook(){
System.out.println("学生购买了书籍");
}
}

调用学生类的购买电脑和购买书籍的方法

package com.ruoyi.demo.designPrinc.dip;

public class dipTest {
public static void main(String[] args) {
Student student = new Student();
student.buyComputer();
student.buyBook();
}
}

学生购买了电脑和书籍,如果还要购买别的商品。这时候因为业务扩展,要从低层到高层(调用层)依次修改代码。

在Student类中添加buyPen()方法,在高层调用方也要追加调用。这样一来,系统发布之后很不稳定,可能导致

意想不到的风险。

下面优化代码

创建一个商品的抽象IGoods接口,抽离出购买方法

package com.ruoyi.demo.designPrinc.dip;

public interface IGoods {
void buy();
}

然后编写购买电脑类

package com.ruoyi.demo.designPrinc.dip;

public class BuyComputer implements IGoods{

@Override
public void buy() {
System.out.println("学生买了电脑");
}
}

再编写购买书籍类

package com.ruoyi.demo.designPrinc.dip;

public class BuyBook implements IGoods{
@Override
public void buy() {
System.out.println("学生买了书籍");
}
}

修改Student

package com.ruoyi.demo.designPrinc.dip;

public class Student {
public void buy(IGoods goods){
goods.buy();
}
}

最后修改调用方代码

package com.ruoyi.demo.designPrinc.dip;

public class dipTest {
public static void main(String[] args) {
Student student = new Student();
student.buy(new BuyComputer());
student.buy(new BuyBook());
}
}

这时,当再有新的业务,比如购买手机等扩展时,只需要新建一个类,通过传参的方式告诉Student,

而不需要修改底层代码。这种方式也叫依赖注入。注入的方式还有构造器注入和Setter方法。

单一职责原则示例

用商品举例,自营商品可以无条件退货,代理商品需要协商退货。创建商品类

package com.ruoyi.demo.designPrinc.srp;

public class Goods {
public void buy(String goodsType){
if("自营".equals(goodsType)){
System.out.println(goodsType+"商品可以无条件退货");
}else {
System.out.println(goodsType+"商品需要协商退货");
}
}
}

调用代码

package com.ruoyi.demo.designPrinc.srp;

public class srpTest {
public static void main(String[] args) {
Goods goods = new Goods();
goods.buy("自营");
goods.buy("代理");
}
}

上面逻辑中,商品Goods类处理两种逻辑,假如需要对商品进行活动促销,两种类型商品的活动逻辑不一样,

必须修改代码,而修改代码势必会相互影响。所以对职责进行解耦。

分别创建两个类

proprietaryGoods:

package com.ruoyi.demo.designPrinc.srp;

/**
* 自营商品
*/
public class proprietaryGoods {
public void buy(String goodsType){
System.out.println(goodsType+"商品需要协商退货");
}
}

agentGoods :

package com.ruoyi.demo.designPrinc.srp;

/**
* 代理商品
*/
public class agentGoods {
public void buy(String goodsType){
System.out.println(goodsType+"商品可以无条件退货");
}
}

调用代码修改

package com.ruoyi.demo.designPrinc.srp;

public class srpTest {
public static void main(String[] args) {
proprietaryGoods proprietaryGoods = new proprietaryGoods();
proprietaryGoods.buy("自营");
agentGoods agentGoods = new agentGoods();
agentGoods.buy("代理");
}
}

业务发展,要增加会员VIP业务。VIP可以获取商品优惠券,普通会员只能获取商品基本价格。

所以在控制商品上有两个职责,可以将展示的职责和管理的职责分开,实现同一个抽象依赖。

设计顶层接口IGoods

package com.ruoyi.demo.designPrinc.srp;

import java.util.List;

public interface IGoods {
//获取商品价格
String getGoodsPrice();
//获取商品优惠券
List<Double> getCoupons();
//购买商品
void buyGoods();
//商品退货
void returnGoods();
}

将这个接口拆分成两个接口

IGoodsInfo

package com.ruoyi.demo.designPrinc.srp;

import java.util.List;

public interface IGoodsInfo {
//获取商品价格
String getGoodsPrice();
//获取商品优惠券
List<Double> getCoupons();
}

IGoodsManager

package com.ruoyi.demo.designPrinc.srp;

public interface IGoodsManager {
//购买商品
void buyGoods();
//商品退货
void returnGoods();
}

接口隔离原则示例

商品类接口

package com.ruoyi.demo.designPrinc.isp;

public interface IGoods {
void eat();
void drink();
void wear();
}

有能吃的、能喝的、能穿的。

食品类

package com.ruoyi.demo.designPrinc.isp;

public class FoodGoods implements IGoods{
@Override
public void eat() {

}

@Override
public void drink() {

}

@Override
public void wear() {

}
}

衣物类

package com.ruoyi.demo.designPrinc.isp;

public class ClothesGoods implements IGoods{
@Override
public void eat() {

}

@Override
public void drink() {

}

@Override
public void wear() {

}
}

所以,如果按照上面的进行设计,那么食品类的wear只能空着,衣物类的eat和drink也只能空着。

所以分别设计吃、喝、穿三个接口

package com.ruoyi.demo.designPrinc.isp;

public interface IEatGoods {
void eat();
}

package com.ruoyi.demo.designPrinc.isp;

public interface IDrinkGoods {
void drink();
}

package com.ruoyi.demo.designPrinc.isp;

public interface IWearGoods {
void wear();
}

然后食品类是需要实现吃、喝接口即可

package com.ruoyi.demo.designPrinc.isp;

public class FoodGoodsNew implements IEatGoods,IDrinkGoods{

@Override
public void drink() {

}

@Override
public void eat() {

}
}

迪米特原则示例

老板要采购员统计符合条件的商品数量。

商品类

package com.ruoyi.demo.designPrinc.lod;

public class Goods {
}

采购员

package com.ruoyi.demo.designPrinc.lod;

import java.util.List;

public class Buyer {
public void checkNumberOfGoods(List<Goods> goodsList){
System.out.println("符合条件的商品数量为:"+goodsList.size());
}
}

老板

package com.ruoyi.demo.designPrinc.lod;

import java.util.ArrayList;
import java.util.List;

public class Boss {
public void commandCheckNumber(Buyer buyer){
//模拟老板一页一页往下翻页,采购员实时统计
List<Goods> goodsList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
goodsList.add(new Goods());
}
buyer.checkNumberOfGoods(goodsList);
}
}

测试代码

package com.ruoyi.demo.designPrinc.lod;

public class lodTest {
public static void main(String[] args) {
Boss boss = new Boss();
Buyer buyer = new Buyer();
boss.commandCheckNumber(buyer);
}
}

根据迪米特原则,Boss只想要结果,不需要跟商品直接交流。但是采购员统计需要引用商品对象。

所以在Boss类中就出现了Goods商品类。

只需要将Boss类中的Goods类交给采购员,使Boss与Goods不再有关联。

Buyer:

package com.ruoyi.demo.designPrinc.lod;

import java.util.ArrayList;
import java.util.List;

public class Buyer {
public void checkNumberOfGoods(){
List<Goods> goodsList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
goodsList.add(new Goods());
}
System.out.println("符合条件的商品数量为:"+goodsList.size());
}
}

Boss:

package com.ruoyi.demo.designPrinc.lod;

public class Boss {
public void commandCheckNumber(Buyer buyer){
buyer.checkNumberOfGoods();
}
}

里氏替换原则示例

新建鸟类,有飞行速度属性,可以根据飞行距离和飞行速度计算飞行时间

package com.ruoyi.demo.designPrinc.lsp;

/**
* 鸟类
*/
public class Bird {
//飞行速度
double flySpeed;

public double getFlySpeed() {
return flySpeed;
}

public void setFlySpeed(double flySpeed) {
this.flySpeed = flySpeed;
}

public double getFlyTime(double distance){
return (distance/flySpeed);
}
}

新建燕子类,继承自鸟类

package com.ruoyi.demo.designPrinc.lsp;

/**
* 燕子类
*/
public class Swallow extends Bird{

}

新建企鹅类,继承自鸟类

package com.ruoyi.demo.designPrinc.lsp;

/**
* 企鹅类
*/
public class Penguin extends Bird{
public void setFlySpeed(double speed){
flySpeed = 0;
}
}

因为企鹅不会飞,所以设置其飞行速度为0

计算飞行时间业务类

package com.ruoyi.demo.designPrinc.lsp;

public class lspTest {
public static void main(String[] args) {
Bird swallow = new Swallow();
swallow.setFlySpeed(100);

Bird penguin = new Penguin();
penguin.setFlySpeed(100);

try {
System.out.println("燕子飞行50公里耗时:"+swallow.getFlyTime(50));
System.out.println("企鹅飞行50公里耗时:"+penguin.getFlyTime(50));
}catch (Exception e){
System.out.println("出错:"+e.getMessage());
}

}
}

上面的示例中,因为企鹅不具备飞行能力,重写了鸟类的速度方法,违背了里氏替换原则,所以当计算

企鹅的飞行时间时出现了除数不能为0的错误。

将上面的继续关系修改为,增加一个动物类,描述动物的普遍通用行为,比如计算时间和速度,鸟类和企鹅分别继承动物类,

燕子继承鸟类。这样燕子能计算飞行时间,企鹅能计算奔跑时间,同时又避免了重写父类的方法,符合里氏替换原则。

合成复用原则示例

以数据库操作添加商品为例,创建数据库连接类

public class DBConnection {
public String getConnection(){
return "Mysql 数据库连接";
}
}

然后创建GoodsDao

package com.ruoyi.demo.designPrinc.carp;

public class GoodsDao {
private DBConnection dbConnection;

public void setDbConnection(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}

public void addGoods(){
String connection = dbConnection.getConnection();
System.out.println("使用连接"+connection+"增加商品成功");
}
}

上面的DBConnection还不是一种抽象,不便于系统扩展。目前的系统支持Mysql数据库连接。

后续业务扩展,需要支持Oracle数据库连接。

如果直接在DBConnection中增加对Oracle数据库的支持,会违背开闭原则。

可以在不修改Dao代码的前提下,而将DBConnection修改为abstract的。

package com.ruoyi.demo.designPrinc.carp;

public abstract class DBConnection {
public abstract String getConnection();
}

然后将Mysql的逻辑抽离

package com.ruoyi.demo.designPrinc.carp;

public class MySqlConnection extends DBConnection{
@Override
public String getConnection() {
return "Mysql数据库连接";
}
}

再创建Oracle连接

package com.ruoyi.demo.designPrinc.carp;

public class OracleConnection extends DBConnection{
@Override
public String getConnection() {
return "Oracle 数据库连接";
}
}

具体选择交给应用层。