
Python實(shí)現(xiàn)五子棋AI對(duì)戰(zhàn)的詳細(xì)教程
每條日志消息都有一個(gè)LogLevel
,它是由枚舉定義的:
export enum LogLevel {
DEBUG = 0,
INFO = 1,
ERROR = 2,
}
為了簡(jiǎn)單起見(jiàn),我們將 Logger 庫(kù)限制為只有三個(gè)日志級(jí)別。
我們會(huì)使用一個(gè)抽象LoggerConfig
來(lái)定義可能的配置選項(xiàng):
export abstract class LoggerConfig {
abstract level: LogLevel;
abstract formatter: Type<LogFormatter>;
abstract appenders: Type<LogAppender>[];
}
我們有意將其定義為抽象類,因?yàn)榻涌跓o(wú)法作為 DI 的令牌(token)。該類的一個(gè)常量定義了配置選項(xiàng)的默認(rèn)值:
export const defaultConfig: LoggerConfig = {
level: LogLevel.DEBUG,
formatter: DefaultLogFormatter,
appenders: [DefaultLogAppender],
};
LogFormatter
用于在通過(guò)LogAppender
發(fā)布日志消息之前對(duì)其進(jìn)行格式化:
export abstract class LogFormatter {
abstract format(level: LogLevel, category: string, msg: string): string;
}
與LoggerConfiguration
類似,LogFormatter
是一個(gè)抽象類,可以用作令牌。Logger 庫(kù)的消費(fèi)者可以通過(guò)提供自己的實(shí)現(xiàn)來(lái)調(diào)整格式,也可以使用庫(kù)提供的默認(rèn)實(shí)現(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
是另一個(gè)可替換的概念,它會(huì)負(fù)責(zé)將日志消息追加到日志中:
export abstract class LogAppender {
abstract append(level: LogLevel, category: string, msg: string): void;
}
默認(rèn)實(shí)現(xiàn)會(huì)將日志消息打印至控制臺(tái)。
@Injectable()
export class DefaultLogAppender implements LogAppender {
append(level: LogLevel, category: string, msg: string): void {
console.log(category + ' ' + msg);
}
}
盡管我們只能有一個(gè)LogFormatter
,但是這個(gè)庫(kù)支持多個(gè)LogAppender
。例如,第一個(gè)LogAppender
可以將消息寫到控制臺(tái),而第二個(gè)可以將消息發(fā)送至服務(wù)器。
為了實(shí)現(xiàn)這一點(diǎn),各個(gè)LogAppender
是通過(guò)多個(gè)提供者(provider)注冊(cè)的。所以,Injector 在一個(gè)數(shù)組中將它們?nèi)糠祷亍R驗(yàn)閿?shù)組無(wú)法作為 DI 令牌,所以樣例使用了一個(gè)InjectionToken
來(lái)代替:
export const LOG_APPENDERS = new InjectionToken<LogAppender[]>("LOG_APPENDERS");
LoggserService
本身會(huì)通過(guò) DI 來(lái)接收LoggerConfig
、LogFormatter
和包含LogAppender
的數(shù)組,并允許為多個(gè)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);
}
}
黃金法則
在開(kāi)始介紹推斷出的模式之前,我想強(qiáng)調(diào)一下提供服務(wù)的黃金法則:
只要有可能,就使用 @Injectable({providedIn: ‘root’})
在庫(kù)中有些場(chǎng)景也應(yīng)該使用這種方式,它提供了一些我們想要的特征:簡(jiǎn)單、支持搖樹(tree-shakable),并且能夠與懶加載協(xié)作。最后一項(xiàng)特征與其說(shuō)是 Angular 的優(yōu)點(diǎn),不如說(shuō)是底層打包器的優(yōu)點(diǎn):在懶加載包(bundle)中需要的所有內(nèi)容都會(huì)放在這里。
提供者工廠是一個(gè)函數(shù),它會(huì)為給定的庫(kù)返回一個(gè)包含提供者的數(shù)組。這個(gè)數(shù)組會(huì)被轉(zhuǎn)換為 Angular 的EnvironmentProviders
類型,以確保提供者只能在環(huán)境作用域內(nèi)使用,具體來(lái)講就是根作用域以及懶路由配置引入的作用域。
Angular 和 NGRX 將這些函數(shù)放在provider.ts
文件中。
如下的提供者函數(shù)(Provider Function)provideLogger
會(huì)接收一個(gè)LoggerConfiguration
,并使用它來(lái)創(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,
})),
]);
}
缺失的配置會(huì)使用默認(rèn)配置的值。Angular 的makeEnvironmentProviders
會(huì)將Provider
數(shù)組包裝到一個(gè)EnvironmentProviders
實(shí)例中。
這個(gè)函數(shù)允許消費(fèi)庫(kù)的應(yīng)用在引導(dǎo)過(guò)程中像使用其他庫(kù)(如HttpClient
或Router
)那樣設(shè)置 logger:
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
provideRouter(APP_ROUTES),
[...]
// Setting up the Logger:
provideLogger(loggerConfig),
]
}
Router
和HttpClient
的提供者工廠有第二個(gè)可選參數(shù),以提供額外的特性(參見(jiàn)下文的特性模式)。LogFormatter
)。HttpClient
能夠通過(guò)with
函數(shù)(參見(jiàn)下文的特性模式)獲取函數(shù)化攔截器的數(shù)組。這些函數(shù)也會(huì)被注冊(cè)為服務(wù)。提供者工廠會(huì)接收一個(gè)包含特性對(duì)象的可選數(shù)組。每個(gè)特性對(duì)象都有一個(gè)叫做kind
的標(biāo)識(shí)符和一個(gè)providers
數(shù)組。kind
屬性允許校驗(yàn)傳入特性的組合。比如,可能會(huì)存在互斥的特性,如為HttpClient
同時(shí)提供配置 XSRF 令牌處理和禁用 XSRF 令牌處理的特性。
我們的樣例使用了一個(gè)著色的特性,為不同的LoggerLevel
顯示不同的顏色:
為了對(duì)特性進(jìn)行分類,我們使用了一個(gè)枚舉:
export enum LoggerFeatureKind {
COLOR,
OTHER_FEATURE,
ADDITIONAL_FEATURE
}
每個(gè)特性都使用LoggerFeature
對(duì)象來(lái)表示:
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,
},
],
};
}
提供者工廠通過(guò)可選的第二個(gè)參數(shù)接收多個(gè)特性,它們定義為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
屬性用來(lái)檢查和驗(yàn)證傳入的特性。如果一切正常的話,特性中發(fā)現(xiàn)的提供者會(huì)被放到返回的EnvironmentProviders
對(duì)象中。
DefaultLogAppender
能夠通過(guò)依賴注入獲取著色特性提供的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
中。如果特性不可用的話會(huì)遇到異常。除此之外,DefaultLogAppender
還需要對(duì)null
值進(jìn)行檢查。
Router
使用了它,比如用來(lái)配置預(yù)加載或激活調(diào)試跟蹤。HttpClient
使用了它,比如提供攔截器、配置 JSONP 和配置 / 禁用 XSRF 令牌的處理。Router
和HttpClient
都將可能的特性組合成了一個(gè)聯(lián)合類型(如export type AllowedFeatures = ThisFeature | ThatFeature
)。這能夠幫助 IDE 提示內(nèi)置特性。Injector
,并使用它來(lái)查找配置了哪些特性。這是對(duì)使用optional: true
的一種命令式替換。kind
和providers
屬性上添加了?
前綴,因此將它們聲明成了內(nèi)部屬性。配置提供者工廠能夠擴(kuò)展現(xiàn)存服務(wù)的行為。它們可以提供額外的服務(wù),并使用ENVIRONMENT_INITIALIZER
來(lái)獲取所提供的服務(wù)以及要擴(kuò)展的現(xiàn)存服務(wù)的實(shí)例。
我們假設(shè)有個(gè)擴(kuò)展版本的LoggerService
,可以為每個(gè)日志類別定義一個(gè)額外的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);
}
}
[...]
}
為了給某個(gè)類別配置LogAppender
,可以引入另外一個(gè)提供者工廠:
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;
},
},
]);
}
這個(gè)工廠為LogAppender
類創(chuàng)建了一個(gè)提供者。但是,我們并不需要這個(gè)類,而是需要它的一個(gè)實(shí)例。同時(shí),我們還需要Injector
解析該示例的依賴。這兩者均需要在通過(guò)注入檢索LogAppender
時(shí)提供。
確切地講,這是通過(guò)ENVIRONMENT_INITIALIZER
實(shí)現(xiàn)的,它是綁定到ENVIRONMENT_INITIALIZER
令牌并指向某個(gè)函數(shù)的多個(gè)提供者。它能夠獲取注入的LogAppender
和LoggerService
。然后,LogAppender
會(huì)被注冊(cè)到 logger 上。
這樣,就能擴(kuò)展甚至來(lái)自父作用域的現(xiàn)有LoggerService
。例如,如下的樣例假設(shè)LoggerService
在根作用域中,而額外的日志級(jí)別是在懶加載路由中設(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
使用該模式來(lái)注冊(cè)特性切片(slice)。@ngrx/effects
使用該模式來(lái)裝配特性提供的效果。withDebugTracing
特性使用該模式訂閱Router
的events
Observable。NgModules
的現(xiàn)有代碼。EnvironmentProviders
設(shè)置應(yīng)用的部分功能。NgModule 橋是一個(gè)通過(guò)提供者工廠衍生的 NgModule。為了讓調(diào)用者對(duì)服務(wù)有更多的控制權(quán),可以使用像forRoot
這樣的靜態(tài)方法。這些方法也可以接收一個(gè)配置對(duì)象。
如下的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 時(shí),這種方式是很常用的,所以消費(fèi)者可以利用現(xiàn)有的知識(shí)和慣例。
當(dāng)同一個(gè)服務(wù)被放在多個(gè)嵌套的環(huán)境 injector 中時(shí),我們通常只能得到當(dāng)前作用域的服務(wù)實(shí)例。因此,在嵌套作用域中,對(duì)服務(wù)的調(diào)用無(wú)法反映到父作用域中。為了解決這個(gè)問(wèn)題,服務(wù)可以在父作用域中查找自己的實(shí)例并將調(diào)用委托給它。
假設(shè)為一個(gè)懶加載的路由再次提供了日志庫(kù):
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,
},
[...]
],
},
];
在這里,我們?cè)趹屑虞d路由及其子路由中的環(huán)境 injector 中設(shè)置了另外一套 Logger 的服務(wù)。該服務(wù)會(huì)屏蔽掉父作用域中對(duì)應(yīng)的服務(wù)。因此,當(dāng)懶加載作用域中的組件調(diào)用LoggerService
時(shí),父作用域中的服務(wù)不會(huì)被觸發(fā)。
為了防止這種情況,可以從父作用域中獲取LoggerService
。更準(zhǔn)確地說(shuō),這不一定是父作用域,而是提供LoggerService
的“最近的祖先作用域”。隨后,該服務(wù)可以委托給它的父服務(wù)。這樣,服務(wù)就被鏈結(jié)起來(lái)了。
@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
來(lái)獲取父 LoggerService 時(shí),我們需要傳遞optional: true
,避免祖先作用域在沒(méi)有提供LoggerService
時(shí)出現(xiàn)異常。傳遞skipSelf: true
能夠確保只有祖先作用域會(huì)被搜索。否則,Angular 會(huì)從當(dāng)前作用域開(kāi)始進(jìn)行搜索,因此會(huì)返回調(diào)用服務(wù)本身。
另外,上述的樣例允許通過(guò)LoggerConfiguration
中的新標(biāo)記chaining
啟用或停用這種行為。
HttpClient
使用這種模式可以在父作用域中觸發(fā)HttpInterceptor
。關(guān)于鏈?zhǔn)?code>HttpInterceptor的更多細(xì)節(jié),可以參閱 該文。在這里,鏈?zhǔn)叫袨榭梢酝ㄟ^(guò)一個(gè)單獨(dú)的特性來(lái)激活。從技術(shù)上講,這個(gè)特性注冊(cè)了另一個(gè)攔截器,將調(diào)用委托給了父作用域中的服務(wù)。庫(kù)能夠避免強(qiáng)迫消費(fèi)者按照給定的接口實(shí)現(xiàn)基于類的服務(wù),而是允許使用函數(shù)。在內(nèi)部,它們可以使用useValue
注冊(cè)服務(wù)。
在本例中,消費(fèi)者可以直接傳入一個(gè)函數(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
類型來(lái)定義函數(shù)的簽名:
export type LogFormatFn = (
level: LogLevel,
category: string,
msg: string
) =>
同時(shí),因?yàn)楹瘮?shù)不能用作令牌,所以需要引入InjectionToken
:
export const LOG_FORMATTER = new InjectionToken<LogFormatter | LogFormatFn>(
"LOG_FORMATTER"
);
這個(gè)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
注冊(cè)的,而對(duì)于函數(shù)式服務(wù),則需要使用useValue
。
此外,LogFormatter
的消費(fèi)者需要為函數(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ù)式攔截器。它們是通過(guò)一個(gè)特性注冊(cè)的(參見(jiàn)特性模式)。Router
允許使用函數(shù)來(lái)實(shí)現(xiàn)守衛(wèi)和解析器。原文鏈接: Patterns for Custom Standalone APIs in Angular
Python實(shí)現(xiàn)五子棋AI對(duì)戰(zhàn)的詳細(xì)教程
2025年AI代碼生成工具Tabnine AI的9個(gè)替代者推薦
一步步教你配置Obsidian Copilot實(shí)現(xiàn)API集成
如何使用python和django構(gòu)建后端rest api
如何將soap api轉(zhuǎn)換為rest api
如何使用REST API自動(dòng)化工具提升效率
如何處理REST API響應(yīng)的完整指南
快速上手 Python 創(chuàng)建 REST API
如何通過(guò)禁用詞查詢API實(shí)現(xiàn)高效敏感詞過(guò)濾
對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力
一鍵對(duì)比試用API 限時(shí)免費(fèi)