跳到主要内容

装饰器模式

问题

什么是装饰器模式?TypeScript 装饰器和设计模式中的装饰器有什么关系?前端有哪些实际应用?

答案

装饰器模式(Decorator Pattern)动态地给对象添加额外功能,是继承的一种替代方案。它通过包装对象来扩展功能,比继承更加灵活。


核心概念

特点

特点说明
动态扩展运行时添加功能
组合复用多个装饰器可叠加
开闭原则不修改原类
透明性装饰后保持原接口

传统装饰器模式

基础实现

// 组件接口
interface Coffee {
cost(): number;
description(): string;
}

// 具体组件
class SimpleCoffee implements Coffee {
cost() {
return 10;
}

description() {
return '简单咖啡';
}
}

// 装饰器基类
abstract class CoffeeDecorator implements Coffee {
protected coffee: Coffee;

constructor(coffee: Coffee) {
this.coffee = coffee;
}

cost() {
return this.coffee.cost();
}

description() {
return this.coffee.description();
}
}

// 具体装饰器
class MilkDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 3;
}

description() {
return this.coffee.description() + ' + 牛奶';
}
}

class SugarDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1;
}

description() {
return this.coffee.description() + ' + 糖';
}
}

class WhipDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 5;
}

description() {
return this.coffee.description() + ' + 奶油';
}
}

// 使用 - 装饰器可以叠加
let coffee: Coffee = new SimpleCoffee();
console.log(coffee.description(), coffee.cost()); // 简单咖啡 10

coffee = new MilkDecorator(coffee);
console.log(coffee.description(), coffee.cost()); // 简单咖啡 + 牛奶 13

coffee = new SugarDecorator(coffee);
console.log(coffee.description(), coffee.cost()); // 简单咖啡 + 牛奶 + 糖 14

coffee = new WhipDecorator(coffee);
console.log(coffee.description(), coffee.cost()); // 简单咖啡 + 牛奶 + 糖 + 奶油 19

函数装饰器

// 函数式装饰器更符合 JavaScript 风格
type AnyFunction = (...args: unknown[]) => unknown;

// 日志装饰器
function withLogging<T extends AnyFunction>(fn: T): T {
return function (...args: Parameters<T>): ReturnType<T> {
console.log(`调用 ${fn.name},参数:`, args);
const result = fn.apply(this, args);
console.log(`返回:`, result);
return result as ReturnType<T>;
} as T;
}

// 计时装饰器
function withTiming<T extends AnyFunction>(fn: T): T {
return function (...args: Parameters<T>): ReturnType<T> {
const start = performance.now();
const result = fn.apply(this, args);
const end = performance.now();
console.log(`${fn.name} 执行时间: ${(end - start).toFixed(2)}ms`);
return result as ReturnType<T>;
} as T;
}

// 错误处理装饰器
function withErrorHandling<T extends AnyFunction>(fn: T): T {
return function (...args: Parameters<T>): ReturnType<T> | undefined {
try {
return fn.apply(this, args) as ReturnType<T>;
} catch (error) {
console.error(`${fn.name} 执行错误:`, error);
return undefined;
}
} as T;
}

// 使用
function calculate(a: number, b: number): number {
return a + b;
}

const decorated = withLogging(withTiming(calculate));
decorated(1, 2);
// 调用 calculate,参数: [1, 2]
// calculate 执行时间: 0.01ms
// 返回: 3

TypeScript 装饰器

类装饰器

// 类装饰器
function Component(options: { selector: string; template: string }) {
return function <T extends new (...args: unknown[]) => object>(constructor: T) {
return class extends constructor {
selector = options.selector;
template = options.template;
};
};
}

@Component({
selector: 'app-hello',
template: '<h1>Hello</h1>',
})
class HelloComponent {
name = 'Hello';
}

const hello = new HelloComponent();
console.log((hello as unknown as { selector: string }).selector); // app-hello

方法装饰器

// 方法装饰器
function Log(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = function (...args: unknown[]) {
console.log(`[LOG] ${propertyKey} called with:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] ${propertyKey} returned:`, result);
return result;
};

return descriptor;
}

// Debounce 装饰器
function Debounce(delay: number) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let timer: ReturnType<typeof setTimeout>;
const originalMethod = descriptor.value;

