【架构设计】你真的理解软件设计中的SOLID原则吗?
前言
在软件架构设计领域,有一个大名鼎鼎的设计原则——SOLID原则,它是由由Robert C. Martin(也称为 Uncle Bob)提出的,指导我们写出可维护、可以测试、高扩展、高内聚、低耦合的代码。是不是很牛,但是你们都理解这个设计原则吗,如果理解不深入的话,更这我通过JAVA示例深入浅出的明白这个重要的原则吧。SOLID实际上是由5条原则组成, 我们逐一介绍。
S:单一职责原则(SRP)
O : 开闭原则 (OSP)
L : 里氏替换原则 (LSP)
I:接口隔离原则(ISP)
D:依赖倒置原则(DIP)
欢迎关注个人公众号【JAVA旭阳】交流学习
单一职责原则(SRP)
这个原则指出“一个类应该只有一个改变的理由”,这意味着每个类都应该有单一的责任或单一的目的。
举个例子来理解其中的核心思想,假设有一个BankService
的类需要执行以下操作:
- 存钱
- 取钱
- 打印通票簿
- 获取贷款信息
- 发送一次性密码
package com.alvin.solid.srp;
public class BankService {
// 存钱
public long deposit(long amount, String accountNo) {
//deposit amount
return 0;
}
// 取钱
public long withDraw(long amount, String accountNo) {
//withdraw amount
return 0;
}
// 打印通票簿
public void printPassbook() {
//update transaction info in passbook
}
// 获取贷款信息
public void getLoanInterestInfo(String loanType) {
if (loanType.equals("homeLoan")) {
//do some job
}
if (loanType.equals("personalLoan")) {
//do some job
}
if (loanType.equals("car")) {
//do some job
}
}
// 发送一次性密码
public void sendOTP(String medium) {
if (medium.equals("email")) {
//write email related logic
//use JavaMailSenderAPI
}
}
}
现在我们来看看这么写会带来什么问题?
比如对于获取贷款信息 getLoanInterestInfo()
方法,现在银行服务只提供个人贷款、房屋贷款和汽车贷款的信息,假设将来银行的人想要提供一些其他贷款功能,如黄金贷款和学习贷款,那么你需要修改这个类实现对吗?
同样,考虑 sendOTP()
方法,假设 BankService
支持将 OTP
媒体作为电子邮件发送,但将来他们可能希望引入将 OTP
媒体通过手机短信发送,这时候需要再次修改BankService
来实现。
发现没有,它不遵循单一职责原则,因为这个类有许多责任或任务要执行,不仅会让BankService
这个类很庞大,可维护性差。
为了实现单一职责原则的目标,我们应该实现一个单独的类,它只执行单一的功能。
- 打印相关的工作
PrinterService
public class PrinterService {
public void printPassbook() {
//update transaction info in passbook
}
}
- 贷款相关的工作
LoanService
public class LoanService {
public void getLoanInterestInfo(String loanType) {
if (loanType.equals("homeLoan")) {
//do some job
}
if (loanType.equals("personalLoan")) {
//do some job
}
if (loanType.equals("car")) {
//do some job
}
}
}
- 通知相关的工作
NotificationService
public class NotificationService{
public void sendOTP(String medium) {
if (medium.equals("email")) {
//write email related logic
//use JavaMailSenderAPI
}
}
}
现在,如果你观察到每个类都有单一的责任来执行他们的任务。这正是 单一职责 SRP
的核心思想。
开闭原则(OSP)
该原则指出“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭”,这意味着您应该能够扩展类行为,而无需修改它。
让我们通过一个例子来理解这个原则。让我们考虑一下我们刚刚创建的同一个通知服务。
public class NotificationService {
public void sendOTP(String medium) {
if (medium.equals("email")) {
//write email related logic
//use JavaMailSenderAPI
}
}
}
如前所述,如果您想通过手机号码发送 OTP
,那么您需要修改 NotificationService
,对吗?
但是根据OSP原则,对扩展开放,对修改关闭, 因此不建议为增加一个通知方式就修改NotificationService
类,而是要扩展,怎么扩展呢?
- 定义一个通知服务接口
public interface NotificationService {
public void sendOTP();
}
- E-mail方式通知类
EmailNotification
public class EmailNotification implements NotificationService{
public void sendOTP(){
// write Logic using JavaEmail api
}
}
- 手机方式通知类
MobileNotification
public class MobileNotification implements NotificationService{
public void sendOTP(){
// write Logic using Twilio SMS API
}
}
- 同样可以添加微信通知服务的实现
WechatNotification
public class WechatNotification implements NotificationService{
public void sendOTP(String medium){
// write Logic using wechat API
}
}
这样的方式就是遵循开闭原则的,你不用修改核心的业务逻辑,这样可能带来意向不到的后果,而是扩展实现方式,由调用方根据他们的实际情况调用。
里氏替换原则(LSP)
该原则指出“派生类或子类必须可替代其基类或父类”。换句话说,如果类 A 是类 B 的子类型,那么我们应该能够在不中断程序行为的情况下用 A 替换 B。
这个原理有点棘手和有趣,它是基于继承概念设计的,所以让我们通过一个例子更好地理解它。
让我们考虑一下我有一个名为 SocialMedia
的抽象类,它支持所有社交媒体活动供用户娱乐,如下所示:
package com.alvin.solid.lsp;
public abstract class SocialMedia {
public abstract void chatWithFriend();
public abstract void publishPost(Object post);
public abstract void sendPhotosAndVideos();
public abstract void groupVideoCall(String... users);
}
社交媒体可以有多个实现或可以有多个子类,如 Facebook
、Wechat
、Weibo
和 Twitter
等。
现在让我们假设 Facebook
想要使用这个特性或功能。
package com.alvin.solid.lsp;
public class Wechat extends SocialMedia {
public void chatWithFriend() {
//logic
}
public void publishPost(Object post) {
//logic
}
public void sendPhotosAndVideos() {
//logic
}
public void groupVideoCall(String... users) {
//logic
}
}
我们都知道Facebook
都提供了所有上述的功能,所以这里我们可以认为Facebook
是SocialMedia
类的完全替代品,两者都可以无中断地替代。
现在让我们讨论 Weibo
类
package com.alvin.solid.lsp;
public class Weibo extends SocialMedia {
public void chatWithFriend() {
//logic
}
public void publishPost(Object post) {
//logic
}
public void sendPhotosAndVideos() {
//logic
}
public void groupVideoCall(String... users) {
//不适用
}
}
我们都知道Weibo
微博这个产品是没有群视频功能的,所以对于 groupVideoCall
方法来说 Weibo
子类不能替代父类 SocialMedia
。所以我们认为它是不符合里式替换原则。
那有什么解决方案吗?
那就把功能拆开呗。
public interface SocialMedia {
public void chatWithFriend();
public void sendPhotosAndVideos()
}
public interface SocialPostAndMediaManager {
public void publishPost(Object post);
}
public interface VideoCallManager{
public void groupVideoCall(String... users);
}
现在,如果您观察到我们将特定功能隔离到单独的类以遵循LSP。
现在由实现类决定支持功能,根据他们所需的功能,他们可以使用各自的接口,例如 Weibo
不支持视频通话功能,因此 Weibo
实现可以设计成这样:
public class Instagram implements SocialMedia,SocialPostAndMediaManager{
public void chatWithFriend(){
//logic
}
public void sendPhotosAndVideos(){
//logic
}
public void publishPost(Object post){
//logic
}
}
这样子就是符合里式替换原则LSP。
接口隔离原则(ISP)
这个原则是第一个适用于接口而不是 SOLID
中类的原则,它类似于单一职责原则。它声明“不要强迫任何客户端实现与他们无关的接口”。
例如,假设您有一个名为 UPIPayment
的接口,如下所示
public interface UPIPayments {
public void payMoney();
public void getScratchCard();
public void getCashBackAsCreditBalance();
}
现在让我们谈谈 UPIPayments
的一些实现,比如 Google Pay
和 AliPay
。
Google Pay
支持这些功能所以他可以直接实现这个 UPIPayments
但 AliPay
不支持 getCashBackAsCreditBalance()
功能所以这里我们不应该强制客户端 AliPay
通过实现 UPIPayments
来覆盖这个方法。
我们需要根据客户需要分离接口,所以为了支持这个ISP,我们可以如下设计:
- 创建一个单独的接口来处理现金返还。
public interface CashbackManager{
public void getCashBackAsCreditBalance();
}
现在我们可以从 UPIPayments
接口中删除getCashBackAsCreditBalance
,AliPay
也不需要实现getCashBackAsCreditBalance()
这个它没有的方法了。
依赖倒置原则(DIP)
该原则指出我们需要使用抽象(抽象类和接口)而不是具体实现,高级模块不应该直接依赖于低级模块,但两者都应该依赖于抽象。
我们直接上例子来理解。
假如你去当地一家商店买东西,并决定使用刷卡付款。因此,当您将卡交给店员进行付款时,店员不会检查你提供的是哪种卡,借记卡还是信用卡,他们只会进行刷卡,这就是店员和你之间传递“卡”这个抽象。
现在让我们用代码替换这个例子,以便更好地理解它。
- 借记卡
public class DebitCard {
public void doTransaction(int amount){
System.out.println("tx done with DebitCard");
}
}
- 信用卡
public class CreditCard{
public void doTransaction(int amount){
System.out.println("tx done with CreditCard");
}
}
现在用这两张卡你去购物中心购买了一些订单并决定使用信用卡支付
public class ShoppingMall {
private DebitCard debitCard;
public ShoppingMall(DebitCard debitCard) {
this.debitCard = debitCard;
}
public void doPayment(Object order, int amount){ debitCard.doTransaction(amount);
}
public static void main(String[] args) {
DebitCard debitCard=new DebitCard();
ShoppingMall shoppingMall=new ShoppingMall(debitCard);
shoppingMall.doPayment("some order",5000);
}
}
上面的做法是一个错误的方式,因为 ShoppingMall
类与 DebitCard
紧密耦合。
现在你的借记卡余额不足,想使用信用卡,那么这是不可能的,因为 ShoppingMall
与借记卡紧密结合。
当然你也可以这样做,从构造函数中删除借记卡并注入信用卡。但这不是一个好的方式,它不符合依赖倒置原则。
那该如何正确设计呢?
- 定义依赖的抽象接口
BankCard
public interface BankCard {
public void doTransaction(int amount);
}
- 现在
DebitCard
和CreditCard
都实现BankCard
public class CreditCard implements BankCard{
public void doTransaction(int amount){
System.out.println("tx done with CreditCard");
}
}
public class DebitCard implements BankCard {
public void doTransaction(int amount){
System.out.println("tx done with DebitCard");
}
}
- 现在重新设计购物中心这个高级类,他也是去依赖这个抽象,而不是直接低级模块的实现类
public class ShoppingMall {
private BankCard bankCard;
public ShoppingMall(BankCard bankCard) {
this.bankCard = bankCard;
}
public void doPayment(Object order, int amount){
bankCard.doTransaction(amount);
}
public static void main(String[] args) {
BankCard bankCard=new CreditCard();
ShoppingMall shoppingMall1=new ShoppingMall(bankCard);
shoppingMall1.doPayment("do some order", 10000);
}
}
现在,如果您观察购物中心与 BankCard
松散耦合,任何类型的卡处理支付都不会产生任何影响,这就是符合依赖倒置原则的。
总结
我们再来回顾总结下SOLID原则,
单一职责原则:每个类应该负责系统的单个部分或功能。
开闭原则:软件组件应该对扩展开放,而不是对修改开放。
里式替换原则:超类的对象应该可以用其子类的对象替换而不破坏系统。
接口隔离原则不应强迫客户端依赖于它不使用的方法。
依赖倒置原则:高层模块不应该依赖低层模块,两者都应该依赖抽象。
这些原则看起来都很简单,但用起来用的好就比较难了,希望大家在平时的开发的过程中多多思考、多多实践。
欢迎关注个人公众号【JAVA旭阳】交流学习
【架构设计】你真的理解软件设计中的SOLID原则吗?的更多相关文章
- SLAM+语音机器人DIY系列:(四)差分底盘设计——2.stm32主控软件设计
摘要 运动底盘是移动机器人的重要组成部分,不像激光雷达.IMU.麦克风.音响.摄像头这些通用部件可以直接买到,很难买到通用的底盘.一方面是因为底盘的尺寸结构和参数是要与具体机器人匹配的:另一方面是因为 ...
- TypeScript 中的 SOLID 原则
下面的文章解释了正确使用 TypeScrip的 SOLID原则. 原文地址:https://samueleresca.net/2016/08/solid-principles-using-typesc ...
- (转载)你真的理解Android AIDL中的in,out,inout么?
前言 这其实是一个很小的知识点,大部分人在使用AIDL的过程中也基本没有因为这个出现过错误,正因为它小,所以在大部分的网上关于AIDL的文章中,它都被忽视了——或者并没有,但所占篇幅甚小,且基本上都是 ...
- SLAM+语音机器人DIY系列:(四)差分底盘设计——1.stm32主控硬件设计
摘要 运动底盘是移动机器人的重要组成部分,不像激光雷达.IMU.麦克风.音响.摄像头这些通用部件可以直接买到,很难买到通用的底盘.一方面是因为底盘的尺寸结构和参数是要与具体机器人匹配的:另一方面是因为 ...
- 我的敏捷、需求分析、UML、软件设计电子书 - 下载(持续更新中)
我将所有我的电子书汇总在一起,方便大家下载!(持续更新) 文档保存在我的网站——软件知识原创基地上(www.umlonline.org),请放心下载. 1)软件设计是怎样炼成的?(2014-4-1 发 ...
- 13 JSP、MVC开发模式、EL表达式和JSPL标签+软件设计架构---学习笔记
1.JSP (1)JSP概念:Java Server Pages 即java服务器端页面可以理解为:一个特殊的页面,其中既可以指定定义html标签,又可以定义java代码用于简化书写!!! (2)原理 ...
- ABSD 基于架构的软件设计方法方法简介(摘抄)
ABSD(Architecture-Based Software Design)基于架构的软件设计方法 有三个基础: 第一个基础是功能分解.在功能分解中,ABSD方法使用已有的基于模块的内聚和耦合技术 ...
- 最简单直接地理解Java软件设计原则之开闭原则
写在前面 本文属于Java软件设计原则系列文章的其中一篇,后续会继续分享其他的原则.想以最简单的方式,最直观的demo去彻底理解设计原则.文章属于个人整理.也欢迎大家提出不同的想法. 首先是一些理论性 ...
- 多核片上系统(SoC)架构的嵌入式DSP软件设计
多核片上系统(SoC)架构的嵌入式DSP软件设计 Multicore a System-on-a-Chip (SoC) Architecture SoCs的软件开发涉及到基于最强大的计算模型在各种处理 ...
- MVC:开发模式&&三层架构:软件设计架构
MVC:开发模式 jsp演变历史 早期只有servlet,只能使用response输出标签数据,非常麻烦 后来又jsp,简化了Servlet的开发,如果过度使用jsp,在jsp中即写大量的java代码 ...
随机推荐
- windows和虚拟机上的Ubuntu互传文件
1.简介 本文讲述的是通过ssh登录虚拟机上的Ubuntu系统,实现互传文件 2.Ubuntu端 2.1.安装ssh sudo apt-get update sudo apt-get install ...
- Codeforces Round #827 (Div. 4) A-G
比赛链接 A 题解 知识点:模拟. 时间复杂度 \(O(1)\) 空间复杂度 \(O(1)\) 代码 #include <bits/stdc++.h> #define ll long lo ...
- Git 02: git管理码云代码仓库 + IDEA集成使用git
Git项目搭建 创建工作目录与常用指令 工作目录(WorkSpace)一般就是你希望Git帮助你管理的文件夹,可以是你项目的目录,也可以是一个空目录,建议不要有中文. 日常使用只要记住下图6个命令: ...
- [苹果APP上架]ios App Store上架详细教程-一条龙顺滑上架-适合小白
如何在 2022 年将您的应用提交到 App Store 您正在启动您的第一个应用程序,或者距离上次已经有一段时间了.作者纸飞机@cheng716051来给你讲讲将应用程序提交到 App Store ...
- PageRank原理分析
pagerank是将众多网页看成一个有向图,每个页面就是有向图中的节点.计算每个节点的出度和入度.如果一个网站被大量其他的网页引用,那么他就会有更高的pr分数. 原理 对于所有与节点i相连的节点,用他 ...
- 云原生之旅 - 7)部署Terrform基础设施代码的自动化利器 Atlantis
前言 前面有几篇文章讲述了如何使用Terraform创建资源 (基础设施即代码 Terraform 快速入门, 使用 Terraform 创建 Kubernetes) 以及 Kubernetes时代的 ...
- Go语言正/反向代理的姿势
先重温一下什么叫反向代理,正向代理. 鹅厂二面,nginx回忆录 所谓正向,反向代理取决于代理的是出站请求,还是入站请求. 正向代理: 代理的出站请求, 客户端能感知到代理程序,架构上距离客户端更近. ...
- javax.script.ScriptException: Cannot find engine named: 'nashorn', ensure you set language field in JSR223 Test Element: JSR223 预处理程序
jmeter运行脚本报错,跟java版本有关,做个记录. 1. 问题记录: 执行登录接口测试,登录失败.点击jmeter右上角[黄色!],查看错误日志.显示如下: 2022-09-23 10:29:5 ...
- 【Virt.Contest】CF1321(div.2)
第一次打虚拟赛. CF 传送门 T1:Contest for Robots 统计 \(r[i]=1\) 且 \(b[i]=0\) 的位数 \(t1\) 和 \(r[i]=0\) 且 \(b[i]=1\ ...
- 安装harbor仓库
1.安装docker-compose curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-c ...