前言
温馨提示
对于原本不太熟悉设计模式的人来说(比如在下),这些内容是需要一定的时间消化的!慢慢来 😆
👋 你好啊,我是你的人类朋友!
今天说说设计模式的原则有哪些!
在开发用户权限系统时,你是否遇到过这样的问题:
当创建新的管理员用户类型时,发现它无法兼容普通用户的所有方法,导致系统中到处需要判断用户类型?
让我们了解设计模式的基本原则,构建更健壮的软件架构~
健壮是啥意思? 健壮是指软件系统在面对变化和复杂性时,能够保持稳定运行的能力。也就是耐造的能力。
正文
SOLID 原则
SOLID 是面向对象设计的五个基本原则。
1. 单一职责原则 (SRP - Single Responsibility Principle)
含义:一个类应该只有一个引起变化的原因。
javascript
/* 违反SRP:订单处理类承担了验证、计算、存储、通知多个职责 */
class OrderProcessor {
validateOrder(order) {
return order.items.length > 0
}
calculateTotal(order) {
return order.items.reduce((sum, item) => sum + item.price, 0)
}
saveToDatabase(order) {
console.log('保存订单到数据库')
}
sendEmailConfirmation(order) {
console.log('发送订单确认邮件')
}
}
// 使用
const processor = new OrderProcessor()
processor.validateOrder({items: [{price: 100}]})
/* 违反SRP结束 */
/* 遵循SRP:每个类只负责一个明确职责 */
class OrderValidator {
validate(order) {
return order.items.length > 0
}
}
class OrderCalculator {
calculateTotal(order) {
return order.items.reduce((sum, item) => sum + item.price, 0)
}
}
class OrderRepository {
save(order) {
console.log('保存订单到数据库')
}
}
class OrderProcessor {
constructor() {
this.validator = new OrderValidator()
this.calculator = new OrderCalculator()
this.repository = new OrderRepository()
}
process(order) {
if (!this.validator.validate(order)) return
order.total = this.calculator.calculateTotal(order)
this.repository.save(order)
}
}
// 使用
const order = {items: [{price: 100}, {price: 200}]}
const processor = new OrderProcessor()
processor.process(order)
console.log(order.total) // 输出: 300
/* 遵循SRP结束 */
2. 开闭原则 (OCP - Open-Closed Principle)
含义:软件实体应该对扩展开放,对修改关闭。
javascript
/* 违反OCP:添加新通知类型需要修改原有类 */
class NotificationService {
send(message, type) {
if (type === 'email') {
console.log(`发送邮件: ${message}`)
} else if (type === 'sms') {
console.log(`发送短信: ${message}`)
}
// 添加新类型需要修改这里
}
}
// 使用
const notifier = new NotificationService()
notifier.send('订单创建成功', 'email')
/* 违反OCP结束 */
/* 遵循OCP:通过扩展而非修改来支持新类型 */
class Notification {
send(message) {
throw new Error('必须实现send方法')
}
}
class EmailNotification extends Notification {
send(message) {
console.log(`发送邮件: ${message}`)
}
}
class SMSNotification extends Notification {
send(message) {
console.log(`发送短信: ${message}`)
}
}
class PushNotification extends Notification {
send(message) {
console.log(`发送推送: ${message}`)
}
}
class NotificationService {
constructor() {
this.notifications = []
}
addNotification(notification) {
this.notifications.push(notification)
}
broadcast(message) {
this.notifications.forEach(notification => {
notification.send(message)
})
}
}
// 使用
const service = new NotificationService()
service.addNotification(new EmailNotification())
service.addNotification(new SMSNotification())
service.broadcast('系统维护通知')
// 输出: 发送邮件: 系统维护通知
// 输出: 发送短信: 系统维护通知
/* 遵循OCP结束 */
3. 里氏替换原则 (LSP - Liskov Substitution Principle)
含义:子类应该能够替换它们的父类,而不影响程序的正确性。
javascript
/* 违反LSP:子类改变了父类的权限检查逻辑 */
class User {
constructor(permissions) {
this.permissions = permissions || []
}
hasPermission(permission) {
return this.permissions.includes(permission)
}
canDeleteContent() {
return this.hasPermission('delete')
}
}
class AdminUser extends User {
canDeleteContent() {
// 违反:管理员绕过权限检查,破坏了父类契约
return true
}
}
// 使用 - 会出现不一致行为
function deleteContent(user, content) {
if (user.canDeleteContent()) {
console.log(`删除内容: ${content}`)
}
}
const regularUser = new User(['read'])
const adminUser = new AdminUser([]) // 没有delete权限但可以删除
deleteContent(regularUser, '文章') // 无输出
deleteContent(adminUser, '文章') // 输出: 删除内容: 文章
/* 违反LSP结束 */
/* 遵循LSP:子类遵守父类的行为契约 */
class User {
constructor(permissions) {
this.permissions = permissions || []
}
hasPermission(permission) {
return this.permissions.includes(permission)
}
canDeleteContent() {
return this.hasPermission('delete')
}
}
class AdminUser extends User {
canDeleteContent() {
// 遵循:仍然进行权限检查,只是权限范围更大
return this.hasPermission('delete') || this.hasPermission('admin_delete')
}
}
// 使用 - 行为一致
const regularUser = new User(['delete'])
const adminUser = new AdminUser(['admin_delete'])
deleteContent(regularUser, '文章') // 输出: 删除内容: 文章
deleteContent(adminUser, '文章') // 输出: 删除内容: 文章
/* 遵循LSP结束 */
4. 接口隔离原则 (ISP - Interface Segregation Principle)
含义:客户端不应该被迫依赖它们不使用的接口。
javascript
/* 违反ISP:简单商店被迫依赖所有支付方法 */
class PaymentProcessor {
processCreditCard(amount) {
console.log(`信用卡支付: $${amount}`)
}
processPayPal(amount) {
console.log(`PayPal支付: $${amount}`)
}
processBankTransfer(amount) {
console.log(`银行转账: $${amount}`)
}
}
class SimpleStore {
constructor() {
this.paymentProcessor = new PaymentProcessor()
}
processPayment(amount) {
// 只使用信用卡,但依赖了所有支付方法
this.paymentProcessor.processCreditCard(amount)
}
}
// 使用
const store = new SimpleStore()
store.processPayment(100) // 输出: 信用卡支付: $100
/* 违反ISP结束 */
/* 遵循ISP:客户端只依赖需要的接口 */
class PaymentProcessor {
process(amount) {
throw new Error('必须实现process方法')
}
}
class CreditCardProcessor extends PaymentProcessor {
process(amount) {
console.log(`信用卡支付: $${amount}`)
}
}
class PayPalProcessor extends PaymentProcessor {
process(amount) {
console.log(`PayPal支付: $${amount}`)
}
}
class SimpleStore {
constructor(paymentProcessor) {
this.paymentProcessor = paymentProcessor
}
processPayment(amount) {
this.paymentProcessor.process(amount)
}
}
// 使用
const creditStore = new SimpleStore(new CreditCardProcessor())
const paypalStore = new SimpleStore(new PayPalProcessor())
creditStore.processPayment(100) // 输出: 信用卡支付: $100
paypalStore.processPayment(200) // 输出: PayPal支付: $200
/* 遵循ISP结束 */
5. 依赖倒置原则 (DIP - Dependency Inversion Principle)
含义:高层模块不应该依赖低层模块,两者都应该依赖抽象。
javascript
/* 违反DIP:服务层直接依赖具体的数据访问实现 */
class MySQLUserRepository {
findUserById(id) {
console.log(`从MySQL查询用户: ${id}`)
return {id, name: 'MySQL用户'}
}
}
class UserService {
constructor() {
// 违反:直接依赖具体实现
this.userRepository = new MySQLUserRepository()
}
getUserProfile(id) {
return this.userRepository.findUserById(id)
}
}
// 使用
const service = new UserService()
const user = service.getUserProfile(1)
console.log(user.name) // 输出: MySQL用户
/* 违反DIP结束 */
/* 遵循DIP:通过抽象解耦依赖关系 */
class UserRepository {
findUserById(id) {
throw new Error('必须实现findUserById方法')
}
}
class MySQLUserRepository extends UserRepository {
findUserById(id) {
console.log(`从MySQL查询用户: ${id}`)
return {id, name: 'MySQL用户'}
}
}
class MongoDBUserRepository extends UserRepository {
findUserById(id) {
console.log(`从MongoDB查询用户: ${id}`)
return {id, name: 'MongoDB用户'}
}
}
class UserService {
constructor(userRepository) {
// 遵循:依赖抽象接口
this.userRepository = userRepository
}
getUserProfile(id) {
return this.userRepository.findUserById(id)
}
}
// 使用 - 可以轻松切换不同的数据源
const mysqlService = new UserService(new MySQLUserRepository())
const mongoService = new UserService(new MongoDBUserRepository())
mysqlService.getUserProfile(1) // 输出: 从MySQL查询用户: 1
mongoService.getUserProfile(1) // 输出: 从MongoDB查询用户: 1
/* 遵循DIP结束 */
其他重要原则
6. 合成复用原则 (CRP - Composite Reuse Principle)
含义:优先使用对象组合,而不是继承来达到复用的目的。
javascript
/* 违反CRP:使用继承导致类层次复杂 */
class Logger {
log(message) {
console.log(`基础日志: ${message}`)
}
}
class FileLogger extends Logger {
log(message) {
super.log(message)
console.log(`文件日志: ${message}`)
}
}
// 使用
const fileLogger = new FileLogger()
fileLogger.log('测试消息')
/* 违反CRP结束 */
/* 遵循CRP:使用组合提高灵活性 */
class ConsoleWriter {
write(message) {
console.log(`控制台: ${message}`)
}
}
class FileWriter {
write(message) {
console.log(`写入文件: ${message}`)
}
}
class DatabaseWriter {
write(message) {
console.log(`保存到数据库: ${message}`)
}
}
class Logger {
constructor(writer) {
this.writer = writer
}
log(message) {
this.writer.write(message)
}
}
// 使用 - 可以灵活组合不同的写入器
const consoleLogger = new Logger(new ConsoleWriter())
const fileLogger = new Logger(new FileWriter())
const dbLogger = new Logger(new DatabaseWriter())
consoleLogger.log('控制台消息') // 输出: 控制台: 控制台消息
fileLogger.log('文件消息') // 输出: 写入文件: 文件消息
dbLogger.log('数据库消息') // 输出: 保存到数据库: 数据库消息
/* 遵循CRP结束 */
7. 迪米特法则 (LoD - Law of Demeter)
含义:一个对象应该对其他对象有最少的了解,只与直接的朋友通信。
javascript
/* 违反LoD:订单服务深入访问多层对象 */
class OrderService {
constructor(userService) {
this.userService = userService
}
getOrderSummary(orderId) {
const order = this.userService.orderRepository.findOrder(orderId)
const user = order.getUser()
const address = user.getAddress() // 违反:访问间接对象
const city = address.getCity() // 违反:访问更深层对象
return {city}
}
}
// 使用 - 耦合度过高
const service = new OrderService(userService)
const summary = service.getOrderSummary(123)
/* 违反LoD结束 */
/* 遵循LoD:只与直接朋友通信,减少耦合 */
class OrderRepository {
getOrderSummary(orderId) {
const order = this.findOrder(orderId)
return order.getSummary() // 委托给订单对象处理内部细节
}
}
class OrderService {
constructor(orderRepository) {
this.orderRepository = orderRepository
}
getOrderSummary(orderId) {
const orderSummary = this.orderRepository.getOrderSummary(orderId)
return {
orderId: orderSummary.id,
userName: orderSummary.userName,
city: orderSummary.city
} // 遵循:只访问直接提供的数据
}
}
// 使用 - 耦合度低,易于测试和维护
const orderRepo = new OrderRepository()
const service = new OrderService(orderRepo)
const summary = service.getOrderSummary(123)
console.log(summary.city)
/* 遵循LoD结束 */
最后
回到前言中的问题:
如何确保新的管理员用户类型能够兼容普通用户?
答案就是应用里氏替换原则。
确保子类完全遵守父类的行为契约,任何使用父类的地方都可以安全地替换为子类,不会破坏系统功能!