在大學的時候,學校一般只會教你你寫程式語言,比如C、C++、JAVA等程式語言。但是當你離開大學進入這個行業開始工作時,才知道程式設計不只是知道程式語言、語法等,要想寫好程式碼,必須還要了解一些程式設計原則才行。本文主要討論KISS
、DRY
和SOLID
這些常見的程式設計原則,而且你會發現隨著工作時間越久,越能感受這些程式設計原則的精妙之處,歷久彌香。
Keep It Simple, Stupid!
你是不是有過接手同事的程式碼感到十分頭疼的經歷,明明可以有更加簡單、明白的寫法,非要繞來繞去,看不明白?
其實,我們在寫程式碼的時候應該要遵守KISS
原則,核心思想就是儘量保持簡單。程式碼的可讀性和可維護性是衡量程式碼質量非常重要的兩個標準。而 KISS
原則就是保持程式碼可讀和可維護的重要手段。程式碼足夠簡單,也就意味著很容易讀懂,bug 比較難隱藏。即便出現 bug,修復起來也比較簡單。
我們寫程式碼的的時候要站在別人的角度出發,就像馬丁·福勒說的,我們寫的程式碼不是給機器看的,而是給人看的。
「任何傻瓜都可以編寫計算機可以理解的程式碼。優秀的程式設計師編寫出人類可以理解的程式碼。」 — 馬丁·福勒
那麼如何才能寫出滿足KISS原則的程式碼呢?
我們直接上例子,下面的校驗IP是否合法的3種實現方式,大家覺得哪個最KISS
?
KISS
原則的設計初衷上來講,這種實現方式並不符合 KISS
原則。StringUtils
類、Integer
類提供的一些現成的工具函數,來處理 IP地址字串,邏輯清晰,可讀性好。所以說,符合KISS
原則的程式碼並不是程式碼越少越好,還要考慮程式碼是否邏輯清晰、是否容易理解、是否夠穩定。
總結以下如何寫出KISS
原則的程式碼:
bug
的概率會更高,維護的成本也比較高。 if-else
、使用一些過於底層的函數等)來優化程式碼,犧牲程式碼的可讀性。Don't Repeat Yourself
你是不是有過這樣的經歷,專案中很多重複邏輯的程式碼,然後修改一個地方,另外一個地方忘記修改,導致測試給你提了很多bug?
DRY
原則,英文全稱Don’t Repeat Yourself
,直譯過來就是不要重複你自己。這裡的重複不僅僅是程式碼一模一樣,還包括實現邏輯重複、功能語意重複、程式碼執行重複等。我們不要偷懶,有責任把這些存在重複的地方識別出來,然後優化它們。
我們直接上例子,程式碼重複的我就不講了,很好理解,關於實現邏輯或者功能語意重複的我覺個例子。
還是上面校驗IP的例子,團隊中兩個同事由於不知道就有了兩種寫法。
儘管兩段程式碼的實現邏輯不重複,但語意重複,也就是功能重複,我們認為它違反了 DRY
原則。我們應該在專案中,統一一種實現思路,所有用到判斷 IP
地址是否合法的地方,都統一呼叫同一個函數。不然哪天校驗規則變了,很容易只改了其中一個,另外一個漏改,就會出現莫名其妙的bug
。
其他的比如邏輯重複的意思是雖然功能是不一致的,但是裡面的邏輯都是一模一樣的。舉個例子,比如校驗使用者名稱和校驗密碼,雖然功能不一致,但是校驗邏輯都是相似,判空、字元長度等等,這種情況我們就需要把相似的邏輯抽取到一個方法中,不然也是不符合DRY
原則。
那麼我們平時寫程式碼注意些什麼才是符合DRY
原則呢?
其實最關鍵的就是寫程式碼帶腦子,用到一個方法先看看有沒有現成的,不要看看不看,就動手在那裡造輪子。
對於高度耦合的程式碼,當我們希望複用其中的一個功能,想把這個功能的程式碼抽取出來成為一個獨立的模組、類或者函數的時候,往往會發現牽一髮而動全身。移動一點程式碼,就要牽連到很多其他相關的程式碼。所以,高度耦合的程式碼會影響到程式碼的複用性,我們要儘量減少程式碼耦合。
我們前面講過,如果職責不夠單一,模組、類設計得大而全,那依賴它的程式碼或者它依賴的程式碼就會比較多,進而增加了程式碼的耦合。根據上一點,也就會影響到程式碼的複用性。相反,越細粒度的程式碼,程式碼的通用性會越好,越容易被複用。
這裡的「模組」,不單單指一組類構成的模組,還可以理解為單個類、函數。我們要善於將功能獨立的程式碼,封裝成模組。獨立的模組就像一塊一塊的積木,更加容易複用,可以直接拿來搭建更加複雜的系統。
越是跟業務無關的程式碼越是容易複用,越是針對特定業務的程式碼越難複用。所以,為了複用跟業務無關的程式碼,我們將業務和非業務邏輯程式碼分離,抽取成一些通用的框架、類庫、元件等。
從分層的角度來看,越底層的程式碼越通用、會被越多的模組呼叫,越應該設計得足夠可複用。一般情況下,在程式碼分層之後,為了避免交叉呼叫導致呼叫關係混亂,我們只允許上層程式碼呼叫下層程式碼及同層程式碼之間的呼叫,杜絕下層程式碼呼叫上層程式碼。所以,通用的程式碼我們儘量下沉到更下層。
在講物件導向特性的時候,我們講到,利用繼承,可以將公共的程式碼抽取到父類別,子類複用父類別的屬性和方法。利用多型,我們可以動態地替換一段程式碼的部分邏輯,讓這段程式碼可複用。除此之外,抽象和封裝,從更加廣義的層面、而非狹義的物件導向特性的層面來理解的話,越抽象、越不依賴具體的實現,越容易複用。程式碼封裝成模組,隱藏可變的細節、暴露不變的介面,就越容易複用。
一些設計模式,也能提高程式碼的複用性。比如,模板模式利用了多型來實現,可以靈活地替換其中的部分程式碼,整個流程模板程式碼可複用。
SOLID
原則不是一個單一的原則,而是對軟體開發至關重要的 5 條原則,遵循這些原則有助於我們寫出高內聚、低耦合、可延伸、可維護性好的程式碼。
一個類應該有一個,而且只有一個改變它的理由。
單一職責原則在我看來是最容易理解也是最重要的一個原則。它的核心思想就是一個模組、類或者方法只做一件事,只有一個職責,千萬不要越俎代庖。它可以帶來下面的好處:
舉個例子,我們有兩個類Person
和Account
。 兩者都負有儲存其特定資訊的單一責任。 如果要更改Person
的狀態,則無需修改類Account
,反之亦然, 不要把賬戶的行為比如修改賬戶名changeAcctName
寫在Person
類中。
public class Person {
private Long personId;
private String firstName;
private String lastName;
private String age;
private List<Account> accounts;
// 錯誤做法
public void changeAcctName(Account account, String acctName) {
acccount.setAccountName(acctName);
// 更新到資料庫
}
}
public class Account {
private Long guid;
private String accountNumber;
private String accountName;
private String status;
private String type;
}
所以大家在編寫程式碼的時候,一定要停頓思考下這個段程式碼真的寫在這裡嗎?另外很關鍵的一點是如果發現一個類或者一個方法十分龐大,那麼很有可能已經違背單一職責原則了,後續維護可想而知十分痛苦。
軟體實體(類、模組、函數等)應該對擴充套件開放,對修改關閉。
對擴充套件開放,對修改關閉,什麼意思?很簡單,其實就是我們要儘量通過新增類實現功能,而不是修改原有的類或者邏輯。因為修改已有程式碼很有可能對已有功能引入bug。
讓我們通過一個例子來理解這個原則,比如一個通知服務。
public class NotificationService {
public void sendOTP(String medium) {
if (medium.equals("email")) {
//email 傳送
} else if (medium.equals("mobile")) {
// 手機傳送
}
}
現在需要新增微信的方式通知,你要怎麼做呢? 是在加一個if else
嗎? 這樣就不符合開閉原則了,我們看下開閉原則該怎麼寫。
public interface NotificationService {
public void sendOTP();
}
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
}
}
這樣的方式就是遵循開閉原則的,你不用修改核心的業務邏輯,這樣可能帶來意向不到的後果,而是擴充套件實現方式,由呼叫方根據他們的實際情況呼叫。
是不是想到了設計模式中的策略模式,其實設計模式就是指導我們寫出高內聚、低耦合的程式碼。
派生類或子類必須可替代其基礎類別或父類別
這個原則稍微有點難以理解,它的核心思想是每個子類或派生類都應該可以替代/等效於它們的基礎類別或父類別。這樣有一個好處,就是無論子類是什麼型別,使用者端通過父類別呼叫都不會產生意外的後果。
理解不了?那我我們通過一個例子來理解一下。
讓我們考慮一下我有一個名為 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
。所以我們認為它是不符合裡式替換原則。
如果強行這麼做的話,會導致使用者端用父類別SocialMedia
呼叫,但是實現類注入的可能是個Weibo
的實現,呼叫groupVideoCall
行為,產生意想不到的後果。
那有什麼解決方案嗎?
那就把功能拆開唄。
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。
介面不應該強迫他們的客戶依賴它不使用的方法。
大家可以看看自己的工程,是不是一個介面類中有很多很多的介面,每次呼叫API
方法的時候IDE
工具給你彈出一大堆,十分的"臃腫肥胖"。所以該原則的核心思想要將你的介面拆小,拆細,打破」胖介面「,不用強迫使用者端實現他們不需要的介面。是不是和單一職責原則有點像?
例如,假設有一個名為 UPIPayment
的介面,如下所示
public interface UPIPayments {
public void payMoney();
public void getScratchCard();
public void getCashBackAsCreditBalance();
}
現在讓我們談談 UPIPayments
的一些實現,比如 Google Pay
和 AliPay
。
Google Pay
支援這些功能所以他可以直接實現這個 UPIPayments
但 AliPay
不支援 getCashBackAsCreditBalance()
功能所以這裡我們不應該強制使用者端 AliPay
通過實現 UPIPayments
來覆蓋這個方法。
我們需要根據客戶需要分離介面,所以為了滿足介面隔離原則,我們可以如下設計:
public interface CashbackManager{
public void getCashBackAsCreditBalance();
}
現在我們可以從 UPIPayments
介面中刪除getCashBackAsCreditBalance
,AliPay
也不需要實現getCashBackAsCreditBalance()
這個它沒有的方法了。
高層模組不應該依賴低層模組,兩者都應該依賴於抽象(介面)。抽象不應該依賴於細節(具體實現),細節應該取決於抽象。
這個原則我覺得也不是很好理解,所謂高層模組和低層模組的劃分,簡單來說就是,在呼叫鏈上,呼叫者屬於高層,被呼叫者屬於低層。比如大家都知道的MVC模式,controller
是呼叫service
層介面這個抽象,而不是實現類。這也是我們經常說的要面向介面程式設計,而非細節或者具體實現,因為介面意味著契約,更加穩定。
我們通過一個例子加深一下理解。
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);
}
}
我們還可以拿 Tomcat
這個 Servlet
容器作為例子來解釋一下。
Tomcat
是執行 Java Web
應用程式的容器。我們編寫的 Web
應用程式程式碼只需要部署在Tomcat
容器下,便可以被 Tomcat
容器呼叫執行。按照之前的劃分原則,Tomcat
就是高層模組,我們編寫的 Web
應用程式程式碼就是低層模組。Tomcat
和應用程式程式碼之間並沒有直接的依賴關係,兩者都依賴同一個「抽象」,也就是 Sevlet
規範。Servlet
規範不依賴具體的 Tomcat
容器和應用程式的實現細節,而 Tomcat
容器和應用程式依賴 Servlet
規範。
本文總結了軟體程式設計中的黃金原則,KISS
原則,DRY
原則,SOLID
原則。這些原則不僅僅適用於程式設計,也可以指導我們在架構設計上。雖然其中有些原則很抽象,但是大家多多實踐和思考,會體會到這些原則的精妙。
歡迎關注個人公眾號【JAVA旭陽】交流學習
本文來自部落格園,作者:JAVA旭陽,轉載請註明原文連結:https://www.cnblogs.com/alvinscript/p/17433913.html