您現在的位置是:首頁 > 運動
25種程式碼壞味道總結+最佳化例項!
firstname是什麼意思
前言
什麼樣的程式碼是好程式碼呢?好的程式碼應該命名規範、可讀性強、擴充套件性強、健壯性……而不好的程式碼又有哪些典型特徵呢?這25種程式碼壞味道大家要注意啦
1。 Duplicated Code (重複程式碼)
重複程式碼就是
不同地點,有著相同的程式結構
。一般是因為需求迭代比較快,開發小夥伴擔心影響已有功能,就複製貼上造成的。重複程式碼
很難維護
的,如果你要修改其中一段的程式碼邏輯,就需要修改多次,很可能出現遺漏的情況。
如何最佳化重複程式碼呢?分
三種
情況討論:
同一個類的兩個函式含有相同的表示式
class A {
public void
method1
() {
doSomething1
doSomething2
doSomething3
}
public void
method2
() {
doSomething1
doSomething2
doSomething4
}
}
最佳化手段:可以使用
Extract Method(提取公共函式)
抽出重複的程式碼邏輯,組成一個公用的方法。
class A {
public void
method1
() {
commonMethod();
doSomething3
}
public void
method2
() {
commonMethod();
doSomething4
}
public void
commonMethod
(){
doSomething1
doSomething2
}
}
兩個互為兄弟的子類內含相同的表示式
class A extend C {
public void
method1
() {
doSomething1
doSomething2
doSomething3
}
}
class B extend C {
public void
method1
() {
doSomething1
doSomething2
doSomething4
}
}
最佳化手段:對兩個類都使用
Extract Method(提取公共函式)
,然後把
抽取出來的函式放到父類
中。
class C {
public void
commonMethod
(){
doSomething1
doSomething2
}
}
class A extend C {
public void
method1
() {
commonMethod();
doSomething3
}
}
class B extend C {
public void
method1
() {
commonMethod();
doSomething4
}
}
兩個毫不相關的類出現重複程式碼
如果是兩個毫不相關的類出現重複程式碼,可以使用
Extract Class
將重複程式碼提煉到一個類中。這個新類可以是一個普通類,也可以是一個工具類,看具體業務怎麼劃分吧。
2 。Long Method (長函式)
長函式是指一個函式方法幾百行甚至上千行,可讀性大大降低,不便於理解。
反例如下:
public class Test {
private String name;
private Vector
public void
printOwing
() {
//
banner
System。out。println(
“****************”
);
System。out。println(
“*****customer Owes *****”
);
System。out。println(
“****************”
);
//calculate totalAmount
Enumeration env = orders。elements();
double totalAmount = 0。0;
while
(env。hasMoreElements()) {
Order order = (Order) env。nextElement();
totalAmount += order。getAmout();
}
//
details
System。out。println(
“name:”
+ name);
System。out。println(
“amount:”
+ totalAmount);
……
}
}
可以使用
Extract Method
,抽取功能單一的程式碼段,組成命名清晰的小函式,去解決長函式問題,
正例如下
:
public class Test {
private String name;
private Vector
public void
printOwing
() {
//
banner
printBanner();
//calculate totalAmount
double totalAmount = getTotalAmount();
//
details
printDetail(totalAmount);
}
void
printBanner
(){
System。out。println(
“****************”
);
System。out。println(
“*****customer Owes *****”
);
System。out。println(
“****************”
);
}
double
getTotalAmount
(){
Enumeration env = orders。elements();
double totalAmount = 0。0;
while
(env。hasMoreElements()) {
Order order = (Order) env。nextElement();
totalAmount += order。getAmout();
}
return
totalAmount;
}
void printDetail(double totalAmount){
System。out。println(
“name:”
+ name);
System。out。println(
“amount:”
+ totalAmount);
}
}
3。 Large Class (過大的類)
一個類做太多事情,維護了太多功能,可讀性變差,效能也會下降。舉個例子,訂單相關的功能你放到一個類A裡面,商品庫存相關的也放在類A裡面,積分相關的還放在類A裡面。。
。反例
如下:
Class A{
public void
printOrder
(){
System。out。println(
“訂單”
);
}
public void
printGoods
(){
System。out。println(
“商品”
);
}
public void
printPoints
(){
System。out。println(
“積分”
);
}
}
試想一下,亂七八糟的程式碼塊都往一個類裡面塞,還談啥可讀性。應該按單一職責,使用
Extract Class
把程式碼劃分開,正例如下:
Class Order{
public void
printOrder
(){
System。out。println(
“訂單”
);
}
}
Class Goods{
public void
printGoods
(){
System。out。println(
“商品”
);
}
}
Class Points{
public void
printPoints
(){
System。out。println(
“積分”
);
}
}
}
4。 Long Parameter List (過長引數列)
方法引數數量過多的話,可讀性很差。如果有多個過載方法,引數很多的話,有時候你都不知道調哪個呢。並且,如果引數很多,做新老介面相容處理也比較麻煩。
public void getUserInfo(String name,String age,String sex,String mobile){
//
do
something 。。。
}
如何解決過長引數列問題呢?將引數封裝成結構或者類,比如我們將引數封裝成一個DTO類,如下:
public void getUserInfo(UserInfoParamDTO userInfoParamDTO){
//
do
something 。。。
}
class UserInfoParamDTO{
private String name;
private String age;
private String sex;
private String mobile;
}
5。 Divergent Change (發散式變化)
對程式進行維護時,
如果新增修改元件, 要同時修改一個類中的多個方法
, 那麼這就是 Divergent Change。舉個汽車的例子,某個汽車廠商生產三種品牌的汽車:BMW、Benz和LaoSiLaiSi,每種品牌又可以選擇燃油、純電和混合動力。
反例如下
:
/**
* 公眾號:撿田螺的小男孩
*/
public class Car {
private String name;
void start(Engine engine) {
if
(
“HybridEngine”
。equals(engine。getName())) {
System。out。println(
“Start Hybrid Engine。。。”
);
}
else
if
(
“GasolineEngine”
。equals(engine。getName())) {
System。out。println(
“Start Gasoline Engine。。。”
);
}
else
if
(
“ElectricEngine”
。equals(engine。getName())) {
System。out。println(
“Start Electric Engine”
);
}
}
void drive(Engine engine,Car car) {
this。start(engine);
System。out。println(
“Drive ”
+ getBrand(car) +
“ car。。。”
);
}
String getBrand(Car car) {
if
(
“Baoma”
。equals(car。getName())) {
return
“BMW”
;
}
else
if
(
“BenChi”
。equals(car。getName())) {
return
“Benz”
;
}
else
if
(
“LaoSiLaiSi”
。equals(car。getName())) {
return
“LaoSiLaiSi”
;
}
return
null;
}
}
如果新增一種品牌新能源電車,然後它的啟動引擎是核動力呢,那麼就需要修改Car類的
start
和
getBrand
方法啦,這就是程式碼壞味道:
Divergent Change (發散式變化)
。
如何最佳化呢?一句話總結:
拆分類,將總是一起變化的東西放到一塊
。
“
運用提煉類(Extract Class) 拆分類的行為。
如果不同的類有相同的行為,提煉超類(Extract Superclass) 和 提煉子類(Extract Subclass)。
”
正例如下:
因為Engine是獨立變化的,所以提取一個Engine介面,如果新加一個啟動引擎,多一個實現類即可。如下:
//IEngine
public interface IEngine {
void start();
}
public class HybridEngineImpl implements IEngine {
@Override
public void
start
() {
System。out。println(
“Start Hybrid Engine。。。”
);
}
}
因為
drive
方法依賴於
Car,IEngine,getBand
方法;
getBand
方法是變化的,也跟Car是有關聯的,所以可以搞個抽象Car的類,每個品牌汽車繼承於它即可,如下
public abstract class AbstractCar {
protected IEngine engine;
public AbstractCar(IEngine engine) {
this。engine = engine;
}
public abstract void drive();
}
//賓士汽車
public class BenzCar extends AbstractCar {
public BenzCar(IEngine engine) {
super(engine);
}
@Override
public void
drive
() {
this。engine。start();
System。out。println(
“Drive ”
+ getBrand() +
“ car。。。”
);
}
private String
getBrand
() {
return
“Benz”
;
}
}
//寶馬汽車
public class BaoMaCar extends AbstractCar {
public BaoMaCar(IEngine engine) {
super(engine);
}
@Override
public void
drive
() {
this。engine。start();
System。out。println(
“Drive ”
+ getBrand() +
“ car。。。”
);
}
private String
getBrand
() {
return
“BMW”
;
}
}
細心的小夥伴,可以發現不同子類BaoMaCar和BenzCar的
drive
方法,還是有相同程式碼,所以我們可以再擴充套件一個抽象子類,把
drive
方法推進去,如下:
public abstract class AbstractRefinedCar extends AbstractCar {
public AbstractRefinedCar(IEngine engine) {
super(engine);
}
@Override
public void
drive
() {
this。engine。start();
System。out。println(
“Drive ”
+ getBrand() +
“ car。。。”
);
}
abstract String getBrand();
}
//寶馬
public class BaoMaRefinedCar extends AbstractRefinedCar {
public BaoMaRefinedCar(IEngine engine) {
super(engine);
}
@Override
String
getBrand
() {
return
“BMW”
;
}
}
如果再新增一個新品牌,搞個子類,繼承
AbstractRefinedCar
即可,如果新增一種啟動引擎,也是搞個類實現
IEngine
介面即可
6。 Shotgun Surgery(散彈式修改)
當你實現某個小功能時,你需要在很多不同的類做出小修改。這就是
Shotgun Surgery(散彈式修改)
。它跟
發散式變化(Divergent Change)
的區別就是,它指的是同時對多個類進行單一的修改,發散式變化指在一個類中修改多處。
反例如下
:
public class DbAUtils {
@Value(
“
${db。mysql。url}
”
)
private String mysqlDbUrl;
。。。
}
public class DbBUtils {
@Value(
“
${db。mysql。url}
”
)
private String mysqlDbUrl;
。。。
}
多個類使用了
db。mysql。url
這個變數,如果將來需要切換
mysql
到別的資料庫,如
Oracle
,那就需要修改多個類的這個變數!
如何最佳化呢?將各個修改點,集中到一起,抽象成一個新類。
“
可以使用 Move Method (搬移函式)和 Move Field (搬移欄位)把所有需要修改的程式碼放進同一個類,如果沒有合適的類,就去new一個。
”
正例如下:
public class DbUtils {
@Value(
“
${db。mysql。url}
”
)
private String mysqlDbUrl;
。。。
}
7。 Feature Envy (依戀情節)
某個函式為了計算某個值,從另一個物件那裡呼叫幾乎半打的取值函式。通俗點講,就是一個函式使用了大量其他類的成員,有人稱之為
紅杏出牆的函式
。反例如下:
public class User{
private Phone phone;
public User(Phone phone){
this。phone = phone;
}
public void getFullPhoneNumber(Phone phone){
System。out。println(
“areaCode:”
+ phone。getAreaCode());
System。out。println(
“prefix:”
+ phone。getPrefix());
System。out。println(
“number:”
+ phone。getNumber());
}
}
如何解決呢?在這種情況下,你可以考慮將這個方法移動到它使用的那個類中。例如,要將
getFullPhoneNumber()
從
User
類移動到
Phone
類中,因為它呼叫了
Phone
類的很多方法。
8。 Data Clumps(資料泥團)
資料項就像小孩子,喜歡成群結隊地呆在一塊。如果一些資料項總是一起出現的,並且一起出現更有意義的,就可以考慮,按資料的業務含義來封裝成資料物件。反例如下:
public class User {
private String firstName;
private String lastName;
private String province;
private String city;
private String area;
private String street;
}
正例:
public class User {
private UserName username;
private Adress adress;
}
class UserName{
private String firstName;
private String lastName;
}
class Address{
private String province;
private String city;
private String area;
private String street;
}
9。 Primitive Obsession (基本型別偏執)
多數程式設計環境都有兩種資料型別,
結構型別和基本型別
。這裡的基本型別,如果指Java語言的話,不僅僅包括那八大基本型別哈,也包括String等。如果是經常一起出現的基本型別,可以考慮把它們封裝成物件。我個人覺得它有點像
Data Clumps(資料泥團)
舉個反例如下:
// 訂單
public class Order {
private String customName;
private String address;
private Integer orderId;
private Integer price;
}
正例:
// 訂單類
public class Order {
private Custom custom;
private Integer orderId;
private Integer price;
}
// 把custom相關欄位封裝起來,在Order中引用Custom物件
public class Custom {
private String name;
private String address;
}
當然,這裡不是所有的基本型別,都建議封裝成物件,有關聯或者一起出現的,才這麼建議哈。
10。 Switch Statements (Switch 語句)
這裡的Switch語句,不僅包括
Switch
相關的語句,也包括多層
if。。。else
的語句哈。很多時候,switch語句的問題就在於重複,如果你為它新增一個新的case語句,就必須找到所有的switch語句並且修改它們。
示例程式碼如下:
String medalType =
“guest”
;
if
(
“guest”
。equals(medalType)) {
System。out。println(
“嘉賓勳章”
);
}
else
if
(
“vip”
。equals(medalType)) {
System。out。println(
“會員勳章”
);
}
else
if
(
“guard”
。equals(medalType)) {
System。out。println(
“守護勳章”
);
}
。。。
這種場景可以考慮使用多型最佳化:
//勳章介面
public interface IMedalService {
void showMedal();
}
//守護勳章策略實現類
public class GuardMedalServiceImpl implements IMedalService {
@Override
public void
showMedal
() {
System。out。println(
“展示守護勳章”
);
}
}
//嘉賓勳章策略實現類
public class GuestMedalServiceImpl implements IMedalService {
@Override
public void
showMedal
() {
System。out。println(
“嘉賓勳章”
);
}
}
//勳章服務工廠類
public class MedalServicesFactory {
private static final Map
static {
map。put(
“guard”
, new GuardMedalServiceImpl());
map。put(
“vip”
, new VipMedalServiceImpl());
map。put(
“guest”
, new GuestMedalServiceImpl());
}
public static IMedalService getMedalService(String medalType) {
return
map。get(medalType);
}
}
當然,多型只是最佳化的一個方案,一個方向。如果只是單一函式有些簡單選擇示例,並不建議動不動就使用動態,因為顯得有點殺雞使用牛刀了。
11。Parallel Inheritance Hierarchies( 平行繼承體系)
平行繼承體系 其實算是
Shotgun Surgery
的特殊情況啦。當你為A類的一個子類Ax,也必須為另一個類B相應的增加一個子類Bx。
解決方法
:遇到這種情況,就要消除兩個繼承體系之間的引用,有一個類是可以去掉繼承關係的。
12。 Lazy Class (冗贅類)
把這些不再重要的類裡面的邏輯,合併到相關類,刪掉舊的。一個比較常見的場景就是,假設系統已經有日期工具類
DateUtils
,有些小夥伴在開發中,需要用到日期轉化等,不管三七二十一,又自己實現一個新的日期工具類。
13。 Speculative Generality(誇誇其談未來性)
儘量避免過度設計的程式碼。例如:
只有一個if else,那就不需要班門弄斧使用多型;
如果某個抽象類沒有什麼太大的作用,就運用
Collapse Hierarchy
(摺疊繼承體系)
如果函式的某些引數沒用上,就移除。
14。 Temporary Field(令人迷惑的臨時欄位)
某個例項變數僅為某種特定情況而定而設,這樣的程式碼就讓人不易理解,我們稱之為
Temporary Field(令人迷惑的臨時欄位)
。反例如下:
public class PhoneAccount {
private double excessMinutesCharge;
private static final double RATE = 8。0;
public double computeBill(int minutesUsed, int includedMinutes) {
excessMinutesCharge = 0。0;
int excessMinutes = minutesUsed - includedMinutes;
if
(excessMinutes >= 1) {
excessMinutesCharge = excessMinutes * RATE;
}
return
excessMinutesCharge;
}
public double chargeForExcessMinutes(int minutesUsed, int includedMinutes) {
computeBill(minutesUsed, includedMinutes);
return
excessMinutesCharge;
}
}
思考一下,臨時欄位
excessMinutesCharge
是否多餘呢?
15。 Message Chains (過度耦合的訊息鏈)
當你看到使用者向一個物件請求另一個物件,然後再向後者請求另一個物件,然後再請求另一個物件。。。這就是訊息鏈。實際程式碼中,你看到的可能是一長串
getThis()
或一長串臨時變數。反例如下:
A。getB()。getC()。getD()。getTianLuoBoy()。getData();
A想要獲取需要的資料時,必須要知道B,又必須知道C,又必須知道D。。。其實A需要知道得太多啦,回頭想下
封裝性
,嘻嘻。其實可以透過拆函式或者移動函式解決,比如由B作為代理,搞個函式直接返回A需要資料。
16。 Middle Man (中間人)
物件的基本特徵之一就是封裝,即對外部世界隱藏其內部細節。封裝往往伴隨委託,過度運用委託就不好:某個類介面有一半的函式都委託給其他類。可以使用
Remove Middle Man
最佳化。反例如下:
A。B。
getC
(){
return
C。getC();
}
其實,A可以直接透過C去獲取C,而不需要透過B去獲取。
17。 Inappropriate Intimacy(狎暱關係)
如果兩個類過於親密,過分狎暱,你中有我,我中有你,兩個類彼此使用對方的私有的東西,就是一種壞程式碼味道。我們稱之為
Inappropriate Intimacy(狎暱關係)
建議儘量把有關聯的方法或屬性抽離出來,放到公共類,以減少關聯。
18。 Alternative Classes with Different Interfaces (異曲同工的類)
A類的介面a,和B類的介面b,做的的是相同一件事,或者類似的事情。我們就把A和B叫做異曲同工的類。
可以透過
重新命名,移動函式,或抽象子類
等方式最佳化
19。 Incomplete Library Class (不完美的類庫)
大多數物件只要夠用就好,如果類庫構造得不夠好,我們不可能修改其中的類使它完成我們希望完成的工作。可以醬紫:
包一層函式或包成新的類
。
20。 Data Class (純資料類)
什麼是Data Class? 它們擁有一些欄位,以及用於訪問(讀寫)這些欄位的函式。這些類很簡單,僅有公共成員變數,或簡單操作的函式。
如何最佳化呢?將
相關操作封裝進去,減少public成員變數
。比如:
如果擁有public欄位->
Encapsulate Field
如果這些類內含容器類的欄位,應該檢查它們是不是得到了恰當地封裝->
Encapsulate Collection
封裝起來
對於不該被其他類修改的欄位->
Remove Setting Method
->找出取值/設定函式被其他類運用的地點->
Move Method
把這些呼叫行為搬移到
Data Class
來。如果無法搬移整個函式,就運用
Extract Method
產生一個可被搬移的函式->
Hide Method
把這些取值/設定函式隱藏起來。
21。 Refused Bequest (被拒絕的饋贈)
子類應該繼承父類的資料和函式。子類繼承得到所有函式和資料,卻只使用了幾個,那就是
繼承體系設計錯誤
,需要最佳化。
需要為這個子類新建一個兄弟類->
Push Down Method
和
Push Down Field
把所有用不到的函式下推給兄弟類,這樣一來,超類就只持有所有子類共享的東西。所有超類都應該是抽象的。
如果子類複用了超類的實現,又不願意支援超類的介面,可以不以為然。但是不能胡亂修改繼承體系->
Replace Inheritance with Delegation
(用委派替換繼承)。
22。 Comments (過多的註釋)
這個點不是說程式碼不建議寫註釋哦,而是,建議大家
避免用註釋解釋程式碼,避免過多的註釋
。這些都是常見註釋的壞味道:
多餘的解釋
日誌式註釋
用註釋解釋變數等
。。。
如何最佳化呢?
方法函式、變數的
命名要規範、淺顯易懂
、避免用註釋解釋程式碼。
關鍵、複雜的業務
,使用
清晰、簡明
的註釋
23。 神奇命名
方法函式、變數、類名、模組等,都需要簡單明瞭,淺顯易懂。避免靠自己主觀意識瞎起名字。
反例:
boolean
test
= chenkParamResult(req);
正例:
boolean isParamPass = chenkParamResult(req);
24。 神奇魔法數
日常開發中,經常會遇到這種程式碼:
if
(userType==1){
//doSth1
}
else
If( userType ==2){
//doSth2
}
。。。
程式碼中的這個
1和2
都表示什麼意思呢?再比如
setStatus(1)
中的
1
又表示什麼意思呢?看到類似壞程式碼,可以這兩種方式最佳化:
新建個常量類
,把一些常量放進去,統一管理,並且寫好註釋;
建一個列舉類
,把相關的魔法數字放到一起管理。
25。 混亂的程式碼層次呼叫
我們程式碼一般會分
dao層
、
service層
和
controller層
。
dao層主要做資料持久層的工作,與資料庫打交道。
service層主要負責業務邏輯處理。
controller層負責具體的業務模組流程的控制。
所以一般就是
controller
呼叫
service
,
service
調
dao
。如果你在程式碼看到
controller
直接呼叫
dao
,那可以考慮是否最佳化啦。
反例如下
:
@RestController(
“user”
)
public class UserController {
Autowired
private UserDao userDao;
@RequestMapping(
“/queryUserInfo”
)
public String queryUserInfo(String userName) {
return
userDao。selectByUserName(userName);
}
}
參考與感謝
軟工實驗:常見的程式碼壞味道以及重構舉例[2]
22種程式碼的壞味道,一句話概括[3]
【重構】 程式碼的壞味道總結 Bad Smell[4]
Code Smell[5]
《重構改善既有程式碼的設計》
作者:
撿田螺的小男孩
推薦文章
- 背街老碼頭變身城市客廳 “西南最大環湖綠道”又添新意
其中規劃分為一道、六區、十二節點,即182米以上,擬建一條貫穿全境的綠道(腳踏車道、漫步道、跑步道)串聯自然體驗區、健康休閒區、雙井風情區、生態活力區、宜居水岸區、文化旅遊區等6個魅力分割槽和復興花海、月光草坪、濱江花園、陽光沙灘、飛鳳梯不...
- 2020臘八節祝福語,致我最在乎的人!
六、臘八節到了,送你一碗臘八粥...
- 郭德綱說:有些人吃不到狐狸,就說葡萄是酸的
二、有的人雖然喜歡吃葡萄,但葡萄架太高,自身又懶,不想去摘,所以討厭並阻止每一個試圖努力得到葡萄的人...