descriptor.value = function (...args: unknown[]) {
clearTimeout(timer);
timer = setTimeout(() => {
originalMethod.apply(this, args);
}, delay);
};

return descriptor;
};
}

// Throttle 装饰器
function Throttle(limit: number) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let lastCall = 0;
const originalMethod = descriptor.value;

descriptor.value = function (...args: unknown[]) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
return originalMethod.apply(this, args);
}
};

return descriptor;
};
}

class SearchService {
@Log
search(query: string) {
return `Results for: ${query}`;
}

@Debounce(300)
handleInput(value: string) {
console.log('搜索:', value);
}

@Throttle(1000)
handleScroll() {
console.log('处理滚动');
}
}

属性装饰器

// 属性验证装饰器
function Min(minValue: number) {
return function (target: object, propertyKey: string) {
let value: number;

Object.defineProperty(target, propertyKey, {
get() {
return value;
},
set(newValue: number) {
if (newValue < minValue) {
throw new Error(`${propertyKey} 不能小于 ${minValue}`);
}
value = newValue;
},
});
};
}

function Max(maxValue: number) {
return function (target: object, propertyKey: string) {
let value: number;

Object.defineProperty(target, propertyKey, {
get() {
return value;
},
set(newValue: number) {
if (newValue > maxValue) {
throw new Error(`${propertyKey} 不能大于 ${maxValue}`);
}
value = newValue;
},
});
};
}

function Required(target: object, propertyKey: string) {
let value: unknown;

Object.defineProperty(target, propertyKey, {
get() {
return value;
},
set(newValue: unknown) {
if (newValue === undefined || newValue === null || newValue === '') {
throw new Error(`${propertyKey} 是必填项`);
}
value = newValue;
},
});
}

class User {
@Required
name!: string;

@Min(0)
@Max(150)
age!: number;
}

参数装饰器

// 参数装饰器 - 配合元数据使用
import 'reflect-metadata';

const REQUIRED_KEY = Symbol('required');

function Required(target: object, propertyKey: string, parameterIndex: number) {
const existingRequired: number[] =
Reflect.getMetadata(REQUIRED_KEY, target, propertyKey) || [];
existingRequired.push(parameterIndex);
Reflect.defineMetadata(REQUIRED_KEY, existingRequired, target, propertyKey);
}

function Validate(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = function (...args: unknown[]) {
const requiredParams: number[] =
Reflect.getMetadata(REQUIRED_KEY, target, propertyKey) || [];

for (const index of requiredParams) {
if (args[index] === undefined || args[index] === null) {
throw new Error(`参数 ${index} 是必需的`);
}
}

return originalMethod.apply(this, args);
};

return descriptor;
}

class UserService {
@Validate
createUser(@Required name: string, age?: number) {
return { name, age };
}
}

前端实际应用

1. HOC 高阶组件(React)

import React, { ComponentType, useState, useEffect } from 'react';

// Loading 装饰器
function withLoading<P extends object>(
WrappedComponent: ComponentType<P>,
loadingText = 'Loading...'
) {
return function WithLoadingComponent(props: P & { isLoading?: boolean }) {
const { isLoading, ...rest } = props;

if (isLoading) {
return <div className="loading">{loadingText}</div>;
}

return <WrappedComponent {...(rest as P)} />;
};
}

// 错误边界装饰器
function withErrorBoundary<P extends object>(
WrappedComponent: ComponentType<P>,
FallbackComponent: ComponentType<{ error: Error }>
) {
return class WithErrorBoundary extends React.Component<
P,
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };

static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}

render() {
if (this.state.hasError && this.state.error) {
return <FallbackComponent error={this.state.error} />;
}
return <WrappedComponent {...this.props} />;
}
};
}

// 鉴权装饰器
function withAuth<P extends object>(WrappedComponent: ComponentType<P>) {
return function WithAuthComponent(props: P) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
// 检查认证状态
const checkAuth = async () => {
const token = localStorage.getItem('token');
setIsAuthenticated(!!token);
setIsLoading(false);
};
checkAuth();
}, []);

if (isLoading) {
return <div>检查登录状态...</div>;
}

if (!isAuthenticated) {
return <div>请先登录</div>;
}

