代碼整潔的-Java
Github地址: https://github.com/junbin1011/clean-code-java
目錄
簡(jiǎn)介
將源自 Robert C. Martin 的 Clean Code
的軟件工程原則適配到 Java 。 這不是一個(gè)代碼風(fēng)格指南, 它是一個(gè)使用 Java 來生產(chǎn)
可讀的, 可重用的, 以及可重構(gòu)的軟件的指南。
這里的每一項(xiàng)原則都不是必須遵守的, 甚至只有更少的能夠被廣泛認(rèn)可。 這些僅僅是指南而已, 但是卻是
Clean Code 作者多年經(jīng)驗(yàn)的結(jié)晶。
我們的軟件工程行業(yè)只有短短的 50 年, 依然有很多要我們?nèi)W(xué)習(xí)。 當(dāng)軟件架構(gòu)與建筑架構(gòu)一樣古老時(shí),
也許我們將會(huì)有硬性的規(guī)則去遵守。 而現(xiàn)在, 讓這些指南做為你和你的團(tuán)隊(duì)生產(chǎn)的 Java 代碼的
質(zhì)量的標(biāo)準(zhǔn)。
還有一件事: 知道這些指南并不能馬上讓你成為一個(gè)更加出色的軟件開發(fā)者, 并且使用它們工作多年也并
不意味著你不再會(huì)犯錯(cuò)誤。 每一段代碼最開始都是草稿, 像濕粘土一樣被打造成最終的形態(tài)。 最后當(dāng)我們
和搭檔們一起審查代碼時(shí)清除那些不完善之處, 不要因?yàn)樽畛跣枰纳频牟莞宕a而自責(zé), 而是對(duì)那些代
碼下手。
變量
使用有意義并且可讀的變量名稱
不好的:
String yyyymmdstr = new SimpleDateFormat("YYYY/MM/DD").format(new Date());
好的:
String currentDate = new SimpleDateFormat("YYYY/MM/DD").format(new Date());
為相同類型的變量使用相同的詞匯
不好的:
getUserInfo();
getClientData();
getCustomerRecord();
好的:
getUser();
使用可搜索的名稱
我們要閱讀的代碼比要寫的代碼多得多, 所以我們寫出的代碼的可讀性和可搜索性是很重要的。 使用沒有
意義的變量名將會(huì)導(dǎo)致我們的程序難于理解, 將會(huì)傷害我們的讀者, 所以請(qǐng)使用可搜索的變量名。
不好的:
// 艸, 86400000 是什么鬼?
setTimeout(blastOff, 86400000);
好的:
// 將它們聲明為全局常量。
public static final int MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
使用解釋性的變量
不好的:
String address = "One Infinite Loop, Cupertino 95014";
String cityZipCodeRegex = "/^[^,\\\\]+[,\\\\\\s]+(.+?)\\s*(\\d{5})?$/";
saveCityZipCode(address.split(cityZipCodeRegex)[0],
address.split(cityZipCodeRegex)[1]);
好的:
String address = "One Infinite Loop, Cupertino 95014";
String cityZipCodeRegex = "/^[^,\\\\]+[,\\\\\\s]+(.+?)\\s*(\\d{5})?$/";
String city = address.split(cityZipCodeRegex)[0];
String zipCode = address.split(cityZipCodeRegex)[1];
saveCityZipCode(city, zipCode);
避免心理映射
顯示比隱式更好
不好的:
String [] l = {"Austin", "New York", "San Francisco"};
for (int i = 0; i < l.length; i++) {
String li = l[i];
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// Wait, what is `$li` for again?
dispatch(li);
}
好的:
String[] locations = {"Austin", "New York", "San Francisco"};
for (String location : locations) {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
}
不添加不必要的上下文
如果你的類名/對(duì)象名有意義, 不要在變量名上再重復(fù)。
不好的:
class Car {
public String carMake = "Honda";
public String carModel = "Accord";
public String carColor = "Blue";
}
void paintCar(Car car) {
car.carColor = "Red";
}
好的:
class Car {
public String make = "Honda";
public String model = "Accord";
public String color = "Blue";
}
void paintCar(Car car) {
car.color = "Red";
}
函數(shù)
函數(shù)參數(shù) (兩個(gè)以下最理想)
限制函數(shù)參數(shù)的個(gè)數(shù)是非常重要的, 因?yàn)檫@樣將使你的函數(shù)容易進(jìn)行測(cè)試。 一旦超過三個(gè)參數(shù)將會(huì)導(dǎo)致組
合爆炸, 因?yàn)槟悴坏貌痪帉懘罅酷槍?duì)每個(gè)參數(shù)的測(cè)試用例。
沒有參數(shù)是最理想的, 一個(gè)或者兩個(gè)參數(shù)也是可以的, 三個(gè)參數(shù)應(yīng)該避免, 超過三個(gè)應(yīng)該被重構(gòu)。 通常,
如果你有一個(gè)超過兩個(gè)函數(shù)的參數(shù), 那就意味著你的函數(shù)嘗試做太多的事情。 如果不是, 多數(shù)情況下一個(gè)
更高級(jí)對(duì)象可能會(huì)滿足需求。
當(dāng)你發(fā)現(xiàn)你自己需要大量的參數(shù)時(shí), 你可以使用一個(gè)對(duì)象。
不好的:
void createMenu(String title,String body,String buttonText,boolean cancellable){}
好的:
class MenuConfig{
String title;
String body;
String buttonText;
boolean cancellable;
}
void createMenu(MenuConfig menuConfig){}
函數(shù)應(yīng)當(dāng)只做一件事情
這是軟件工程中最重要的一條規(guī)則, 當(dāng)函數(shù)需要做更多的事情時(shí), 它們將會(huì)更難進(jìn)行編寫、 測(cè)試和推理。
當(dāng)你能將一個(gè)函數(shù)隔離到只有一個(gè)動(dòng)作, 他們將能夠被容易的進(jìn)行重構(gòu)并且你的代碼將會(huì)更容易閱讀。 如
果你嚴(yán)格遵守本指南中的這一條, 你將會(huì)領(lǐng)先于許多開發(fā)者。
不好的:
public void emailClients(List<Client> clients) {
for (Client client : clients) {
Client clientRecord = repository.findOne(client.getId());
if (clientRecord.isActive()){
email(client);
}
}
}
好的:
public void emailClients(List<Client> clients) {
for (Client client : clients) {
if (isActiveClient(client)) {
email(client);
}
}
}
private boolean isActiveClient(Client client) {
Client clientRecord = repository.findOne(client.getId());
return clientRecord.isActive();
}
函數(shù)名稱應(yīng)該說明它要做什么
不好的:
private void addToDate(Date date, int month){
//..
}
Date date = new Date();
// It's hard to to tell from the method name what is added
addToDate(date, 1);
好的:
private void addMonthToDate(Date date, int month){
//..
}
Date date = new Date();
addMonthToDate(1, date);
函數(shù)應(yīng)該只有一個(gè)抽象級(jí)別
當(dāng)在你的函數(shù)中有多于一個(gè)抽象級(jí)別時(shí), 你的函數(shù)通常做了太多事情。 拆分函數(shù)將會(huì)提升重用性和測(cè)試性。
不好的:
void parseBetterJSAlternative(String code){
String[] REGECES={};
String[] statements=code.split(" ");
String[] tokens={};
for(String regex: Arrays.asList(REGECES)){
for(String statement:Arrays.asList(statements)){
//...
}
}
String[] ast={};
for(String token:Arrays.asList(tokens)){
//lex ...
}
for(String node:Arrays.asList(ast)){
//parse ...
}
}
好的:
String[] tokenize(String code){
String[] REGECES={};
String[] statements=code.split(" ");
String[] tokens={};
for(String regex: Arrays.asList(REGECES)){
for(String statement:Arrays.asList(statements)){
//tokens push
}
}
return tokens;
}
String[] lexer(String[] tokens){
String[] ast={};
for(String token:Arrays.asList(tokens)){
//ast push
}
return ast;
}
void parseBetterJSAlternative(String code){
String[] tokens=tokenize(code);
String[] ast=lexer(tokens);
for(String node:Arrays.asList(ast)){
//parse ...
}
}
移除冗余代碼
竭盡你的全力去避免冗余代碼。 冗余代碼是不好的, 因?yàn)樗馕吨?dāng)你需要修改一些邏輯時(shí)會(huì)有多個(gè)地方
需要修改。
想象一下你在經(jīng)營一家餐館, 你需要記錄所有的庫存西紅柿, 洋蔥, 大蒜, 各種香料等等。 如果你有多
個(gè)記錄列表, 當(dāng)你用西紅柿做一道菜時(shí)你得更新多個(gè)列表。 如果你只有一個(gè)列表, 就只有一個(gè)地方需要更
新!
你有冗余代碼通常是因?yàn)槟阌袃蓚€(gè)或多個(gè)稍微不同的東西, 它們共享大部分, 但是它們的不同之處迫使你使
用兩個(gè)或更多獨(dú)立的函數(shù)來處理大部分相同的東西。 移除冗余代碼意味著創(chuàng)建一個(gè)可以處理這些不同之處的
抽象的函數(shù)/模塊/類。
讓這個(gè)抽象正確是關(guān)鍵的, 這是為什么要你遵循 Classes 那一章的 SOLID 的原因。 不好的抽象比冗
余代碼更差, 所以要謹(jǐn)慎行事。 既然已經(jīng)這么說了, 如果你能夠做出一個(gè)好的抽象, 才去做。 不要重復(fù)
你自己, 否則你會(huì)發(fā)現(xiàn)當(dāng)你要修改一個(gè)東西時(shí)時(shí)刻需要修改多個(gè)地方。
不好的:
void showDeveloperList(List<Developer> developers){
for(Developer developer:developers){
render(new Data(developer.expectedSalary,developer.experience,developer.githubLink));
}
}
void showManagerrList(List<Manager> managers){
for(Manager manager:managers){
render(new Data(manager.expectedSalary,manager.experience,manager.portfolio));
}
}
好的:
void showList(List<Employee> employees){
for(Employee employee:employees){
Data data=new Data(employee.expectedSalary,employee.experience,employee.githubLink);
String portfolio=employee.portfolio;
if("manager".equals(employee)){
portfolio=employee.portfolio;
}
data.portfolio=portfolio;
render(data);
}
}
不要使用標(biāo)記位做為函數(shù)參數(shù)
標(biāo)記位是告訴你的用戶這個(gè)函數(shù)做了不只一件事情。 函數(shù)應(yīng)該只做一件事情。 如果你的函數(shù)因?yàn)橐粋€(gè)布爾值
出現(xiàn)不同的代碼路徑, 請(qǐng)拆分它們。
不好的:
void createFile(String name,boolean temp){
if(temp){
new File("./temp"+name);
}else{
new File(name);
}
}
好的:
void createFile(String name){
new File(name);
}
void createTempFile(String name){
new File("./temp"+name);
}
避免副作用
如果一個(gè)函數(shù)做了除接受一個(gè)值然后返回一個(gè)值或多個(gè)值之外的任何事情, 它將會(huì)產(chǎn)生副作用, 它可能是
寫入一個(gè)文件, 修改一個(gè)全局變量, 或者意外的把你所有的錢連接到一個(gè)陌生人那里。
現(xiàn)在在你的程序中確實(shí)偶爾需要副作用, 就像上面的代碼, 你也許需要寫入到一個(gè)文件, 你需要做的是集
中化你要做的事情, 不要讓多個(gè)函數(shù)或者類寫入一個(gè)特定的文件, 用一個(gè)服務(wù)來實(shí)現(xiàn)它, 一個(gè)并且只有一
個(gè)。
重點(diǎn)是避免這些常見的易犯的錯(cuò)誤, 比如在對(duì)象之間共享狀態(tài)而不使用任何結(jié)構(gòu), 使用任何地方都可以寫入
的可變的數(shù)據(jù)類型, 沒有集中化導(dǎo)致副作用。 如果你能做到這些, 那么你將會(huì)比其它的碼農(nóng)大軍更加幸福。
不好的:
String name="Ryan McDermott";
void splitIntoFirstAndLastName(){
name=name.split(" ").toString();
}
splitIntoFirstAndLastName();
System.out.println(name);
好的:
String name="Ryan McDermott";
String splitIntoFirstAndLastName(){
return name.split(" ").toString();
}
String newName=splitIntoFirstAndLastName();
System.out.println(name);
System.out.println(newName);
函數(shù)式編程優(yōu)于指令式編程
函數(shù)式語言更加簡(jiǎn)潔
并且更容易進(jìn)行測(cè)試, 當(dāng)你可以使用函數(shù)式編程風(fēng)格時(shí)請(qǐng)盡情使用。
不好的:
List<Integer> programmerOutput=new ArrayList<>();
programmerOutput.add(500);
programmerOutput.add(1500);
programmerOutput.add(150);
programmerOutput.add(1000);
int totalOutput=0;
for(int i=0;i<programmerOutput.size();i++){
totalOutput+=programmerOutput.get(i);
}
好的:
List<Integer> programmerOutput=new ArrayList<>();
programmerOutput.add(500);
programmerOutput.add(1500);
programmerOutput.add(150);
programmerOutput.add(1000);
int totalOutput= programmerOutput.stream().filter(programmer -> programmer > 500).mapToInt(programmer -> programmer).sum();
封裝條件語句
不好的:
if(fsm.state.equals("fetching")&&listNode.isEmpty(){
//...
}
好的:
void shouldShowSpinner(Fsm fsm, String listNode) {
return fsm.state.equals("fetching")&&listNode.isEmpty();
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
避免負(fù)面條件
不好的:
void isDOMNodeNotPresent(Node node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
好的:
void isDOMNodePresent(Node node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
避免條件語句
這看起來似乎是一個(gè)不可能的任務(wù)。 第一次聽到這個(gè)時(shí), 多數(shù)人會(huì)說: “沒有 if 語句還能期望我干
啥呢”, 答案是多數(shù)情況下你可以使用多態(tài)來完成同樣的任務(wù)。 第二個(gè)問題通常是 “好了, 那么做很棒,
但是我為什么想要那樣做呢”, 答案是我們學(xué)到的上一條代碼整潔之道的理念: 一個(gè)函數(shù)應(yīng)當(dāng)只做一件事情。
當(dāng)你有使用 if 語句的類/函數(shù)是, 你在告訴你的用戶你的函數(shù)做了不止一件事情。 記?。?只做一件
事情。
不好的:
class Airplane{
int getCurisingAltitude(){
switch(this.type){
case "777":
return this.getMaxAltitude()-this.getPassengerCount();
case "Air Force One":
return this.getMaxAltitude();
case "Cessna":
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}
好的:
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
int getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
int getCruisingAltitude() {
return this.getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
int getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
移除僵尸代碼
僵死代碼和冗余代碼同樣糟糕。 沒有理由在代碼庫中保存它。 如果它不會(huì)被調(diào)用, 就刪掉它。 當(dāng)你需要
它時(shí), 它依然保存在版本歷史記錄中。
不好的:
void oldRequestModule(String url) {
// ...
}
void newRequestModule(String url) {
// ...
}
String req = newRequestModule;
inventoryTracker("apples", req, "www.inventory-awesome.io");
好的:
void newRequestModule(String url) {
// ...
}
String req = newRequestModule;
inventoryTracker("apples", req, "www.inventory-awesome.io");
對(duì)象和數(shù)據(jù)結(jié)構(gòu)
使用 getters 和 setters
使用 getters 和 setters 來訪問對(duì)象上的數(shù)據(jù)比簡(jiǎn)單的在一個(gè)對(duì)象上查找屬性
要好得多。 “為什么?” 你可能會(huì)問, 好吧, 原因請(qǐng)看下面的列表:
- 當(dāng)你想在獲取一個(gè)對(duì)象屬性的背后做更多的事情時(shí), 你不需要在代碼庫中查找和修改每一處訪問;
- 使用
set可以讓添加驗(yàn)證變得容易; - 封裝內(nèi)部實(shí)現(xiàn);
- 使用 getting 和 setting 時(shí), 容易添加日志和錯(cuò)誤處理;
- 繼承這個(gè)類, 你可以重寫默認(rèn)功能;
- 你可以延遲加載對(duì)象的屬性, 比如說從服務(wù)器獲取。
不好的:
class BankAccount{
public int balance=1000;
}
BankAccount bankAccount=new BankAccount();
bankAccount.balance-=100;
好的:
class BankAccount{
private int blance=1000;
public int getBlance() {
return blance;
}
public void setBlance(int blance) {
if(verifyIfAmountCanBeSetted(blance)){
this.blance = blance;
}
}
void verifyIfAmountCanBeSetted(int amount){
//...
}
}
BankAccount bankAccount=new BankAccount();
bankAccount.setBlance(2000);
int balance=bankAccount.getBlance();
類
使用方法鏈
這個(gè)模式在 Java 中是非常有用的, 并且你可以在許多類庫比如 Glide 和 OkHttp 中見到。
它使你的代碼變得富有表現(xiàn)力, 并減少啰嗦。 因?yàn)檫@個(gè)原因, 我說, 使用方法鏈然后再看看你的代碼
會(huì)變得多么簡(jiǎn)潔。 在你的類/方法中, 簡(jiǎn)單的在每個(gè)方法的最后返回 this , 然后你就能把這個(gè)類的
其它方法鏈在一起。
不好的:
class Car{
private String make;
private String model;
private String color;
public void setMake(String make) {
this.make = make;
}
public void setModel(String model) {
this.model = model;
}
public void setColor(String color) {
this.color = color;
}
public void save(){
console.log(this.make, this.model, this.color);
}
}
Car car=new Car();
car.setColor("pink");
car.setMake("Ford");
car.setModel("F-150");
car.save();
好的:
class Car{
private String make;
private String model;
private String color;
public Car setMake(String make) {
this.make = make;
return this;
}
public Car setModel(String model) {
this.model = model;
return this;
}
public Car setColor(String color) {
this.color = color;
return this;
}
public Car save(){
console.log(this.make, this.model, this.color);
return this;
}
}
Car car=new Car()
.setColor("pink")
.setMake("Ford")
.setModel("F-150")
.save();
組合優(yōu)先于繼承
正如設(shè)計(jì)模式四人幫所述, 如果可能,
你應(yīng)該優(yōu)先使用組合而不是繼承。 有許多好的理由去使用繼承, 也有許多好的理由去使用組合。這個(gè)格言
的重點(diǎn)是, 如果你本能的觀點(diǎn)是繼承, 那么請(qǐng)想一下組合能否更好的為你的問題建模。 很多情況下它真的
可以。
那么你也許會(huì)這樣想, “我什么時(shí)候改使用繼承?” 這取決于你手上的問題, 不過這兒有一個(gè)像樣的列表說
明什么時(shí)候繼承比組合更好用:
- 你的繼承表示"是一個(gè)"的關(guān)系而不是"有一個(gè)"的關(guān)系(人類->動(dòng)物 vs 用戶->用戶詳情);
- 你可以重用來自基類的代碼(人可以像所有動(dòng)物一樣行動(dòng));
- 你想通過基類對(duì)子類進(jìn)行全局的修改(改變所有動(dòng)物行動(dòng)時(shí)的熱量消耗);
不好的:
class Employee{
private String name;
private String email;
}
// 不好是因?yàn)楣蛦T“有”稅率數(shù)據(jù), EmployeeTaxData 不是一個(gè) Employee 類型。
class EmployeeTaxData extends Employee{
private String ssn;
private String salary;
}
好的:
class EmployeeTaxData{
private String ssn;
private String salary;
public EmployeeTaxData(String ssn, String salary) {
this.ssn = ssn;
this.salary = salary;
}
}
class Employee{
private String name;
private String email;
private EmployeeTaxData taxData;
void setTaxData(String ssn,String salary){
this.taxData=new EmployeeTaxData(ssn,salary);
}
}
SOLID
單一職責(zé)原則 (SRP)
正如代碼整潔之道所述, “永遠(yuǎn)不要有超過一個(gè)理由來修改一個(gè)類”。 給一個(gè)類塞滿許多功能, 就像你在航
班上只能帶一個(gè)行李箱一樣, 這樣做的問題你的類不會(huì)有理想的內(nèi)聚性, 將會(huì)有太多的理由來對(duì)它進(jìn)行修改。
最小化需要修改一個(gè)類的次數(shù)時(shí)很重要的, 因?yàn)槿绻粋€(gè)類擁有太多的功能, 一旦你修改它的一小部分,
將會(huì)很難弄清楚會(huì)對(duì)代碼庫中的其它模塊造成什么影響。
不好的:
class UserSettings {
User user;
void changeSettings(UserSettings settings) {
if (this.verifyCredentials()) {
// ...
}
}
void verifyCredentials() {
// ...
}
}
好的:
User user;
UserAuth auth;
public UserSettings(User user) {
this.user = user;
this.auth = new UserAuth(user);
}
void changeSettings(UserSettings settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
開閉原則 (OCP)
Bertrand Meyer 說過, “軟件實(shí)體 (類, 模塊, 函數(shù)等) 應(yīng)該為擴(kuò)展開放, 但是為修改關(guān)閉。” 這
是什么意思呢? 這個(gè)原則基本上說明了你應(yīng)該允許用戶添加功能而不必修改現(xiàn)有的代碼。
不好的:
class AjaxAdapter extends Adapter {
private String name;
public AjaxAdapter() {
this.name = "ajaxAdapter";
}
}
class NodeAdapter extends Adapter {
private String name;
public NodeAdapter() {
this.name = "nodeAdapter";
}
}
class HttpRequester {
public HttpRequester(Adapter adapter) {
this.adapter = adapter;
}
void fetch(String url) {
if ("ajaxAdapter".equals(this.adapter.name)) {
makeAjaxCall(url);
} else if ("httpNodeAdapter".equals(this.adapter.name)) {
makeHttpCall(url);
}
}
}
void makeAjaxCall(String url) {
// request and return promise
}
void makeHttpCall(String url) {
// request and return promise
}
好的:
class AjaxAdapter extends Adapter {
private String name;
public AjaxAdapter() {
this.name = "ajaxAdapter";
}
void request(String url){
}
}
class NodeAdapter extends Adapter {
private String name;
public NodeAdapter() {
this.name = "nodeAdapter";
}
void request(String url){
}
}
class HttpRequester {
public HttpRequester(Adapter adapter) {
this.adapter = adapter;
}
void fetch(String url) {
this.adapter.request(url);
}
}
里氏代換原則 (LSP)
這是針對(duì)一個(gè)非常簡(jiǎn)單的里面的一個(gè)恐怖意圖, 它的正式定義是: “如果 S 是 T 的一個(gè)子類型, 那么類
型為 T 的對(duì)象可以被類型為 S 的對(duì)象替換(例如, 類型為 S 的對(duì)象可作為類型為 T 的替代品)兒不需
要修改目標(biāo)程序的期望性質(zhì) (正確性、 任務(wù)執(zhí)行性等)?!?這甚至是個(gè)恐怖的定義。
最好的解釋是, 如果你又一個(gè)基類和一個(gè)子類, 那個(gè)基類和字類可以互換而不會(huì)產(chǎn)生不正確的結(jié)果。 這可
能還有有些疑惑, 讓我們來看一下這個(gè)經(jīng)典的正方形與矩形的例子。 從數(shù)學(xué)上說, 一個(gè)正方形是一個(gè)矩形,
但是你用 "is-a" 的關(guān)系用繼承來實(shí)現(xiàn), 你將很快遇到麻煩。
不好的:
class Rectangle {
protected int width;
protected int height;
public Rectangle() {
this.width = 0;
this.height = 0;
}
void setColor(String color) {
// ...
}
void render(int area) {
// ...
}
void setWidth(int width) {
this.width = width;
}
void setHeight(int height) {
this.height = height;
}
int getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
void setWidth(int width) {
this.width = width;
this.height = width;
}
void setHeight(int height) {
this.width = height;
this.height = height;
}
}
void renderLargeRectangles(List<Rectangle> rectangles) {
for(Rectangle rectangle:rectangles) {
rectangle.setWidth(4);
rectangle.setHeight(5);
int area = rectangle.getArea(); // BAD: Will return 25 for Square. Should be 20.
rectangle.render(area);
}
}
List<Rectangle> rectangles=new ArrayList<>();
rectangles.add(new Rectangle());
rectangles.add(new Rectangle());
rectangles.add(new Square());
renderLargeRectangles(rectangles);
好的:
class Shape {
void setColor(String color) {
// ...
}
int getArea() {
}
void render(int area) {
// ...
}
}
class Rectangle extends Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
super();
this.width = width;
this.height = height;
}
int getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
private int length;
public Square(int length) {
super();
this.length = length;
}
int getArea() {
return this.length * this.length;
}
}
void renderLargeShapes(List<Shape> shapes) {
for(Shape shape:shapes) {
int area = shape.getArea();
shape.render(area);
}
}
List<Shape> shapes=new ArrayList<>();
shapes.add(new Rectangle(4,5));
shapes.add(new Rectangle(4,5));
shapes.add(new Square(5));
renderLargeShapes(shapes);
接口隔離原則 (ISP)
接口隔離原則說的是 “客戶端不應(yīng)該強(qiáng)制依賴他們不需要的接口?!?/p>
不好的:
interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
class A{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method2();
}
public void depend3(I i){
i.method3();
}
}
class B{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method4();
}
public void depend3(I i){
i.method5();
}
}
class C implements I{
public void method1() {
System.out.println("類B實(shí)現(xiàn)接口I的方法1");
}
public void method2() {
System.out.println("類B實(shí)現(xiàn)接口I的方法2");
}
public void method3() {
System.out.println("類B實(shí)現(xiàn)接口I的方法3");
}
//對(duì)于類B來說,method4和method5不是必需的,但是由于接口A中有這兩個(gè)方法,
//所以在實(shí)現(xiàn)過程中即使這兩個(gè)方法的方法體為空,也要將這兩個(gè)沒有作用的方法進(jìn)行實(shí)現(xiàn)。
public void method4() {}
public void method5() {}
}
class D implements I{
public void method1() {
System.out.println("類D實(shí)現(xiàn)接口I的方法1");
}
//對(duì)于類D來說,method2和method3不是必需的,但是由于接口A中有這兩個(gè)方法,
//所以在實(shí)現(xiàn)過程中即使這兩個(gè)方法的方法體為空,也要將這兩個(gè)沒有作用的方法進(jìn)行實(shí)現(xiàn)。
public void method2() {}
public void method3() {}
public void method4() {
System.out.println("類D實(shí)現(xiàn)接口I的方法4");
}
public void method5() {
System.out.println("類D實(shí)現(xiàn)接口I的方法5");
}
}
好的:
interface I1 {
public void method1();
}
interface I2 {
public void method2();
public void method3();
}
interface I3 {
public void method4();
public void method5();
}
class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I2 i){
i.method2();
}
public void depend3(I2 i){
i.method3();
}
}
class B{
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}
class C implements I1, I2{
public void method1() {
System.out.println("類B實(shí)現(xiàn)接口I1的方法1");
}
public void method2() {
System.out.println("類B實(shí)現(xiàn)接口I2的方法2");
}
public void method3() {
System.out.println("類B實(shí)現(xiàn)接口I2的方法3");
}
}
class D implements I1, I3{
public void method1() {
System.out.println("類D實(shí)現(xiàn)接口I1的方法1");
}
public void method4() {
System.out.println("類D實(shí)現(xiàn)接口I3的方法4");
}
public void method5() {
System.out.println("類D實(shí)現(xiàn)接口I3的方法5");
}
依賴反轉(zhuǎn)原則 (DIP)
這個(gè)原則闡述了兩個(gè)重要的事情:
- 高級(jí)模塊不應(yīng)該依賴于低級(jí)模塊, 兩者都應(yīng)該依賴與抽象;
- 抽象不應(yīng)當(dāng)依賴于具體實(shí)現(xiàn), 具體實(shí)現(xiàn)應(yīng)當(dāng)依賴于抽象。
這個(gè)一開始會(huì)很難理解, 但是如果你使用過 Angular.js , 你應(yīng)該已經(jīng)看到過通過依賴注入來實(shí)現(xiàn)的這
個(gè)原則, 雖然他們不是相同的概念, 依賴反轉(zhuǎn)原則讓高級(jí)模塊遠(yuǎn)離低級(jí)模塊的細(xì)節(jié)和創(chuàng)建, 可以通過 DI
來實(shí)現(xiàn)。 這樣做的巨大益處是降低模塊間的耦合。 耦合是一個(gè)非常糟糕的開發(fā)模式, 因?yàn)闀?huì)導(dǎo)致代碼難于
重構(gòu)。
如上所述, JavaScript 沒有接口, 所以被依賴的抽象是隱式契約。 也就是說, 一個(gè)對(duì)象/類的方法和
屬性直接暴露給另外一個(gè)對(duì)象/類。 在下面的例子中, 任何一個(gè) Request 模塊的隱式契約 InventoryTracker
將有一個(gè) requestItems 方法。
不好的:
class InventoryRequester {
private String REQ_METHODS;
public InventoryRequester() {
this.REQ_METHODS = "HTTP";
}
void requestItem(String item) {
// ...
}
}
class InventoryTracker {
private List<String> items;
private InventoryRequester requester;
public InventoryTracker(List<String> items) {
this.items = items;
// 不好的: 我們已經(jīng)創(chuàng)建了一個(gè)對(duì)請(qǐng)求的具體實(shí)現(xiàn)的依賴, 我們只有一個(gè) requestItems 方法依
// 賴一個(gè)請(qǐng)求方法 'request'
this.requester = new InventoryRequester();
}
void requestItems() {
this.items.stream().forEach(item->this.requester.requestItem(item));
}
}
List<String> items=new ArrayList<>();
items.add("apples");
items.add("bananas");
InventoryTracker inventoryTracker = new InventoryTracker(items);
inventoryTracker.requestItems();
好的:
interface Requester{
void requestItem(String item);
}
class InventoryTracker {
List<String> items;
Requester requester;
public InventoryTracker(List<String> items, Requester requester) {
this.items = items;
this.requester = requester;
}
void requestItems() {
this.items.stream().forEach(item->requester.requestItem(item));
}
}
class InventoryRequesterV1 implements Requester{
String REQ_METHODS;
public InventoryRequesterV1() {
this.REQ_METHODS="HTTP";
}
@Override
public void requestItem(String item) {
// ...
}
}
class InventoryRequesterV2 implements Requester{
String REQ_METHODS;
public InventoryRequesterV2() {
this.REQ_METHODS="WS";
}
@Override
public void requestItem(String item) {
// ...
}
}
// 通過外部創(chuàng)建依賴項(xiàng)并將它們注入, 我們可以輕松的用一個(gè)嶄新的使用 WebSockets 的請(qǐng)求模塊進(jìn)行替換。
List<String> items=new ArrayList<>();
items.add("apples");
items.add("bananas");
InventoryTracker inventoryTracker = new InventoryTracker(items, new InventoryRequesterV2());
inventoryTracker.requestItems();
測(cè)試
測(cè)試比發(fā)布更加重要。 如果你沒有測(cè)試或者測(cè)試不夠充分, 每次發(fā)布時(shí)你就不能確認(rèn)沒有破壞任何事情。
測(cè)試的量由你的團(tuán)隊(duì)決定, 但是擁有 100% 的覆蓋率(包括所有的語句和分支)是你為什么能達(dá)到高度自信
和內(nèi)心的平靜。 這意味著需要一個(gè)額外的偉大的測(cè)試框架, 也需要一個(gè)好的覆蓋率工具。
沒有理由不寫測(cè)試。 這里有大量的優(yōu)秀的 JS 測(cè)試框架,
選一個(gè)適合你的團(tuán)隊(duì)的即可。 當(dāng)為團(tuán)隊(duì)選擇了測(cè)試框架之后, 接下來的目標(biāo)是為生產(chǎn)的每一個(gè)新的功能/模
塊編寫測(cè)試。 如果你傾向于測(cè)試驅(qū)動(dòng)開發(fā)(TDD), 那就太棒了, 但是要點(diǎn)是確認(rèn)你在上線任何功能或者重
構(gòu)一個(gè)現(xiàn)有功能之前, 達(dá)到了需要的目標(biāo)覆蓋率。
一個(gè)測(cè)試一個(gè)概念
不好的:
void testMakeMomentJSGreatAgain(){
Date date;
date = new MakeMomentJSGreatAgain("1/1/2015");
date.addDays(30);
Assert.equal(date.getString(),"1/31/2015").
date = new MakeMomentJSGreatAgain("2/1/2016");
date.addDays(28);
Assert.equal(date.getString(),"02/29/2016");
date = new MakeMomentJSGreatAgain("2/1/2015");
date.addDays(28);
Assert.equal(date.getString(),"03/01/2015");
}
好的:
void testThirtyDayMonths(){
Date date = new MakeMomentJSGreatAgain("1/1/2015");
date.addDays(30);
Assert.equal(date.getString(),"1/31/2015");
}
void testLeapYear(){
Date date = new MakeMomentJSGreatAgain("2/1/2016");
date.addDays(28);
Assert.equal(date.getString(),"02/29/2016");
}
void testNonLeapYear(){
Date date = new MakeMomentJSGreatAgain("2/1/2015");
date.addDays(28);
Assert.equal(date.getString(),"03/01/2015");
}
錯(cuò)誤處理
拋出錯(cuò)誤是一件好事情! 他們意味著當(dāng)你的程序有錯(cuò)時(shí)運(yùn)行時(shí)可以成功確認(rèn), 并且通過停止執(zhí)行當(dāng)前堆棧
上的函數(shù)來讓你知道, 結(jié)束當(dāng)前進(jìn)程(在 Node 中), 在控制臺(tái)中用一個(gè)堆棧跟蹤提示你。
不要忽略捕捉到的錯(cuò)誤
對(duì)捕捉到的錯(cuò)誤不做任何處理不能給你修復(fù)錯(cuò)誤或者響應(yīng)錯(cuò)誤的能力。 向控制臺(tái)記錄錯(cuò)誤 (console.log)
也不怎么好, 因?yàn)橥鶗?huì)丟失在海量的控制臺(tái)輸出中。 如果你把任意一段代碼用 try/catch 包裝那就
意味著你想到這里可能會(huì)錯(cuò), 因此你應(yīng)該有個(gè)修復(fù)計(jì)劃, 或者當(dāng)錯(cuò)誤發(fā)生時(shí)有一個(gè)代碼路徑。
不好的:
try {
functionThatMightThrow();
} catch (Exception error) {
console.log(error);
}
好的:
try {
functionThatMightThrow();
} catch (Exception error) {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
}
格式化
格式化是主觀的。 就像其它規(guī)則一樣, 沒有必須讓你遵守的硬性規(guī)則。 重點(diǎn)是不要因?yàn)楦袷饺?zhēng)論, 這
里有大量的工具來自動(dòng)格式化, 使用其中的一個(gè)即可! 因
為做為工程師去爭(zhēng)論格式化就是在浪費(fèi)時(shí)間和金錢。
針對(duì)自動(dòng)格式化工具不能涵蓋的問題(縮進(jìn)、 制表符還是空格、 雙引號(hào)還是單引號(hào)等), 這里有一些指南。
使用一致的大小寫
JavaScript 是無類型的, 所以大小寫告訴你關(guān)于你的變量、 函數(shù)等的很多事情。 這些規(guī)則是主觀的,
所以你的團(tuán)隊(duì)可以選擇他們想要的。 重點(diǎn)是, 不管你們選擇了什么, 要保持一致。
不好的:
int DAYS_IN_WEEK = 7;
int daysInMonth = 30;
String[] songs = {"Back In Black", "Stairway to Heaven", "Hey Jude"};
String[] Artists = {"ACDC", "Led Zeppelin", "The Beatles"};
void eraseDatabase() {}
void restore_database() {}
class animal {}
class Alpaca {}
好的:
int DAYS_IN_WEEK = 7;
int DAYS_IN_MONTH = 30;
String[] songs = {"Back In Black", "Stairway to Heaven", "Hey Jude"};
String[] artists = {"ACDC", "Led Zeppelin", "The Beatles"};
void eraseDatabase() {}
void restoreDatabase() {}
class Animal {}
class Alpaca {}
函數(shù)的調(diào)用方與被調(diào)用方應(yīng)該靠近
如果一個(gè)函數(shù)調(diào)用另一個(gè), 則在代碼中這兩個(gè)函數(shù)的豎直位置應(yīng)該靠近。 理想情況下,保持被調(diào)用函數(shù)在被
調(diào)用函數(shù)的正上方。 我們傾向于從上到下閱讀代碼, 就像讀一章報(bào)紙。 由于這個(gè)原因, 保持你的代碼可
以按照這種方式閱讀。
不好的:
class PerformanceReview {
String employee;
public PerformanceReview(String employee) {
this.employee = employee;
}
String lookupPeers() {
return db.lookup(this.employee, "peers");
}
String lookupManager() {
return db.lookup(this.employee, "manager");
}
void getPeerReviews() {
String peers = this.lookupPeers();
// ...
}
public void perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
void getManagerReview() {
String manager = this.lookupManager();
}
void getSelfReview() {
// ...
}
}
PerformanceReview review = new PerformanceReview("user");
review.perfReview();
好的:
class PerformanceReview {
String employee;
public PerformanceReview(String employee) {
this.employee = employee;
}
void perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
void getPeerReviews() {
String peers = this.lookupPeers();
// ...
}
String lookupPeers() {
return db.lookup(this.employee, "peers");
}
void getManagerReview() {
String manager = this.lookupManager();
}
String lookupManager() {
return db.lookup(this.employee, "manager");
}
public void getSelfReview() {
// ...
}
}
PerformanceReview review = new PerformanceReview("user");
review.perfReview();
注釋
僅僅對(duì)包含復(fù)雜業(yè)務(wù)邏輯的東西進(jìn)行注釋
注釋是代碼的辯解, 不是要求。 多數(shù)情況下, 好的代碼就是文檔。
不好的:
void hashIt(String data) {
// The hash
long hash = 0;
// Length of string
int length = data.length();
// Loop through every character in data
for (int i = 0; i < length; i++) {
// Get character code.
char mChar = data.charAt(i);
// Make the hash
hash = ((hash << 5) - hash) + mChar;
// Convert to 32-bit integer
hash &= hash;
}
}
好的:
void hashIt(String data) {
long hash = 0;
int length = data.length();
for (int i = 0; i < length; i++) {
char mchar = data.charAt(i);
hash = ((hash << 5) - hash) + mchar;
// Convert to 32-bit integer
hash &= hash;
}
}
不要在代碼庫中保存注釋掉的代碼
因?yàn)橛邪姹究刂疲?把舊的代碼留在歷史記錄即可。
不好的:
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
好的:
doStuff();
不要有日志式的注釋
記住, 使用版本控制! 不需要僵尸代碼, 注釋掉的代碼, 尤其是日志式的注釋。 使用 git log 來
獲取歷史記錄。
不好的:
/**
* 2016-12-20: Removed monads, didn't understand them (RM)
* 2016-10-01: Improved using special monads (JP)
* 2016-02-03: Removed type-checking (LI)
* 2015-03-14: Added combine with type-checking (JR)
*/
void combine(String a, String b) {
return a + b;
}
好的:
void combine(String a, String b) {
return a + b;
}
避免占位符
它們僅僅添加了干擾。 讓函數(shù)和變量名稱與合適的縮進(jìn)和格式化為你的代碼提供視覺結(jié)構(gòu)。
不好的:
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
String[] model = {"foo","bar"};
////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
void action(){
//...
}
好的:
String[] model = {"foo","bar"};
void action(){
//...
}
? 返回頂部
工具
Lint
Android Studio 提供了一個(gè)名為 Lint 的代碼掃描工具,可幫助您發(fā)現(xiàn)并更正代碼結(jié)構(gòu)質(zhì)量的問題,而無需您實(shí)際執(zhí)行應(yīng)用,也不必編寫測(cè)試用例。系統(tǒng)會(huì)報(bào)告該工具檢測(cè)到的每個(gè)問題并提供問題的描述消息和嚴(yán)重級(jí)別,以便您可以快速確定需要優(yōu)先進(jìn)行的關(guān)鍵改進(jìn)。此外,您還可以降低問題的嚴(yán)重級(jí)別以忽略與項(xiàng)目無關(guān)的問題,或者提高嚴(yán)重級(jí)別以突出特定問題。
Lint 工具可以檢查您的 Android 項(xiàng)目源文件是否有潛在的錯(cuò)誤,以及在正確性、安全性、性能、易用性、無障礙性和國際化方面是否需要優(yōu)化改進(jìn)。
Lint不僅支持Java語言,同是支持Kotlin、C/C++等的規(guī)范性檢查,Android官方推薦的代碼掃描工具。
CheckStyle
CheckStyle作為檢驗(yàn)代碼規(guī)范的插件,除了可以使用配置默認(rèn)給定的開發(fā)規(guī)范,如Sun的,Google的開發(fā)規(guī)范啊,也可以導(dǎo)入像阿里的開發(fā)規(guī)范的插件。事實(shí)上,每一個(gè)公司都存在不同的開發(fā)規(guī)范要求,所以大部分公司會(huì)給定自己的check規(guī)范,一般導(dǎo)入給定的checkstyle.xml文件即可實(shí)現(xiàn)。CheckStyle來輔助判斷代碼格式是否滿足規(guī)范,保證團(tuán)隊(duì)成員開發(fā)的code風(fēng)格一致。
CheckStyle檢驗(yàn)的主要內(nèi)容
- Javadoc注釋
- 命名約定
- 標(biāo)題
- Import語句
- 體積大小
- 空白
- 修飾符
- 塊
- 代碼問題
- 類設(shè)計(jì)
- 混合檢查(包括一些有用的比如非必須的System.out和printstackTrace)
從上面可以看出,CheckStyle提供了大部分功能都是對(duì)于代碼規(guī)范的檢查。但CheckStyle目前只支持檢查Java語言。
SonarLint
SonarLint是一個(gè)IDE擴(kuò)展,可幫助您在編寫代碼時(shí)檢測(cè)和修復(fù)質(zhì)量問題,在提交代碼之前進(jìn)行修復(fù)。
- 錯(cuò)誤檢測(cè)
受益于已有的數(shù)千條規(guī)則 ; 可以檢測(cè)常見錯(cuò)誤,棘手錯(cuò)誤和已知漏洞
- 即時(shí)反饋
就像拼寫檢查器一樣,在編碼時(shí)會(huì)檢測(cè)到并報(bào)告問題
- 豐富的文檔
精確地指出了問題所在,并為您提供了解決方法的建議
Alibaba Java Coding Guidelines
阿里巴巴于10月14號(hào)在杭州云棲大會(huì)上,正式發(fā)布《阿里巴巴Java開發(fā)規(guī)約》的掃描插件。該插件在掃描代碼后,將不符合規(guī)約的代碼按Blocker/Critical/Major三個(gè)等級(jí)顯示在下方,甚至在IDEA上,該插件還基于Inspection機(jī)制提供了實(shí)時(shí)檢測(cè)功能,編寫代碼的同時(shí)也能快速發(fā)現(xiàn)問題所在。對(duì)于歷史代碼,部分規(guī)則實(shí)現(xiàn)了批量一鍵修復(fù)的功能。
- Blocker
即系統(tǒng)無法執(zhí)行、崩潰或嚴(yán)重資源不足、應(yīng)用模塊無法啟動(dòng)或異常退出、無法測(cè)試、造成系統(tǒng)不穩(wěn)定。
嚴(yán)重花屏、內(nèi)存泄漏、用戶數(shù)據(jù)丟失或破壞、系統(tǒng)崩潰/死機(jī)/凍結(jié)、模塊無法啟動(dòng)或異常退出、嚴(yán)重的數(shù)值計(jì)算錯(cuò)誤、功能設(shè)計(jì)與需求嚴(yán)重不符、其它導(dǎo)致無法測(cè)試的錯(cuò)誤, 如服務(wù)器500錯(cuò)誤
- Critical
即影響系統(tǒng)功能或操作,主要功能存在嚴(yán)重缺陷,但不會(huì)影響到系統(tǒng)穩(wěn)定性。
功能未實(shí)現(xiàn)、功能錯(cuò)誤、系統(tǒng)刷新錯(cuò)誤、數(shù)據(jù)通訊錯(cuò)誤、輕微的數(shù)值計(jì)算錯(cuò)誤、影響功能及界面的錯(cuò)誤字或拼寫錯(cuò)誤、安全性問題
- Major
即界面、性能缺陷、兼容性。操作界面錯(cuò)誤(包括數(shù)據(jù)窗口內(nèi)列名定義、含義是否一致)、邊界條件下錯(cuò)誤、提示信息錯(cuò)誤(包括未給出信息、信息提示錯(cuò)誤等)、長時(shí)間操作無進(jìn)度提示、系統(tǒng)未優(yōu)化(性能問題)、光標(biāo)跳轉(zhuǎn)設(shè)置不好,鼠標(biāo)(光標(biāo))定位錯(cuò)誤、兼容性問題
不同的代碼檢查工具原理相同,但側(cè)重點(diǎn)不一樣,在實(shí)際的項(xiàng)目開發(fā)中,可以根據(jù)自己的團(tuán)隊(duì)及項(xiàng)目情況進(jìn)行選擇及組合,定義自己的團(tuán)隊(duì)規(guī)范。
? 返回頂部
謎題
程序員在背負(fù)期限的壓力下,只好追求快速的開發(fā)速度,于是為代碼制造了混 亂,卻認(rèn)為自己因此沒法做到更快。
制造混亂只會(huì)立刻拖慢你,叫你錯(cuò)過期限。趕上期限的唯一方法、做得快的唯一方法就是始終盡可能保持代碼整潔。
關(guān)于
歡迎關(guān)注我的個(gè)人公眾號(hào)
微信搜索:一碼一浮生,或者搜索公眾號(hào)ID:life2code