SOLID原则之Java实战
本文翻译自国外论坛 medium,原文地址:salithachathuranga94.medium.com/solid-princ…
本文将带领大家学习在日常编程中如何使用 SOLID 原则。
如果你是一名优秀的编程人员,那么我要讨论的内容应该是一个众所周知的话题!废话不多说,让我们进入主题。
SOLID 原则由 Robert C. Martin 提出。它们是创建更易于维护、更易于理解和更灵活的软件代码的设计原则。这些经验法则帮助我们以更少的复杂性来编写我们的项目代码。SOLID 原则五个单词含义如下:
- 单一职责原则 (SRP) [ S ]
- 开闭原理 (OCP) [ O ]
- 里氏替换原理 (LSP) [ L ]
- 接口隔离原则 (ISP) [ I ]
- 依赖倒置原则 (DIP) [ D
现在我们用实际例子来一一列举。
单一职责原则(SRP)
该原则规定每个 Java 类必须执行单一功能。在这里单一功能意味着:类必须执行只属于该类的操作。
假设我们有一个名为 BankService 的类。在应用单一职责原则(SRP)之前会是这样的。存款、取款、发送通知、打印存折等所有操作均由 BankService 完成。这样 BankService 类就具有多个彼此不相关的职责。
public class BankService { public void withdraw(double amount) { System.out.println("Withdraw money : " + amount); } public void deposit(double amount) { System.out.println("Deposit money : " + amount); } public String getLoanInfo(String loanType) { if (loanType.equals("professional")) { return "Professional Loan"; } else if (loanType.equals("home")) { return "Home Loan"; } else { return "Personal Loan"; } } public void printPassbook() { System.out.println("Printing Book Details..."); } public void sendOTP(String medium) { if (medium.equals("mobile")) { System.out.println("Sending OTP to mobile"); } else if (medium.equals("email")) { System.out.println("Sending OTP to email"); } else { System.out.println("Not a valid medium"); } } }
让我们将单一职责原则(SRP)应用于 BankService。我们可以将职责划分为多组服务。
// BankService public class BankService { public void withdraw(double amount) { System.out.println("Withdraw money : " + amount); } public void deposit(double amount) { System.out.println("Deposit money : " + amount); } } // LoanService public class LoanService { public String getLoanInfo(String loanType) { if (loanType.equals("professional")) { return "Professional Loan"; } else if (loanType.equals("home")) { return "Home Loan"; } else { return "Personal Loan"; } } } // PrinterService public class PrinterService { public void printPassbook() { System.out.println("Printing Book Details..."); } } // NotificationService public class NotificationService { public void sendOTP(String medium) { if (medium.equals("mobile")) { System.out.println("Sending OTP to mobile"); } else if (medium.equals("email")) { System.out.println("Sending OTP to email"); } else { System.out.println("Not a valid medium"); } } }
现在你可以看到每个类都在执行自己的操作。这样代码看起来更加清晰易懂。这就是单一职责原则(SRP)的意义!
开闭原则(OCP)
该原则规定,当有新需求到来,模块应该对扩展开放,对修改关闭。这样我们应该就能够在现有代码的基础上添加扩展,而不改变原来的基本实现,这使得我们更容易扩展逻辑。
假设我们有一个名为 NotificationService 的服务可以向各种媒介发送通知。我们有两种发送 OTP 通知的方法。他们是手机和电子邮件。如果出现通过 WhatsApp 发送 OTP 通知的新要求,会发生什么情况。想象一下,我们应该做什么?我们只能修改原来 NotificationService 的代码!这就违反了开闭原则(OCP)!
让我们将开闭原则(OCP)应用到这个场景中。我将重新实现这个 NotificationService 类,准确来说 NotificationService 应该是一个接口。然后我将在另外 3 个服务中实现该接口,分别为 MobileNotificationService、EmailNotificationService 和 WhatsAppNotificationService。
public interface NotificationService { void sendOTP(String medium); void sendTransactionHistory(String medium); } public class MobileNotificationService implements NotificationService { @Override public void sendOTP(String medium) { System.out.println("Sending OTP Number Message to: " + medium); } @Override public void sendTransactionHistory(String medium) { System.out.println("Sending Transactions Message to: " + medium); } } public class EmailNotificationService implements NotificationService { @Override public void sendOTP(String medium) { System.out.println("Sending OTP Number Email to: " + medium); } @Override public void sendTransactionHistory(String medium) { System.out.println("Sending Transactions Email to: " + medium); } } public class WhatsAppNotificationService implements NotificationService { @Override public void sendOTP(String medium) { System.out.println("Sending OTP Number to: " + medium); } @Override public void sendTransactionHistory(String medium) { System.out.println("Sending Transactions Details to: " + medium); } }
如果我想发送所有 3 种类型的通知,我可以这样做。
如果又有另一种类型的新媒介需要发送那会发生什么?我们只需要创建另一个服务,从 NotificationService 实现它并完成与新媒介相关的逻辑就行!
这就是所有代码!我们已经成功应用开闭原则(OCP)。
里氏替换原则(LSP)
据说这是大多数开发人员最难理解的原则。这是由芭芭拉·利斯科夫(Barbara Liskov)介绍的。它适用于继承,子类必须完全可替换其父类。
如果类 A 是类 B 的子类型,那么我们应该能够在不中断程序行为的情况下用 A 替换 B。
让我们通过一个例子来理解这一点,但是我要提醒你,这个原则会让文章变得很长。🤔
假设我们要管理多种类型的社交媒体平台。它们是 Facebook、Instagram 和 WhatsApp。
因此,SocialMedia 类有 3 个方法,分别称为 chat()、publish() 和 groupCall()。为了展现里氏替换原则(LSP)的作用,我将直接用 Facebook、Instagram、WhatsApp 3 个类来实现这一点。
public abstract class SocialMedia { abstract void chat(String user); abstract void publish(Object post); abstract void groupCall(String... users); } public class Facebook extends SocialMedia { @Override void chat(String user) { } @Override void publish(Object post) { } @Override void groupCall(String... users) { } } public class Instagram extends SocialMedia { @Override void chat(String user) { } @Override void publish(Object post) { } @Override void groupCall(String... users) { } } public class WhatsApp extends SocialMedia { @Override void chat(String user) { } @Override void publish(Object post) { } @Override void groupCall(String... users) { } }
SocialMedia 是一个抽象类,所有其他平台都是它的子类。预期的逻辑现已实现。那么这里出了什么问题呢?
你是否注意到,这样做的话,所有平台都必须实现这三个方法,即使该平台不支持此方法!让我解释一下
Facebook:支持所有方法 chat()、publish() 和 groupCall()。
WhatsApp:不支持发布帖子 publish()。
Instagram:不支持群组通话 groupCall()。
所以在这种情况下,Instagram 或 WhatsApp 不能完全替代 SocialMedia 类!
现在让我们在这里应用里氏替换原则(LSP)。
public interface SocialMedia { void chat(String user); } public interface PostManager { void publish(Object post); } public interface VideoCallManager { void groupCall(String... users); } public class Facebook implements SocialMedia, PostManager { @Override public void publish(Object post) { System.out.println("Publishing a post on Facebook: " + post); } @Override public void chat(String user) { System.out.println("Chatting on Facebook with: " + user); } } public class WhatsApp implements SocialMedia, VideoCallManager { @Override public void chat(String user) { System.out.println("Chatting on WhatsApp with: " + user); } @Override public void groupCall(String... users) { System.out.println("Taking a Group Call on WhatsApp with: " + Arrays.toString(users)); } }
你现在可以看到 SocialMedia 是一个具有单一职责的接口,包含 chat() 方法。因此我们有单独的界面来管理视频通话和发布帖子。这样所有子类:Facebook 和 WhatsApp 都会执行它们只能执行的操作!
- WhatsApp 是 SocialMedia 和 VideoCallManager 的子类
- Facebook 是 SocialMedia 和 PostManager 的子类
让我们检查一下定义:
如果 WhatsApp 类是 SocialMedia 类的子类,那么我们应该能够在不中断程序行为的情况下用 WhatsApp 替换 SocialMedia。
这就是里氏替换原则(LSP)的全部内容!!!讲解完了!
接口隔离原则(ISP)
该原则指出较大的接口分为较小的接口。因为实现类只使用需要的方法,我们不应该强迫客户使用他们不想使用的方法。
这也有点类似于单一责任原则。正确的应用程序设计和正确的抽象是接口隔离原则背后的关键。
让我们举个例子。
我们有一个支付接口来代表所有类型的支付。 BankPayment 和 LoanPayment 是 Payment 中实现类。
public interface Payment { void init(); Object status(); List getPayments(); } public class LoanPayment implements Payment { @Override public void init() { System.out.println("Initiate LoanPayment..."); } @Override public Object status() { return "LoanPayment Status"; } @Override public List getPayments() { return Arrays.asList("LoanPayment1", "LoanPayment2"); } } public class BankPayment implements Payment { @Override public void init() { System.out.println("Initiate BankPayment..."); } @Override public Object status() { return "BankPayment Status"; } @Override public List getPayments() { return Arrays.asList("BankPayment1", "BankPayment2"); } }
想象一下,我们收到一个新要求,需要向 Payment 接口添加一个方法。但是实际上这个方法只有 LoanPayment 类需要持有。那么我们就得修改 Payment 接口了。除了修改 LoanPayment 我们还得修改 BankPayment 类,因为 BankPayment 也实现了 Payment 接口。
新方法将返回付款期限:贷款 => 10 年
public interface Payment { void init(); Object status(); List getPayments(); int getTimePeriod(); } public class LoanPayment implements Payment { @Override public void init() { System.out.println("Initiate LoanPayment..."); } @Override public Object status() { return "LoanPayment Status"; } @Override public List getPayments() { return Arrays.asList("LoanPayment1", "LoanPayment2"); } public int getTimePeriod() { return 10; } } public class BankPayment implements Payment { @Override public void init() { System.out.println("Initiate BankPayment..."); } @Override public Object status() { return "BankPayment Status"; } @Override public List getPayments() { return Arrays.asList("BankPayment1", "BankPayment2"); } // not needed for BankPayment but we have to override public int getTimePeriod() { retun 0; } }
这样做不好。在这种情况下,一开始的代码设计就很重要了!让我们尝试在这里应用接口隔离原则(ISP)。
public interface Payment { void init(); Object status(); List getPayments(); } public interface Loan extends Payment { int getTimePeriod(); } public interface Bank extends Payment { int getOutstandingBalance(); } public class LoanPayment implements Loan { @Override public int getTimePeriod() { return 10; } @Override public void init() { System.out.println("Initiate LoanPayment..."); } @Override public Object status() { return "LoanPayment Status"; } @Override public List getPayments() { return Arrays.asList("LoanPayment1", "LoanPayment2"); } } public class BankPayment implements Bank { @Override public int getOutstandingBalance() { return 1000; } @Override public void init() { System.out.println("Initiate BankPayment..."); } @Override public Object status() { return "BankPayment Status"; } @Override public List getPayments() { return Arrays.asList("BankPayment1", "BankPayment2"); } }
这就是设计如何让我们的代码变得更好!我们已经彻底摆脱了 ISP 的困扰。我们使用了更多的接口,并全面地划分了职责。
如果出现与银行支付相关的新要求会怎样?
我们只需要修改 Bank 接口并在 BankPayment 类中重写它,不会影响其他类!
依赖倒置原则(DIP)
该原则指出我们必须使用抽象(抽象类和接口)而不是具体实现。高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
让我在这里用另一个例子来解释。
假设我们要创建一个购物场景,我们需要信用卡或借记卡来购买物品,让我们创建它们并进行购买!
public class CreditCard { public void doTransaction(double amount) { System.out.println("Transaction with CreditCard: " + amount); } } public class DebitCard { public void doTransaction(double amount) { System.out.println("Transaction with DebitCard: " + amount); } }
商场场景
public class ShoppingMall { private DebitCard debitCard; public ShoppingMall(DebitCard debitCard) { this.debitCard = debitCard; } public void purchase(double amount) { this.debitCard.doTransaction(amount); } public static void main(String[] args) { DebitCard debitCard = new DebitCard(); ShoppingMall shoppingMall = new ShoppingMall(debitCard); shoppingMall.purchase(10000); } }
你可以在这里看到,DebitCard 作为 ShoppingMall 类的依赖项紧密耦合。我们需要它来执行购买。那么如果有人没有借记卡会怎样?但他/她有信用卡!现在我们再次必须更改 ShoppingMall 类并绑定信用卡而不是借记卡。(代码设计很重要!)
让我们将依赖倒置原则(DIP)应用到购物中心 ShoppingMall。
public interface BankCard { void doTransaction(double amount); } public class DebitCard implements BankCard { @Override public void doTransaction(double amount) { System.out.println("Transaction with DebitCard: " + amount); } } public class CreditCard implements BankCard { @Override public void doTransaction(double amount) { System.out.println("Transaction with CreditCard: " + amount); } }
我们引入了一个接口作为两张卡的父级,一个 BankCard 接口。我们的新购物中心现在将能够使用任何银行卡,而不是与特定的卡类型紧密结合!请参阅下面的客户端代码。
public class ShoppingMall { private BankCard bankCard; public ShoppingMall(BankCard bankCard) { this.bankCard = bankCard; } public void purchase(double amount) { this.bankCard.doTransaction(amount); } public static void main(String[] args) { BankCard bankCard1 = new DebitCard(); BankCard bankCard2 = new CreditCard(); ShoppingMall shoppingMall = new ShoppingMall(bankCard1); // ShoppingMall shoppingMall = new ShoppingMall(bankCard2); shoppingMall.purchase(10000); } }
ShoppingMall 现在接受任何卡作为其依赖项。就这样我们实现了最后一个依赖倒置原则(DIP)。
总结
我希望本文我举的例子能够被大家理解,因为我尝试用熟悉的场景来讲解它们。当我第一次读到 SOLID 原则时,它对我来说也像希腊语(很难理解)。但后来我渐渐地理解了他们,我把我理解这些概念的方式也写在这里,所以这篇文章的内容非常丰富。最后,感谢大家阅读。