当我开始编程时,我很高兴自己的程序正在编译并且可以按预期的方式运行,但是随着时间的推移,我编写了越来越多的代码,我开始欣赏设计模式。
设计模式不仅使我的代码更好,更易读并且更易于维护,而且还为我节省了很多时间,我的意思是要花费大量的调试时间。 因此,我想与您分享一些内容。
这种设计模式源自" DesignPatterns书"的作者"四人帮"。 他们介绍了这些原理,这些原理在面向对象编程中特别有用。 可能您已经在使用所有这些内容,但是刷新您的知识总是好的。
所有代码示例均在TypeScript中。
KISS:保持简单愚蠢
这种模式的主要概念是保持尽可能简单的代码。 这意味着每个方法,类,接口的名称应尽可能清楚,函数,方法等内部的逻辑也应尽可能简单明了。
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原理。 这是可扩展的解决方案:我们可以有多个国家/地区,并轻松添加新的折扣区域,只需在国家/地区类别中修改DiscountRegions属性即可。 由于有了界面,我们还可以确保每个新国家都具有必需的属性。 支付类还具有以其功能命名的方法,由于这种结构,我们优化了代码,使其只有一个。
那就是亲吻:干净,简单的代码。
DRY:不要重复自己
这个概念建议我们将执行相同操作的代码划分为小部分。 有时,我们有多行代码基本上都在做同样的事情:例如使用指定条件按数组过滤,向对象添加一些东西等。通常的好习惯是摆脱它。
不良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();
在此示例中,我们添加了多天,并且类的代码基本相同。 我们可以通过创建用于此目的的方法来避免这种情况。 同样,通过多次手动输入日期名称,我们扩展了出错的可能性。
具有良好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();
在此示例中,我们没有使用每天手动输入的方式来实现带有预定义日期名称的枚举,并且还引入了Day类。 因此,我们可以扩展它以在将来向此类添加更多功能,例如getDaylightTime。 此外,我们还为Week类实现了addDay方法,该方法的作用几乎相同,但是现在,如果发生任何更改,我们只需在代码中更新一个位置即可,而不是更新七个位置。
这是DRY原则。
TDA:告知而不要询问
该原则建议我们应该以一种使对象行为而不是对象处于何种状态的方式来编写代码。这可以避免类之间不必要的依赖关系,这要归功于它们具有更易于维护的代码。 它与代码封装紧密相关。
没有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时,如果我们在程序的许多部分中都拥有该功能并且想要更改其工作方式的逻辑呢,还要看看验证器:所有逻辑 它的行为方式在方法内部,但是我们应该使其更具可伸缩性和可重用性。 下面是一个示例如何执行此操作。
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类的属性仅在其内部使用,UserService正在调用高级方法来访问它。 因此,我们将所有逻辑都放在一个地方,因此当我们要在其他地方使用User类时,程序将按预期运行。
SoC:关注点分离
这个原则告诉我们将一个班级的责任划分给这个班级和仅将这个班级的责任分开。 对象不应共享其功能。 每个类都应该是唯一的,并且应与其他类分开。
不带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。 我们应该分开验证并创建外部记录器,以备将来在其他模块中使用。
适当的SoC的示例:
/**
* 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) );
现在我们每个类都有各自的状态和功能:验证器,用户,AcocuntService和LoggerService。由于SoC,我们可以在应用程序的许多不同模块中分别使用它。 而且,由于存在逻辑的位置较少,因此该代码更易于维护。
YAGNI:您将不需要它
该原则不是在告诉我们如何直接在代码中执行某些操作,而是告诉我们如何有效地进行编码。 总的说来,我们应该只编写在特定时刻需要我们编写的内容。 例如:当我们必须对电子邮件和密码字段进行验证时,我们不应该对名称进行验证,因为我们可能永远不需要它。 确实是TDD:我们仅针对微功能编写测试,并为此编写最少的代码。 " YAGNI"正试图节省我们的时间,专注于冲刺中最重要的事情。
那就是所有的内容
我希望通过这篇文章,您学到了一些东西,或者提醒了您已经知道的一些东西。 :)