return <WrappedComponent {...props} />;
};
}

// 使用
interface UserListProps {
users: string[];
}

const UserList: React.FC<UserListProps> = ({ users }) => (
<ul>
{users.map((user) => (
<li key={user}>{user}</li>
))}
</ul>
);

// 组合多个装饰器
const EnhancedUserList = withAuth(withLoading(UserList));

2. 请求装饰器

// API 装饰器工厂
function createApiDecorators() {
const cache = new Map<string, { data: unknown; expiry: number }>();
const pending = new Map<string, Promise<unknown>>();

// 缓存装饰器
function Cache(ttl: number) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = async function (...args: unknown[]) {
const key = `${propertyKey}_${JSON.stringify(args)}`;
const cached = cache.get(key);

if (cached && cached.expiry > Date.now()) {
return cached.data;
}

const result = await originalMethod.apply(this, args);
cache.set(key, { data: result, expiry: Date.now() + ttl });
return result;
};

return descriptor;
};
}

// 防抖装饰器(请求合并)
function Dedupe() {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = async function (...args: unknown[]) {
const key = `${propertyKey}_${JSON.stringify(args)}`;

if (pending.has(key)) {
return pending.get(key);
}

const promise = originalMethod.apply(this, args);
pending.set(key, promise);

try {
return await promise;
} finally {
pending.delete(key);
}
};

return descriptor;
};
}

