
Python實現(xiàn)五子棋AI對戰(zhàn)的詳細(xì)教程
每條日志消息都有一個LogLevel
,它是由枚舉定義的:
export enum LogLevel {
DEBUG = 0,
INFO = 1,
ERROR = 2,
}
為了簡單起見,我們將 Logger 庫限制為只有三個日志級別。
我們會使用一個抽象LoggerConfig
來定義可能的配置選項:
export abstract class LoggerConfig {
abstract level: LogLevel;
abstract formatter: Type<LogFormatter>;
abstract appenders: Type<LogAppender>[];
}
我們有意將其定義為抽象類,因為接口無法作為 DI 的令牌(token)。該類的一個常量定義了配置選項的默認(rèn)值:
export const defaultConfig: LoggerConfig = {
level: LogLevel.DEBUG,
formatter: DefaultLogFormatter,
appenders: [DefaultLogAppender],
};
LogFormatter
用于在通過LogAppender
發(fā)布日志消息之前對其進(jìn)行格式化:
export abstract class LogFormatter {
abstract format(level: LogLevel, category: string, msg: string): string;
}
與LoggerConfiguration
類似,LogFormatter
是一個抽象類,可以用作令牌。Logger 庫的消費者可以通過提供自己的實現(xiàn)來調(diào)整格式,也可以使用庫提供的默認(rèn)實現(xiàn):
@Injectable()
export class DefaultLogFormatter implements LogFormatter {
format(level: LogLevel, category: string, msg: string): string {
const levelString = LogLevel[level].padEnd(5);
return [${levelString}] ${category.toUpperCase()} ${msg}
;
}
}
LogAppender
是另一個可替換的概念,它會負(fù)責(zé)將日志消息追加到日志中:
export abstract class LogAppender {
abstract append(level: LogLevel, category: string, msg: string): void;
}
默認(rèn)實現(xiàn)會將日志消息打印至控制臺。
@Injectable()
export class DefaultLogAppender implements LogAppender {
append(level: LogLevel, category: string, msg: string): void {
console.log(category + ' ' + msg);
}
}
盡管我們只能有一個LogFormatter
,但是這個庫支持多個LogAppender
。例如,第一個LogAppender
可以將消息寫到控制臺,而第二個可以將消息發(fā)送至服務(wù)器。
為了實現(xiàn)這一點,各個LogAppender
是通過多個提供者(provider)注冊的。所以,Injector 在一個數(shù)組中將它們?nèi)糠祷亍R驗閿?shù)組無法作為 DI 令牌,所以樣例使用了一個InjectionToken
來代替:
export const LOG_APPENDERS = new InjectionToken<LogAppender[]>("LOG_APPENDERS");
LoggserService
本身會通過 DI 來接收LoggerConfig
、LogFormatter
和包含LogAppender
的數(shù)組,并允許為多個LogLevel
記錄日志信息:
@Injectable()
export class LoggerService {
private config = inject(LoggerConfig);
private formatter = inject(LogFormatter);
private appenders = inject(LOG_APPENDERS);
log(level: LogLevel, category: string, msg: string): void {
if (level < this.config.level) {
return;
}
const formatted = this.formatter.format(level, category, msg);
for (const a of this.appenders) {
a.append(level, category, formatted);
}
}
error(category: string, msg: string): void {
this.log(LogLevel.ERROR, category, msg);
}
info(category: string, msg: string): void {
this.log(LogLevel.INFO, category, msg);
}
debug(category: string, msg: string): void {
this.log(LogLevel.DEBUG, category, msg);
}
}
黃金法則
在開始介紹推斷出的模式之前,我想強調(diào)一下提供服務(wù)的黃金法則:
只要有可能,就使用 @Injectable({providedIn: ‘root’})
在庫中有些場景也應(yīng)該使用這種方式,它提供了一些我們想要的特征:簡單、支持搖樹(tree-shakable),并且能夠與懶加載協(xié)作。最后一項特征與其說是 Angular 的優(yōu)點,不如說是底層打包器的優(yōu)點:在懶加載包(bundle)中需要的所有內(nèi)容都會放在這里。
提供者工廠是一個函數(shù),它會為給定的庫返回一個包含提供者的數(shù)組。這個數(shù)組會被轉(zhuǎn)換為 Angular 的EnvironmentProviders
類型,以確保提供者只能在環(huán)境作用域內(nèi)使用,具體來講就是根作用域以及懶路由配置引入的作用域。
Angular 和 NGRX 將這些函數(shù)放在provider.ts
文件中。
如下的提供者函數(shù)(Provider Function)provideLogger
會接收一個LoggerConfiguration
,并使用它來創(chuàng)建一些提供者:
export function provideLogger(
config: Partial<LoggerConfig>
): EnvironmentProviders {
// using default values for missing properties
const merged = { ...defaultConfig, ...config };
return makeEnvironmentProviders([
{
provide: LoggerConfig,
useValue: merged,
},
{
provide: LogFormatter,
useClass: merged.formatter,
},
merged.appenders.map((a) => ({
provide: LOG_APPENDERS,
useClass: a,
multi: true,
})),
]);
}
缺失的配置會使用默認(rèn)配置的值。Angular 的makeEnvironmentProviders
會將Provider
數(shù)組包裝到一個EnvironmentProviders
實例中。
這個函數(shù)允許消費庫的應(yīng)用在引導(dǎo)過程中像使用其他庫(如HttpClient
或Router
)那樣設(shè)置 logger:
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
provideRouter(APP_ROUTES),
[...]
// Setting up the Logger:
provideLogger(loggerConfig),
]
}
Router
和HttpClient
的提供者工廠有第二個可選參數(shù),以提供額外的特性(參見下文的特性模式)。LogFormatter
)。HttpClient
能夠通過with
函數(shù)(參見下文的特性模式)獲取函數(shù)化攔截器的數(shù)組。這些函數(shù)也會被注冊為服務(wù)。提供者工廠會接收一個包含特性對象的可選數(shù)組。每個特性對象都有一個叫做kind
的標(biāo)識符和一個providers
數(shù)組。kind
屬性允許校驗傳入特性的組合。比如,可能會存在互斥的特性,如為HttpClient
同時提供配置 XSRF 令牌處理和禁用 XSRF 令牌處理的特性。
我們的樣例使用了一個著色的特性,為不同的LoggerLevel
顯示不同的顏色:
為了對特性進(jìn)行分類,我們使用了一個枚舉:
export enum LoggerFeatureKind {
COLOR,
OTHER_FEATURE,
ADDITIONAL_FEATURE
}
每個特性都使用LoggerFeature
對象來表示:
export interface LoggerFeature {
kind: LoggerFeatureKind;
providers: Provider[];
}
為了提供著色特性,引入了遵循with Feature
命名模式的工廠函數(shù):
export function withColor(config?: Partial<ColorConfig>): LoggerFeature {
const internal = { ...defaultColorConfig, ...config };
return {
kind: LoggerFeatureKind.COLOR,
providers: [
{
provide: ColorConfig,
useValue: internal,
},
{
provide: ColorService,
useClass: DefaultColorService,
},
],
};
}
提供者工廠通過可選的第二個參數(shù)接收多個特性,它們定義為rest
數(shù)組:
export function provideLogger(
config: Partial<LoggerConfig>,
...features: LoggerFeature[]
): EnvironmentProviders {
const merged = { ...defaultConfig, ...config };
// Inspecting passed features
const colorFeatures =
features?.filter((f) => f.kind === LoggerFeatureKind.COLOR)?.length ?? 0;
// Validating passed features
if (colorFeatures > 1) {
throw new Error("Only one color feature allowed for logger!");
}
return makeEnvironmentProviders([
{
provide: LoggerConfig,
useValue: merged,
},
{
provide: LogFormatter,
useClass: merged.formatter,
},
merged.appenders.map((a) => ({
provide: LOG_APPENDERS,
useClass: a,
multi: true,
})),
// Providing services for the features
features?.map((f) => f.providers),
]);
}
特性中kind
屬性用來檢查和驗證傳入的特性。如果一切正常的話,特性中發(fā)現(xiàn)的提供者會被放到返回的EnvironmentProviders
對象中。
DefaultLogAppender
能夠通過依賴注入獲取著色特性提供的ColorService
:
export class DefaultLogAppender implements LogAppender {
colorService = inject(ColorService, { optional: true });
append(level: LogLevel, category: string, msg: string): void {
if (this.colorService) {
msg = this.colorService.apply(level, msg);
}
console.log(msg);
}
}
由于特性是可選的,DefaultLogAppender
將optional: true
傳入到了inject
中。如果特性不可用的話會遇到異常。除此之外,DefaultLogAppender
還需要對null
值進(jìn)行檢查。
Router
使用了它,比如用來配置預(yù)加載或激活調(diào)試跟蹤。HttpClient
使用了它,比如提供攔截器、配置 JSONP 和配置 / 禁用 XSRF 令牌的處理。Router
和HttpClient
都將可能的特性組合成了一個聯(lián)合類型(如export type AllowedFeatures = ThisFeature | ThatFeature
)。這能夠幫助 IDE 提示內(nèi)置特性。Injector
,并使用它來查找配置了哪些特性。這是對使用optional: true
的一種命令式替換。kind
和providers
屬性上添加了?
前綴,因此將它們聲明成了內(nèi)部屬性。配置提供者工廠能夠擴(kuò)展現(xiàn)存服務(wù)的行為。它們可以提供額外的服務(wù),并使用ENVIRONMENT_INITIALIZER
來獲取所提供的服務(wù)以及要擴(kuò)展的現(xiàn)存服務(wù)的實例。
我們假設(shè)有個擴(kuò)展版本的LoggerService
,可以為每個日志類別定義一個額外的LogAppender
:
@Injectable()
export class LoggerService {
private appenders = inject(LOG_APPENDERS);
private formatter = inject(LogFormatter);
private config = inject(LoggerConfig);
[...]
// Additional LogAppender per log category
readonly categories: Record<string, LogAppender> = {};
log(level: LogLevel, category: string, msg: string): void {
if (level < this.config.level) {
return;
}
const formatted = this.formatter.format(level, category, msg);
// Lookup appender for this very category and use
// it, if there is one:
const catAppender = this.categories[category];
if (catAppender) {
catAppender.append(level, category, formatted);
}
// Also, use default appenders:
for (const a of this.appenders) {
a.append(level, category, formatted);
}
}
[...]
}
為了給某個類別配置LogAppender
,可以引入另外一個提供者工廠:
export function provideCategory(
category: string,
appender: Type<LogAppender>
): EnvironmentProviders {
// Internal/ Local token for registering the service
// and retrieving the resolved service instance
// immediately after.
const appenderToken = new InjectionToken<LogAppender>("APPENDER_" + category);
return makeEnvironmentProviders([
{
provide: appenderToken,
useClass: appender,
},
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue: () => {
const appender = inject(appenderToken);
const logger = inject(LoggerService);
logger.categories[category] = appender;
},
},
]);
}
這個工廠為LogAppender
類創(chuàng)建了一個提供者。但是,我們并不需要這個類,而是需要它的一個實例。同時,我們還需要Injector
解析該示例的依賴。這兩者均需要在通過注入檢索LogAppender
時提供。
確切地講,這是通過ENVIRONMENT_INITIALIZER
實現(xiàn)的,它是綁定到ENVIRONMENT_INITIALIZER
令牌并指向某個函數(shù)的多個提供者。它能夠獲取注入的LogAppender
和LoggerService
。然后,LogAppender
會被注冊到 logger 上。
這樣,就能擴(kuò)展甚至來自父作用域的現(xiàn)有LoggerService
。例如,如下的樣例假設(shè)LoggerService
在根作用域中,而額外的日志級別是在懶加載路由中設(shè)置的:
export const FLIGHT_BOOKING_ROUTES: Routes = [
{
path: '',
component: FlightBookingComponent,
// Providers for this route and child routes
// Using the providers array sets up a new
// environment injector for this part of the
// application.
providers: [
// Setting up an NGRX feature slice
provideState(bookingFeature),
provideEffects([BookingEffects]),
// Provide LogAppender for logger category
provideCategory('booking', DefaultLogAppender),
],
children: [
{
path: 'flight-search',
component: FlightSearchComponent,
},
[...]
],
},
];
@ngrx/store
使用該模式來注冊特性切片(slice)。@ngrx/effects
使用該模式來裝配特性提供的效果。withDebugTracing
特性使用該模式訂閱Router
的events
Observable。NgModules
的現(xiàn)有代碼。EnvironmentProviders
設(shè)置應(yīng)用的部分功能。NgModule 橋是一個通過提供者工廠衍生的 NgModule。為了讓調(diào)用者對服務(wù)有更多的控制權(quán),可以使用像forRoot
這樣的靜態(tài)方法。這些方法也可以接收一個配置對象。
如下的NgModules
以傳統(tǒng)的方式設(shè)置 Logger。
@NgModule({
imports: [/* your imports here */],
exports: [/* your exports here */],
declarations: [/* your delarations here */],
providers: [/* providers, you _always_ want to get, here */],
})
export class LoggerModule {
static forRoot(config = defaultConfig): ModuleWithProviders<LoggerModule> {
return {
ngModule: LoggerModule,
providers: [
provideLogger(config)
],
};
}
static forCategory(
category: string,
appender: Type<LogAppender>
): ModuleWithProviders<LoggerModule> {
return {
ngModule: LoggerModule,
providers: [
provideCategory(category, appender)
],
};
}
}
當(dāng)使用 NgModules 時,這種方式是很常用的,所以消費者可以利用現(xiàn)有的知識和慣例。
當(dāng)同一個服務(wù)被放在多個嵌套的環(huán)境 injector 中時,我們通常只能得到當(dāng)前作用域的服務(wù)實例。因此,在嵌套作用域中,對服務(wù)的調(diào)用無法反映到父作用域中。為了解決這個問題,服務(wù)可以在父作用域中查找自己的實例并將調(diào)用委托給它。
假設(shè)為一個懶加載的路由再次提供了日志庫:
export const FLIGHT_BOOKING_ROUTES: Routes = [
{
path: '',
component: FlightBookingComponent,
canActivate: [() => inject(AuthService).isAuthenticated()],
providers: [
// NGRX
provideState(bookingFeature),
provideEffects([BookingEffects]),
// Providing **another** logger for this part of the app:
provideLogger(
{
level: LogLevel.DEBUG,
chaining: true,
appenders: [DefaultLogAppender],
},
withColor({
debug: 42,
error: 43,
info: 46,
})
),
],
children: [
{
path: 'flight-search',
component: FlightSearchComponent,
},
[...]
],
},
];
在這里,我們在懶加載路由及其子路由中的環(huán)境 injector 中設(shè)置了另外一套 Logger 的服務(wù)。該服務(wù)會屏蔽掉父作用域中對應(yīng)的服務(wù)。因此,當(dāng)懶加載作用域中的組件調(diào)用LoggerService
時,父作用域中的服務(wù)不會被觸發(fā)。
為了防止這種情況,可以從父作用域中獲取LoggerService
。更準(zhǔn)確地說,這不一定是父作用域,而是提供LoggerService
的“最近的祖先作用域”。隨后,該服務(wù)可以委托給它的父服務(wù)。這樣,服務(wù)就被鏈結(jié)起來了。
@Injectable()
export class LoggerService {
private appenders = inject(LOG_APPENDERS);
private formatter = inject(LogFormatter);
private config = inject(LoggerConfig);
private parentLogger = inject(LoggerService, {
optional: true,
skipSelf: true,
});
[...]
log(level: LogLevel, category: string, msg: string): void {
// 1. Do own stuff here
[...]
// 2. Delegate to parent
if (this.config.chaining && this.parentLogger) {
this.parentLogger.log(level, category, msg);
}
}
[...]
}
當(dāng)使用inject
來獲取父 LoggerService 時,我們需要傳遞optional: true
,避免祖先作用域在沒有提供LoggerService
時出現(xiàn)異常。傳遞skipSelf: true
能夠確保只有祖先作用域會被搜索。否則,Angular 會從當(dāng)前作用域開始進(jìn)行搜索,因此會返回調(diào)用服務(wù)本身。
另外,上述的樣例允許通過LoggerConfiguration
中的新標(biāo)記chaining
啟用或停用這種行為。
HttpClient
使用這種模式可以在父作用域中觸發(fā)HttpInterceptor
。關(guān)于鏈?zhǔn)?code>HttpInterceptor的更多細(xì)節(jié),可以參閱 該文。在這里,鏈?zhǔn)叫袨榭梢酝ㄟ^一個單獨的特性來激活。從技術(shù)上講,這個特性注冊了另一個攔截器,將調(diào)用委托給了父作用域中的服務(wù)。庫能夠避免強迫消費者按照給定的接口實現(xiàn)基于類的服務(wù),而是允許使用函數(shù)。在內(nèi)部,它們可以使用useValue
注冊服務(wù)。
在本例中,消費者可以直接傳入一個函數(shù),作為LogFormatter
傳遞給provideLogger
:
bootstrapApplication(AppComponent, {
providers: [
provideLogger(
{
level: LogLevel.DEBUG,
appenders: [DefaultLogAppender],
// Functional CSV-Formatter
formatter: (level, cat, msg) => [level, cat, msg].join(";"),
},
withColor({
debug: 3,
})
),
],
});
為了允許這樣做,Logger 需要使用LogFormatFn
類型來定義函數(shù)的簽名:
export type LogFormatFn = (
level: LogLevel,
category: string,
msg: string
) =>
同時,因為函數(shù)不能用作令牌,所以需要引入InjectionToken
:
export const LOG_FORMATTER = new InjectionToken<LogFormatter | LogFormatFn>(
"LOG_FORMATTER"
);
這個InjectionToken
既支持基于類的LogFormatter
,也支持函數(shù)式的LogFormatter
。
這可以防止破壞現(xiàn)有的代碼。為了支持這兩種情況,providerLogger
需要以稍微不同的方式處理這兩種情況:
export function provideLogger(config: Partial<LoggerConfig>, ...features: LoggerFeature[]): EnvironmentProviders {
const merged = { ...defaultConfig, ...config};
[...]
return makeEnvironmentProviders([
LoggerService,
{
provide: LoggerConfig,
useValue: merged
},
// Register LogFormatter
// - Functional LogFormatter: useValue
// - Class-based LogFormatters: useClass
(typeof merged.formatter === 'function' ) ? {
provide: LOG_FORMATTER,
useValue: merged.formatter
} : {
provide: LOG_FORMATTER,
useClass: merged.formatter
},
merged.appenders.map(a => ({
provide: LOG_APPENDERS,
useClass: a,
multi: true
})),
[...]
]);
}
基于類的服務(wù)是用useClass
注冊的,而對于函數(shù)式服務(wù),則需要使用useValue
。
此外,LogFormatter
的消費者需要為函數(shù)式和基于類的方式進(jìn)行調(diào)整:
@Injectable()
export class LoggerService {
private appenders = inject(LOG_APPENDERS);
private formatter = inject(LOG_FORMATTER);
private config = inject(LoggerConfig);
[...]
private format(level: LogLevel, category: string, msg: string): string {
if (typeof this.formatter === 'function') {
return this.formatter(level, category, msg);
}
else {
return this.formatter.format(level, category, msg);
}
}
log(level: LogLevel, category: string, msg: string): void {
if (level < this.config.level) {
return;
}
const formatted = this.format(level, category, msg);
[...]
}
[...]
}
HttpClient
允許使用函數(shù)式攔截器。它們是通過一個特性注冊的(參見特性模式)。Router
允許使用函數(shù)來實現(xiàn)守衛(wèi)和解析器。原文鏈接: Patterns for Custom Standalone APIs in Angular
Python實現(xiàn)五子棋AI對戰(zhàn)的詳細(xì)教程
2025年AI代碼生成工具Tabnine AI的9個替代者推薦
一步步教你配置Obsidian Copilot實現(xiàn)API集成
如何使用python和django構(gòu)建后端rest api
如何將soap api轉(zhuǎn)換為rest api
如何使用REST API自動化工具提升效率
如何處理REST API響應(yīng)的完整指南
快速上手 Python 創(chuàng)建 REST API
如何用Python結(jié)合Mathpix API高效識別數(shù)學(xué)公式并生成LaTeX代碼