
當我開始編程時,我很高興自己的程序正在編譯并且可以按預(yù)期的方式運行,但是隨著時間的推移,我編寫了越來越多的代碼,我開始欣賞設(shè)計模式。
設(shè)計模式不僅使我的代碼更好,更易讀并且更易于維護,而且還為我節(jié)省了很多時間,我的意思是要花費大量的調(diào)試時間。 因此,我想與您分享一些內(nèi)容。
這種設(shè)計模式源自" DesignPatterns書"的作者"四人幫"。 他們介紹了這些原理,這些原理在面向?qū)ο缶幊讨刑貏e有用。 可能您已經(jīng)在使用所有這些內(nèi)容,但是刷新您的知識總是好的。
所有代碼示例均在TypeScript中。
KISS:保持簡單愚蠢
這種模式的主要概念是保持盡可能簡單的代碼。 這意味著每個方法,類,接口的名稱應(yīng)盡可能清楚,函數(shù),方法等內(nèi)部的邏輯也應(yīng)盡可能簡單明了。
KISS看起來不像的示例:
class payup {
howmuchtopay(region: string, amount: number, tax: number, country: string, price: number) {
if (country == "pl_PL") {
if (region == "masovia" || region == "Lubusz") {
if (amount > 15) {
price -= ((15/100)*price);
price += ((tax/100)*price);
return (price * amount);
}
return ((price+((tax/100)*price)) * amount);
} else {
return ((price+((tax/100)*price)) * amount);
}
} else {
return (price*amount);
}
}
}
const p = new payup();
console.log( p.howmuchtopay("masovia", 25, 23, "pl_PL", 1000) );
在此示例中,我們的代碼什么都沒有告訴我們。 類名,方法名寫不正確。 方法的主體混亂不堪,如果不能維護則很多。
使用KISS原理后,它看起來像這樣:
interface Country {
code: string;
discountAmountPercent: number;
taxAmountPercent: number;
discountRegions: Array<string>;
}
class Poland implements Country {
code: string = "pl_PL";
discountAmountPercent: number = 15;
taxAmountPercent: number = 23;
discountRegions: Array<string> = [
"masovia",
"lubusz"
];
}
class Payment {
setTax(price: any, tax: number) {
return (price + (tax/100*price));
}
setDiscount(price: any, discount: number) {
return (price - ((discount/100)*price));
}
pay(country: Country, region: string, amount: number, nettoPrice: number) {
if (
country.discountRegions.indexOf(region.toLowerCase()) != -1
&& amount > 15
) {
nettoPrice = this.setDiscount(nettoPrice, country.discountAmountPercent);
}
const bruttoPrice = this.setTax(nettoPrice, country.taxAmountPercent);
return (bruttoPrice*amount);
}
}
const payment = new Payment();
console.log ( payment.pay((new Poland), 'masovia', 25, 1000) );
您可以在上面的代碼中找到KISS原理。 這是可擴展的解決方案:我們可以有多個國家/地區(qū),并輕松添加新的折扣區(qū)域,只需在國家/地區(qū)類別中修改DiscountRegions屬性即可。 由于有了界面,我們還可以確保每個新國家都具有必需的屬性。 支付類還具有以其功能命名的方法,由于這種結(jié)構(gòu),我們優(yōu)化了代碼,使其只有一個。
那就是親吻:干凈,簡單的代碼。
DRY:不要重復自己
這個概念建議我們將執(zhí)行相同操作的代碼劃分為小部分。 有時,我們有多行代碼基本上都在做同樣的事情:例如使用指定條件按數(shù)組過濾,向?qū)ο筇砑右恍〇|西等。通常的好習慣是擺脫它。
不良DRY代碼的示例是:
class Week {
days: any;
constructor() {
this.days = [];
this.days.push({
name: "Monday",
order: 1
});
this.days.push({
name: "Tuesday",
order: 2
});
this.days.push({
name: "Wednesday",
order: 3
});
this.days.push({
name: "Thursdya",
order: 4
});
this.days.push({
name: "Friday",
order: 5
});
this.days.push({
name: "Saturday",
order: 6
});
this.days.push({
name: "Sunday",
order: 7
});
}
list() {
console.log(this.days);
}
}
const w = new Week();
w.list();
在此示例中,我們添加了多天,并且類的代碼基本相同。 我們可以通過創(chuàng)建用于此目的的方法來避免這種情況。 同樣,通過多次手動輸入日期名稱,我們擴展了出錯的可能性。
具有良好DRY代碼的適當類的示例:
enum dayNames {
Monday = "Monday",
Tuesday = "Tuesday",
Wednesday = "Wednesday",
Thursday = "Thursday",
Friday = "Friday",
Saturday = "Saturday",
Sunday = "Sunday"
}
class Day {
name: string;
order: number;
constructor(name: string, order: number = 0) {
this.name = name;
this.order = order;
}
setOrder(order: number) : Day {
this.order = order;
return this;
}
}
class Week {
days: Array<Day> = new Array();
private addDay(name: string): Day {
const day = new Day(name);
const index = this.days.push(day);
day.setOrder(index)
return day;
}
constructor() {
for(let dayName in dayNames) {
this.addDay(dayName);
}
}
listDays() {
console.log(this.days);
}
}
const firstWeek = new Week();
firstWeek.listDays();
在此示例中,我們沒有使用每天手動輸入的方式來實現(xiàn)帶有預(yù)定義日期名稱的枚舉,并且還引入了Day類。 因此,我們可以擴展它以在將來向此類添加更多功能,例如getDaylightTime。 此外,我們還為Week類實現(xiàn)了addDay方法,該方法的作用幾乎相同,但是現(xiàn)在,如果發(fā)生任何更改,我們只需在代碼中更新一個位置即可,而不是更新七個位置。
這是DRY原則。
TDA:告知而不要詢問
該原則建議我們應(yīng)該以一種使對象行為而不是對象處于何種狀態(tài)的方式來編寫代碼。這可以避免類之間不必要的依賴關(guān)系,這要歸功于它們具有更易于維護的代碼。 它與代碼封裝緊密相關(guān)。
沒有TDA原理的代碼示例:
class User {
_id: string = '';
firstName: string = '';
lastName: string = '';
tokens: number = 0;
}
class UserService {
register(firstName: string, lastName: string): User {
if ( firstName.length < 3 ) {
throw new Error("Name is not long enough.");
}
if ( lastName.length < 3) {
throw new Error("Name is not long enough");
}
const user = new User();
user._id = Math.random().toString(36).substring(7);
user.firstName = firstName.toLowerCase();
user.lastName = lastName.toLowerCase();
return user;
}
updateTokens(user: User, operation: string, amount: number): User {
if (operation == 'add') {
user.tokens += amount;
}
if (operation == 'sub') {
if (user.tokens - amount >= 0 ) {
user.tokens -= amount;
} else {
user.tokens = 0;
}
}
return user
}
}
const uService = new UserService();
const u = uService.register("John", "Smith");
uService.updateTokens(u, 'add', 1000);
console.log( u );
uService.updateTokens(u, 'sub', 1100);
console.log( u );
正如我們可以看到UserService多次訪問User對象屬性一樣,特別是在更新User.tokens時,如果我們在程序的許多部分中都擁有該功能并且想要更改其工作方式的邏輯呢,還要看看驗證器:所有邏輯 它的行為方式在方法內(nèi)部,但是我們應(yīng)該使其更具可伸縮性和可重用性。 下面是一個示例如何執(zhí)行此操作。
TDA示例:
/**
* VALIDATORS
*/
class StringLengthValidator {
static greaterThan(value: string, length: number): boolean {
if ( value.length > length) {
return true
} else {
throw new Error("String is not long enough.");
}
}
}
class NaturalNumberValidator {
static operation(from: number, amount: number) {
if (from + amount <= 0) {
return 0;
}
return from + amount;
}
}
/**
* INTERFACES
*/
interface IUserAccount {
_id: string;
firstName: string;
lastName: string;
tokens: number;
}
/**
* ENUMS
*/
enum operations {
add = 'add',
sub = 'sub'
}
/**
* CLASSES
*/
class User implements IUserAccount {
_id : string = '';
firstName: string = '';
lastName: string = '';
tokens: number = 0;
constructor(firstName, lastName) {
this._id = this._generateRandomID();
this.setFirstName(firstName);
this.setLastName(lastName);
}
setFirstName(newFirstName: string): User {
StringLengthValidator.greaterThan(newFirstName, 3);
this.firstName = newFirstName;
return this;
}
setLastName(newLastName: string): User {
StringLengthValidator.greaterThan(newLastName, 3);
this.lastName = newLastName;
return this;
}
updateTokens(amount: number): User {
this.tokens = NaturalNumberValidator.operation(this.tokens, amount);
return this;
}
private _generateRandomID() {
return Math.random().toString(36).substring(7);
}
}
class UserService {
register(firstName: string, lastName: string): User {
let user : User = null;
try {
user = new User(firstName, lastName);
} catch (e) {
console.log(e);
}
return user;
}
updateTokens(user: User, operation: operations, amount: number): User {
if (operation === operations.sub) {
amount *= -1;
}
return user.updateTokens(amount);
}
}
/**
* PROGRAM
*/
const uService = new UserService();
const u = uService.register("john", "smith");
uService.updateTokens(u, operations.add, 1000);
console.log(u);
uService.updateTokens(u, operations.sub, 1100);
console.log(u);
乍一看,似乎似乎過于復雜,需要更多代碼。但是,長遠來說,感謝封裝和獨立的驗證器,我們可以在許多通用情況下多次使用它。 User類的屬性僅在其內(nèi)部使用,UserService正在調(diào)用高級方法來訪問它。 因此,我們將所有邏輯都放在一個地方,因此當我們要在其他地方使用User類時,程序?qū)搭A(yù)期運行。
SoC:關(guān)注點分離
這個原則告訴我們將一個班級的責任劃分給這個班級和僅將這個班級的責任分開。 對象不應(yīng)共享其功能。 每個類都應(yīng)該是唯一的,并且應(yīng)與其他類分開。
不帶SoC的代碼示例:
class User {
_id: string = '';
name: string = '';
balance: number = 0;
constructor(name) {
this._id = Math.random().toString(36).substring(7);
this.name = name;
}
}
class AccountService {
log(msg: string) {
console.log((new Date()) + " :: " + msg);
}
transfer(user1: User, user2: User, amount: number): any {
// validate amount
if ( amount <= 0 ){
this.log("amount 0, nothing changed.");
return {user1, user2};
}
// validate if user1 have enough
if ((user1.balance - amount) < 0) {
this.log("user " + user1._id + " did not have enough funds.");
return {user1, user2};
}
//get from user1
user1.balance -= amount;
// add to user2
user2.balance += amount;
this.log("User " + user1._id + " now has " + user1.balance);
this.log("User " + user2._id + " now has " + user2.balance);
return {user1, user2};
}
updateBalance(user: User, amount: number): User {
user.balance += amount;
this.log("User " + user._id + " now has " + user.balance);
return user;
}
}
const aService = new AccountService();
let u1 = new User("john");
u1 = aService.updateBalance(u1, 1000);
let u2 = new User("bob");
u2 = aService.updateBalance(u2, 500);
console.log( aService.transfer(u1, u2, 250) );
我們擁有AccountService,它負責多項工作:記錄,驗證和操作用戶余額。 同樣,未實施TDA。 我們應(yīng)該分開驗證并創(chuàng)建外部記錄器,以備將來在其他模塊中使用。
適當?shù)腟oC的示例:
/**
* VALIDATORS
*/
class StringLengthValidator {
static greaterThan(value: string, length: number): boolean {
if ( value.length > length) {
return true
} else {
throw new Error("String is not long enough.");
}
}
}
class UserBalanceValidator {
static haveEnoughFunds(user: User, amount: number): boolean {
return (user.getBalance() - amount) > 0;
}
}
/**
* INTERFACES
*/
interface IUserAccount {
_id: string;
name: string;
balance: number;
}
/**
* CLASSES
*/
class User implements IUserAccount {
_id: string = '';
name: string = '';
balance: number = 0;
constructor(name) {
this._id = this._generateRandomID();
this.setName(name);
}
private _generateRandomID() {
return Math.random().toString(36).substring(7);
}
getId(): string {
return this._id;
}
setName(name: string): User {
StringLengthValidator.greaterThan(name, 2);
this.name = name;
return this;
}
getBalance(): number {
return this.balance;
}
setBalance(amount: number): User {
this.balance = amount;
LoggerService.log("User " + this.getId() + " now has " + this.getBalance() );
return this;
}
}
class LoggerService {
static log(message: string): string {
message = (new Date()) + " :: " + message;
console.log(message);
return message;
}
}
class AccountService {
transfer(fromUser: User, toUser: User, amount: number): any {
if (!UserBalanceValidator.haveEnoughFunds(fromUser, amount)) {
LoggerService.log("User " + fromUser.getId() + " has not enough funds.");
return {fromUser, toUser};
}
fromUser.setBalance(fromUser.getBalance() - amount);
toUser.setBalance(toUser.getBalance() + amount);
return {fromUser, toUser}
}
updateBalance(user: User, amount: number) : User {
user.setBalance(user.getBalance() + amount);
return user;
}
}
const aService = new AccountService();
let u1 = new User("john");
let u2 = new User("bob");
u1 = aService.updateBalance(u1, 1000);
u2 = aService.updateBalance(u2, 500);
console.log( aService.transfer(u1, u2, 250) );
現(xiàn)在我們每個類都有各自的狀態(tài)和功能:驗證器,用戶,AcocuntService和LoggerService。由于SoC,我們可以在應(yīng)用程序的許多不同模塊中分別使用它。 而且,由于存在邏輯的位置較少,因此該代碼更易于維護。
YAGNI:您將不需要它
該原則不是在告訴我們?nèi)绾沃苯釉诖a中執(zhí)行某些操作,而是告訴我們?nèi)绾斡行У剡M行編碼。 總的說來,我們應(yīng)該只編寫在特定時刻需要我們編寫的內(nèi)容。 例如:當我們必須對電子郵件和密碼字段進行驗證時,我們不應(yīng)該對名稱進行驗證,因為我們可能永遠不需要它。 確實是TDD:我們僅針對微功能編寫測試,并為此編寫最少的代碼。 " YAGNI"正試圖節(jié)省我們的時間,專注于沖刺中最重要的事情。
那就是所有的內(nèi)容
我希望通過這篇文章,您學到了一些東西,或者提醒了您已經(jīng)知道的一些東西。 :)