// 重试装饰器
function Retry(times: number, delay: number = 1000) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = async function (...args: unknown[]) {
let lastError: Error | undefined;

for (let i = 0; i <= times; i++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
if (i < times) {
console.log(`重试 ${i + 1}/${times}...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}

throw lastError;
};

return descriptor;
};
}

return { Cache, Dedupe, Retry };
}

const { Cache, Dedupe, Retry } = createApiDecorators();

class UserApi {
@Cache(30000)
@Dedupe()
async getUser(id: number) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}

@Retry(3, 1000)
async updateUser(id: number, data: object) {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return response.json();
}
}

3. 表单验证装饰器

import 'reflect-metadata';

const VALIDATORS_KEY = Symbol('validators');

interface Validator {
validate: (value: unknown) => boolean;
message: string;
}

// 验证装饰器
function Validate(validator: Validator) {
return function (target: object, propertyKey: string) {
const validators: Validator[] =
Reflect.getMetadata(VALIDATORS_KEY, target, propertyKey) || [];
validators.push(validator);
Reflect.defineMetadata(VALIDATORS_KEY, validators, target, propertyKey);
};
}

// 常用验证器
function Required(message = '此字段必填') {
return Validate({
validate: (value) => value !== undefined && value !== null && value !== '',
message,
});
}

function MinLength(min: number, message = `最少 ${min} 个字符`) {
return Validate({
validate: (value) => typeof value === 'string' && value.length >= min,
message,
});
}

function MaxLength(max: number, message = `最多 ${max} 个字符`) {
return Validate({
validate: (value) => typeof value === 'string' && value.length <= max,
message,
});
}

function Email(message = '邮箱格式不正确') {
return Validate({
validate: (value) =>
typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
message,
});
}

function Pattern(regex: RegExp, message: string) {
return Validate({
validate: (value) => typeof value === 'string' && regex.test(value),
message,
});
}

// 验证函数
function validate(obj: object): { valid: boolean; errors: Record<string, string[]> } {
const errors: Record<string, string[]> = {};
let valid = true;

for (const key of Object.keys(obj)) {
const validators: Validator[] =
Reflect.getMetadata(VALIDATORS_KEY, obj, key) || [];
const value = (obj as Record<string, unknown>)[key];

for (const validator of validators) {
if (!validator.validate(value)) {
valid = false;
if (!errors[key]) {
errors[key] = [];
}
errors[key].push(validator.message);
}
}
}

return { valid, errors };
}

// 使用
class UserForm {
@Required()
@MinLength(2)
@MaxLength(50)
name: string = '';

@Required()
@Email()
email: string = '';

@Required()
@MinLength(8)
@Pattern(/[A-Z]/, '必须包含大写字母')
@Pattern(/[0-9]/, '必须包含数字')
password: string = '';
}

const form = new UserForm();
form.name = 'J';
form.email = 'invalid';
form.password = 'simple';

const result = validate(form);
console.log(result);
// {
// valid: false,
// errors: {
// name: ['最少 2 个字符'],
// email: ['邮箱格式不正确'],
// password: ['最少 8 个字符', '必须包含大写字母', '必须包含数字']
// }
// }

4. 权限装饰器

// 权限装饰器
const ROLES_KEY = Symbol('roles');

function Roles(...roles: string[]) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
Reflect.defineMetadata(ROLES_KEY, roles, target, propertyKey);
return descriptor;
};
}

function checkPermission<T extends object>(
instance: T,
methodName: keyof T,
currentUserRole: string
): boolean {
const roles: string[] =
Reflect.getMetadata(ROLES_KEY, instance, methodName as string) || [];

if (roles.length === 0) {
return true; // 无权限要求
}

return roles.includes(currentUserRole);
}

// 自动权限检查装饰器
function Protected(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = function (...args: unknown[]) {
const currentUser = getCurrentUser(); // 假设有这个函数
const roles: string[] =
Reflect.getMetadata(ROLES_KEY, target, propertyKey) || [];

if (roles.length > 0 && !roles.includes(currentUser.role)) {
throw new Error('权限不足');
}

return originalMethod.apply(this, args);
};

return descriptor;
}

// 模拟获取当前用户
function getCurrentUser() {
return { role: 'editor' };
}

class AdminService {
@Protected
@Roles('admin')
deleteUser(id: number) {
console.log('删除用户:', id);
}

@Protected
@Roles('admin', 'editor')
updateUser(id: number, data: object) {
console.log('更新用户:', id, data);
}

@Roles('admin', 'editor', 'viewer')
viewUser(id: number) {
console.log('查看用户:', id);
}
}

常见面试问题

Q1: 装饰器模式的优缺点?

答案

优点缺点
动态扩展功能增加系统复杂度
比继承更灵活多层装饰难以调试
符合开闭原则装饰顺序可能影响结果
单一职责-

Q2: 装饰器模式和代理模式的区别?

答案

对比项装饰器模式代理模式
目的增强功能控制访问
关注点添加行为管理访问
叠加可多层叠加通常单层
透明度客户端知道装饰对客户端透明
// 装饰器 - 增强功能
const decorated = withLogging(withCache(originalFn));

// 代理 - 控制访问
const proxy = new Proxy(obj, {
get(target, prop) {
checkPermission();
return target[prop];
},
});

Q3: TypeScript 装饰器执行顺序?

答案

function ClassDecorator1() {
console.log('ClassDecorator1');
return function (target: Function) {
console.log('ClassDecorator1 执行');
};
}

function MethodDecorator1() {
console.log('MethodDecorator1');
return function (target: object, key: string, desc: PropertyDescriptor) {
console.log('MethodDecorator1 执行');
};
}

function MethodDecorator2() {
console.log('MethodDecorator2');
return function (target: object, key: string, desc: PropertyDescriptor) {
console.log('MethodDecorator2 执行');
};
}

@ClassDecorator1()
class Test {
@MethodDecorator1()
@MethodDecorator2()
method() {}
}

// 输出:
// MethodDecorator1
// MethodDecorator2
// MethodDecorator2 执行 (从下往上)
// MethodDecorator1 执行
// ClassDecorator1
// ClassDecorator1 执行

执行规则

  1. 工厂函数从上往下执行
  2. 装饰器从下往上应用
  3. 类装饰器最后执行

Q4: 如何在 React 中使用装饰器?

答案

// 方式1: HOC(推荐)
function withCounter<P extends { count: number }>(
WrappedComponent: React.ComponentType<P>
) {
return function WithCounter(props: Omit<P, 'count'>) {
const [count, setCount] = useState(0);
return <WrappedComponent {...(props as P)} count={count} />;
};
}

// 方式2: 自定义 Hook(推荐)
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = () => setCount((c) => c + 1);
const decrement = () => setCount((c) => c - 1);
return { count, increment, decrement };
}

// 方式3: 类组件装饰器(旧版)
@connect(mapStateToProps) // Redux connect
@withRouter // React Router
class MyComponent extends React.Component {
// ...
}

相关链接