软件开发原则
-
单一职责原则(每个类或模块应该只有一个导致其变化的原因,即它应该只承担一个职责。这样可以使代码更容易理解、维护和扩展。)
-
开闭原则(软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即,在不修改现有代码的前提下,可以通过扩展功能来实现需求的变化。)
-
里氏替换原则(子类对象应该可以替换其父类对象,并且不会导致程序行为的改变。确保继承的正确性,使得子类能够在不修改父类的情况下进行扩展。)
-
接口隔离原则(不应该强迫客户端依赖它们不需要的接口。即,应该将大的接口分割成更小、更具体的接口,这样客户端只需要知道它们直接使用的接口。)
-
迪米特法则 (一个对象应尽量少的了解其他对象。具体来说,一个对象只应该与其直接依赖的对象进行交互,而不应该访问它依赖对象的依赖对象。这个原则可以减少类之间的耦合,增强模块的独立性。)
一、什么是设计模式?请简述其作用
设计模式,是针对设计问题的通用解决方案
作用:为了可重用代码,让代码更容易让别人理解,保证代码的可靠性
设计模式的四大要素:封装、继承、多态、关联。 核心是封装的概念
二、23种设计模式分为哪三大类
1、创建型:如何创建对象(单例模式、工厂模式、抽象工厂模式)
2、结构型:如何实现类和对象的组合(代理模式)
3、行为型:描述类或对象怎样交互以及怎么样分配职责(策略模式)
三、请解释一下什么是单例模式,并阐述一下使用场景
确保一个类只有一个实例,并提供一份访问它的全局访问点
Vuex 也使用了单例模式
单例模式的两种实现方式:类和包装
实现方式:
类class是ES6新增的语法,在之前我们想要新建一个对象实例,是通过new构造函数的方式来实现的。我们每一次用new的时候,就会生成一个新的实例对象,每个实例对象之间是完全独立的。
代码实现出来就是如下
function Car (name) {
this.name = name;
}
var car1 = new Car('benz');
var car2 = new Car('audi');
console.log(car1 === car2) // false
复制代码
如果想要实现单例,就需要由一个变量将第一次new生成的实例对象保存下来,后面再执行new的时候,就直接但会第一次生成的实例对象,这样就实现了单例。
我们通过两种方法来实现一下:类和闭包
3.1类的实现方式
class SingletonCar {
constructor () {
this.name = 'benz';
}
static getInstance () {
if (!SingletonCar.instance) {
SingletonCar.instance = new SingletonCar();
}
return SingletonCar.instance;
}
}
let car1 = SingletonCar.getInstance();
let car2 = SingletonCar.getInstance();
console.log(car1 === car2) // true
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就比哦是该方法不会被实例继承,而是直接通过类的待用,这就被成为“静态方法”。
静态方法可以直接在父类上面调用,SingletonCar.getInstance(),而不是在实例对象上面调用。如果在实例上调用静态方法,就会抛出一个错误,表示不存在该方法。
用类来实现单例模式,只要记住这个 getInstance()静态方法就可以了。
3.2 闭包的实现方式
var SingletonCar = (function () {
var instance;
var SingletonCarTemp = function () {
this.name = 'benz';
};
return function () {
if (!instance) {
instance = new SingletonCarTemp();
}
return instance;
}
})();
var car1 = new SingletonCar();
var car2 = new SingletonCar();
console.log(car1 === car2) // true
复制代码
借助闭包,在内存中保留了instance变量,不会被垃圾回收,用来保存唯一的实例,多次调用new的时候,只返回第一次创建的实例。
3.3 vuex中的单例模式
Vuex用一个全局的Store存储应用所有的状态,然后提供一些API供用户去读取和修改。一看到全局唯一的Store,就可以想到是单例模式了。
vuex的内部代码如下所示:
let Vue
...
export function install (_Vue) {
// 是否已经执行过了 Vue.use(Vuex),如果在非生产环境多次执行,则提示错误
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 如果是第一次执行 Vue.use(Vuex),则把传入的 _Vue 赋值给定义的变量 Vue
Vue = _Vue
// Vuex 初始化逻辑
applyMixin(Vue)
}
Vue.use(Vuex) 的时候,会调用 install 方法,真正的 Vue 会被当做参数传入,如果多次执行 Vue.use(Vuex),也只会生效一次,也就是只会执行一次 applyMixin(Vue),所以只会有一份唯一的 Store,这就是 Vuex 中单例模式的实现,相当于就是用的闭包的实现方式。
3.4练习:
实现一个全局唯一的 Loading 遮罩。
思路
我们在业务开发的过程中,有很多需求都会有 Loading 状态,这时候直接掏出单例模式,记住上面的 getInstance 静态方法或者闭包 instance 变量,三下五除二即可实现。
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>单例模式全局Loading</title>
<style>
.loading {
width: 100vw;
height: 100vh;
line-height: 100vh;
position: absolute;
top: 0;
left: 0;
background-color: #000;
opacity: .7;
color: #fff;
text-align: center;
}
</style>
</head>
<body>
<button id="startLoading">点击加载</button>
<script>
const Loading = (function () {
let instance
return function () {
if (!instance) {
instance = document.createElement('div')
instance.innerHTML = '加载中...'
instance.id = 'loading'
instance.className = 'loading'
document.body.appendChild(instance)
}
return instance
}
})()
document.getElementById('startLoading').addEventListener('click', () => {
const loading = new Loading()
})
</script>
</body>
</html>
复制代码
3.5总结
单例模式,就是确保一个类只有一个实例,如果采用 class 来实现,记住 getInstance 静态方法,如果采用闭包来实现,记住 instance 变量。当我们在业务开发中,遇到类似于 Vuex 这种需要全局唯一状态的时候,就是单例模式登场的时候。
四、简单工厂、工厂方法和抽象工厂模式有什么区别?
工厂方法模式(Factory Method Pattern)和抽象工厂模式(Abstract Factory Pattern)都属于创建型设计模式,但它们在解决问题的方式和应用场景上有一些区别。
工厂方法模式:
工厂方法模式关注于创建单个产品,它通过定义一个抽象的工厂类,该工厂类包含一个抽象的工厂方法,具体的产品创建由子类工厂来实现。每个具体的工厂类都负责创建一种具体的产品,这样就实现了产品的创建和工厂的分离。
适用场景:
当一个类无法预知它需要创建的对象的类时,使用工厂方法模式。工厂方法允许子类决定要创建的对象。
当你希望通过继承来扩展和定制一个特定的类,以创建该类的不同实例。
抽象工厂模式:
抽象工厂模式关注于创建一组相关的产品,它引入了一个抽象的工厂接口,该接口包含一组抽象的工厂方法,每个工厂方法用于创建一类相关的产品。具体的工厂类实现了这个抽象工厂接口,从而可以创建一组相关的产品。
适用场景:
当需要创建一组相关的产品,而这些产品之间存在某种关联或约束时,使用抽象工厂模式。例如,创建不同操作系统下的图形界面组件。
当系统要求在不同的产品族之间切换时,使用抽象工厂模式。产品族是指具有相关性的一组产品,例如不同品牌的手机和配件。
总之,工厂方法模式适用于创建单一产品,具有更多的灵活性,而抽象工厂模式适用于创建一组相关的产品,具有更强的扩展性和变化适应性。在选择使用哪个模式时,需要根据实际问题的需求和复杂度来进行判断。
五、代理模式有什么优缺点?什么时候使用它?
- 代理模式(Proxy Pattern)是一种结构型设计模式,用于为其他对象提供一种代理以控制对该对象的访问。代理模式可以用来实现各种功能,包括延迟加载、访问控制、日志记录等。下面是代理模式的优缺点以及适用场景的详细说明:
使用 JavaScript 实现的代理模式示例代码,它模拟了延迟加载图像的功能。
// 实际对象
class RealImage {
constructor(filename) {
this.filename = filename;
this.loadImage();
}
loadImage() {
console.log(`Loading ${this.filename}`);
}
display() {
console.log(`Displaying ${this.filename}`);
}
}
// 代理对象
class ImageProxy {
constructor(filename) {
this.filename = filename;
this.realImage = null;
}
display() {
if (this.realImage === null) {
this.realImage = new RealImage(this.filename);
}
this.realImage.display();
}
}
// 使用示例
const image = new ImageProxy('test_image.jpg');
image.display(); // 图片将在此处加载并显示
image.display(); // 图片不会再次加载,只会显示
代理模式的优点
- 控制访问:代理模式可以控制对实际对象的访问。例如,可以在代理中实现访问控制逻辑,限制某些用户或系统的访问权限。
- 延迟加载:代理可以在实际对象创建时延迟初始化(即懒加载)。这有助于提高性能,尤其是当实际对象的创建代价很高时。
- 增强功能:代理可以在访问实际对象前后添加额外的功能,例如日志记录、性能监控或缓存。这使得原始对象的代码保持干净,而扩展功能可以集中在代理中实现。
- 解耦:通过引入代理,可以将客户端与实际对象解耦。这使得系统的结构更加灵活和模块化。
代理模式的缺点
- 增加复杂性:引入代理会增加系统的复杂性,因为需要额外的代理类来处理实际对象的访问。对于简单应用,增加代理可能会显得不必要。
- 性能开销:虽然代理可以帮助实现延迟加载,但代理本身也会引入额外的性能开销,特别是在涉及到复杂代理逻辑或频繁的代理访问时。
- 调试困难:使用代理模式可能会使调试变得更加复杂,因为需要跟踪代理和实际对象之间的交互。这可能会影响开发效率。
六、策略模式有什么优缺点?什么时候使用它?
- 策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使得它们可以相互替换。策略模式使得算法可以独立于使用它们的客户端变化而变化。这个模式通过避免条件语句的使用,让代码更加灵活和可扩展。
策略模式的优点
- 避免使用条件语句:策略模式消除了使用大量条件语句的需要(如 if-else 或 switch-case),通过将每种行为封装在独立的策略类中,使得代码更简洁且易于理解。
- 提高代码的可维护性和可扩展性:策略模式将算法与业务逻辑分离,这使得新的策略(算法)可以很容易地添加,而不需要修改现有的代码。这种扩展方式遵循了开闭原则(Open/Closed Principle)。
- 策略可以复用:不同的客户端可以复用相同的策略类,避免代码重复。同时,不同策略之间是独立的,修改某一策略不会影响其他策略。
策略模式的缺点
- 可能导致类数量增加:如果有很多策略类,则会增加类的数量,从而可能导致类管理变得复杂。这在某些情况下可能导致系统设计的复杂性增加。
- 客户端需要了解不同策略:客户端需要知道有哪些策略,并且需要根据不同情况选择合适的策略。对于某些复杂系统,这可能会增加客户端的负担
示例代码
假设我们有一个电商平台,平台根据不同的促销活动选择不同的折扣策略。可以使用策略模式来实现不同的折扣算法。
// 抽象策略类
class DiscountStrategy {
calculate(price) {
throw new Error("This method should be overridden!");
}
}
// 具体策略类 - 正常价格
class RegularPriceStrategy extends DiscountStrategy {
calculate(price) {
return price;
}
}
// 具体策略类 - 打折价格
class PercentageDiscountStrategy extends DiscountStrategy {
constructor(discount) {
super();
this.discount = discount;
}
calculate(price) {
return price * (1 - this.discount);
}
}
// 具体策略类 - 固定金额折扣
class FixedDiscountStrategy extends DiscountStrategy {
constructor(discountAmount) {
super();
this.discountAmount = discountAmount;
}
calculate(price) {
return price - this.discountAmount;
}
}
// 上下文类
class ShoppingCart {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
calculateTotal(price) {
return this.strategy.calculate(price);
}
}
// 使用示例
const cart = new ShoppingCart(new RegularPriceStrategy());
console.log(cart.calculateTotal(100)); // 正常价格: 100
cart.setStrategy(new PercentageDiscountStrategy(0.2));
console.log(cart.calculateTotal(100)); // 打八折: 80
cart.setStrategy(new FixedDiscountStrategy(15));
console.log(cart.calculateTotal(100)); // 减去15元: 85
总结
- 何时使用策略模式: 当你有多种算法或行为可以相互替换,并且需要根据不同条件选择或切换这些算法时,策略模式是一个很好的选择。它使得代码更灵活,易于扩展,且符合开闭原则。
- 优点: 策略模式提供了灵活的设计,减少了条件语句的使用,提高了代码的可维护性和扩展性。
- 缺点: 可能会增加类的数量和系统复杂性,客户端需要知道所有可用策略。