序文
デザインパターンは私たちのプログラミングの道の必然的な部分です。デザインパターンをうまく利用すると、コードの保守性、可読性、スケーラビリティが向上します。これは「エレガンス」の代名詞のようで、すべてのフレームワークとライブラリもその図を見ることができます。
多くの人が開発中に常に特定のデザインパターンをプロジェクトに適用したいと思うのは、そのさまざまな利点のためですが、彼らはしばしばそれを厄介に使用します。その理由の一部は、ビジネス要件が使用されているデザインパターンと一致していないことと、Webプロジェクト内のオブジェクトがSpringフレームワークのIocコンテナーによって管理されており、多くのデザインパターンを直接適用できないことです。 。次に、実際のプロジェクト開発では、フレームワークと組み合わせて実際の開発における実際の利点を十分に活用できるように、デザインパターンを柔軟に変更する必要があります。
プロジェクトでIoCコンテナを導入する場合、通常、依存性注入によって各オブジェクトを使用します。これは、デザインパターンとフレームワークを組み合わせるための鍵です。この記事では、依存性注入を使用して次の3つのデザインパターンを完成させる方法について説明します。
- シングルトンモード
- Chain ofResponsibilityモデル
- 戦略モード
浅いものから深いものまで、いくつかのデザインパターンを理解しながら、依存性注入の魔法の使い方をマスターできます。
この記事のすべてのコードはグループ973961276に配置され、クローンを作成して実行し、効果を確認します。
実際の戦闘
シングルトンモード
シングルトンは、多くの人が最初に接触するデザインパターンである必要があります。他のデザインパターンと比較して、シングルトンの概念は非常に単純です。つまり、プロセスでは、特定のクラスには最初から最後まで1つのインスタンスオブジェクトしかありません。ただし、コンセプトがいかに単純であっても、それを実現するには少しコーディングが必要です。ここではあまり紹介しません。実際の開発でこのモードを使用する方法を見てみましょう。
Spring IoCコンテナによって管理されるオブジェクトはBeansと呼ばれ、各Beanにはスコープがあります。このスコープは、SpringがBeanのライフサイクルを制御する方法として理解できます。作成と破棄はライフサイクルの重要なノードです。シングルトンモードの焦点は、当然、オブジェクトの作成です。Springでオブジェクトを作成するプロセスは、私たちにはわかりません。つまり、Beanを構成してから、依存性注入によってオブジェクトを使用するだけで済みます。
@Service //和@Component功能一样,将该类声明为Bean交由容器管理
public class UserServiceImpl implements UserService{
}
@Controller
public class UserController {
@Autowired // 依赖注入
private UserService userService;
}
このオブジェクトの作成をどのように制御できますか?
実際、Beanのデフォルトのスコープはシングルトンであり、手動でシングルトンを記述する必要はありません。Beanがシングルトンであるかどうかを確認するのは非常に簡単です。プログラムのさまざまな場所でBeanを取得した後、それhashCode
を印刷して同じオブジェクトであるかどうかを確認できます。たとえば、2つの異なるクラスが挿入されUserService
ます。
@Controller
public class UserController {
@Autowired
private UserService userService;
public void test() {
System.out.println(userService.hashCode());
}
}
@Controller
public class OtherController {
@Autowired
private UserService userService;
public void test() {
System.out.println(userService.hashCode());
}
}
印刷結果は両方で同じになりhashCode
ます。
SpringがデフォルトでBeanをインスタンス化するためにシングルトンを使用するのはなぜですか?これは当然、シングルトンでリソースを節約でき、複数のオブジェクトをインスタンス化する必要のないクラスが多数あるためです。
Beanを取得するたびにオブジェクトを作成したい場合はどうなりますか?Bean@Scope
を宣言してスコープを構成するときに、アノテーションを追加できます。
@Service
@Scope("prototype")
public class UserServiceImpl implements UserService{
}
これにより、Beanを取得するたびにインスタンスが作成されます。
Beanにはいくつかのスコープがあり、要件に応じて構成できます。ほとんどの場合、デフォルトのシングルトンを使用します。
名前 | 説明 |
---|---|
シングルトン | デフォルトのスコープ。IoCコンテナごとに作成されるオブジェクトインスタンスは1つだけです。 |
プロトタイプ | 複数のオブジェクトインスタンスとして定義されます。 |
リクエスト | HTTPリクエストのライフサイクルに限定されます。各HTTPクライアント要求には、独自のオブジェクトインスタンスがあります。 |
セッション | HttpSessionのライフサイクルに限定されます。 |
応用 | ServletContextのライフサイクルに限定されます。 |
websocket | WebSocketのライフサイクルに限定されます。 |
ここでもう1つ注意しなければならないのは、Beanのシングルトンは従来の意味で完全にシングルトンではないということです。そのスコープはIoCコンテナにオブジェクトインスタンスが1つしかないことを保証するだけであり、オブジェクトが1つしかないことを保証できないからです。プロセス内のインスタンス。つまり、Springが提供するメソッドを使用してBeanを取得せずに、自分でオブジェクトを作成した場合、プログラムには複数のオブジェクトが含まれます。
public void test() {
// 自己new了一个对象
System.out.println(new UserServiceImpl().hashCode());
}
ここで回避する必要があります。Springは日常の開発の隅々までカバーしていると言えます。Springを意図的にバイパスしない限り、IoCコンテナでシングルトンを確保することは基本的に全体でシングルトンを確保することと同じです。プログラム。
Chain ofResponsibilityモデル
より単純なシングルトンについて説明した後、責任の連鎖モデルを見てみましょう。
モードの説明
パターンは複雑ではありません。リクエストは複数のオブジェクトで処理でき、これらのオブジェクトはチェーンに接続され、オブジェクトが処理するまでリクエストはチェーンに沿って渡されます。このモードの利点は、リクエスターとレシーバーを分離し、処理ロジックを動的に追加および削除できるため、処理オブジェクトの責任に非常に高い柔軟性があることです。私たちの開発で一般的に使用されているフィルターフィルターとインターセプターは、責任連鎖モデルを使用しています。
イントロダクションを見るだけで混乱するだけです。このモードを直接使用する方法を見てみましょう。
例として、職場での休暇承認を取り上げます。休暇申請を開始するとき、通常は複数の承認者がいます。各承認者は責任のノードを表し、独自の承認ロジックを持っています。次の承認者を想定しています。
リーダー:3日以内の休暇のみを承認できます;
マネージャーマネージャー:7日以内の休暇のみを承認
できます;ボス:任意の日数を承認できます。
まず、休暇承認のオブジェクトを定義しましょう。
public class Request {
/**
* 请求人姓名
*/
private String name;
/**
* 请假天数。为了演示就简单按整天来算,不弄什么小时了
*/
private Integer day;
public Request(String name, Integer day) {
this.name = name;
this.day = day;
}
// 省略get、set方法
}
従来の書き方によれば、オブジェクトを受け取った後、受信者は条件付きの判断によって対応する処理を実行します。
public class Handler {
public void process(Request request) {
System.out.println("---");
// Leader审批
if (request.getDay() <= 3) {
System.out.println(String.format("Leader已审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
return;
}
System.out.println(String.format("Leader无法审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
// Manger审批
if (request.getDay() <= 7) {
System.out.println(String.format("Manger已审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
return;
}
System.out.println(String.format("Manger无法审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
// Boss审批
System.out.println(String.format("Boss已审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
System.out.println("---");
}
}
クライアントで承認プロセスをシミュレートします。
public class App {
public static void main( String[] args ) {
Handler handler = new Handler();
handler.process(new Request("张三", 2));
handler.process(new Request("李四", 5));
handler.process(new Request("王五", 14));
}
}
印刷結果は次のとおりです。
---
Leader已审批【张三】的【2】天请假申请
---
Leader无法审批【李四】的【5】天请假申请
Manger已审批【李四】的【5】天请假申请
---
Leader无法审批【王五】的【14】天请假申请
Manger无法审批【王五】的【14】天请假申请
Boss已审批【王五】的【14】天请假申请
---
Handlerクラスのコードが悪臭に満ちていることを確認するのは難しくありません。各責任ノード間の結合は非常に高く、ノードを追加または削除する場合は、この大きなコードを変更する必要がありますが、これは非常に柔軟性がありません。また、ここで示す承認ロジックは、文章を印刷するだけです。実際のビジネスでの処理ロジックは、これよりもはるかに複雑であり、変更するのは大変なことです。
このとき、私たちの責任チェーンモデルが役に立ちます!各責任ノードを独立したオブジェクトにカプセル化し、次にこれらのオブジェクトをチェーンに結合し、統一された入り口を介して1つずつ処理します。
まず、責任ノードのインターフェースを抽象化する必要があり、すべてのノードがこのインターフェースを実装します。
public interface Handler {
/**
* 返回值为true,则代表放行,交由下一个节点处理
* 返回值为false,则代表不放行
*/
boolean process(Request request);
}
このインターフェースを実装する例として、リーダーノードを取り上げます。
public class LeaderHandler implements Handler{
@Override
public boolean process(Request request) {
if (request.getDay() <= 3) {
System.out.println(String.format("Leader已审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
// 处理完毕,不放行
return false;
}
System.out.println(String.format("Leader无法审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
// 放行
return true;
}
}
次に、これらのハンドラーの処理専用のチェーンクラスを定義します。
public class HandlerChain {
// 存放所有Handler
private List<Handler> handlers = new LinkedList<>();
// 给外部提供一个增加Handler的入口
public void addHandler(Handler handler) {
this.handlers.add(handler);
}
public void process(Request request) {
// 依次调用Handler
for (Handler handler : handlers) {
// 如果返回为false,中止调用
if (!handler.process(request)) {
break;
}
}
}
}
次に、責任チェーンを使用して承認プロセスがどのように実行されるかを見てみましょう。
public class App {
public static void main( String[] args ) {
// 构建责任链
HandlerChain chain = new HandlerChain();
chain.addHandler(new LeaderHandler());
chain.addHandler(new ManagerHandler());
chain.addHandler(new BossHandler());
// 执行多个流程
chain.process(new Request("张三", 2));
chain.process(new Request("李四", 5));
chain.process(new Request("王五", 14));
}
}
印刷結果は以前と同じです。
これによってもたらされる利点は明らかです。責任ノードを非常に便利に追加および削除できます。責任ノードのロジックを変更しても、他のノードには影響しません。各ノードは、独自のロジックに注意を払うだけで済みます。また、責任チェーンはノードを固定された順序で実行することであり、各オブジェクトを必要な順序で追加することで、順序を簡単に並べることができます。
さらに、責任チェーンには多くのバリエーションがあります。たとえば、サーブレットフィルタが次のノードを実行するときは、チェーンへの参照も保持する必要があります。
public class MyFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
if (...) {
// 通过链条引用来放行
chain.doFilter(req, resp);
} else {
// 如果没有调用chain的方法则代表中止往下传递
...
}
}
}
さまざまな送信方法に加えて、全体的なリンクロジックも異なる場合があります。
ここで示したのは、リクエストを特定のノードに渡して処理することです。1つが処理されている限り、後続のノードでリクエストを処理する必要はありません。一部の責任チェーンの目的は、処理する特定のノードを見つけることではなく、各ノードが何かを行うことです。これは、組立ラインに相当します。
たとえば、今の承認プロセスのように、アプリケーションが承認される前にすべての承認者が同意する必要がある休暇申請にロジックを変更できます。リーダーが同意した後、承認のためにマネージャーに転送されます。マネージャーが同意した後、承認のためにボスに転送されます。ボスのみが最終的に同意します。有効になるだけです。
多くの形式があり、コアコンセプトはリクエストオブジェクトをチェーンで転送することです。この点から逸脱しなければ、厳密な定義がなくても、責任チェーンモードとしてカウントできます。
フィットフレーム
責任チェーンモデルでは、私たち全員が自分で責任ノードオブジェクトを作成し、それを責任チェーンに追加します。実際の開発では、このように問題が発生します。責任ノードが他のBeanで依存性注入されている場合、オブジェクトを手動で作成すると、オブジェクトはSpringによって管理されず、それらの属性は依存性注入されません。
public class LeaderHandler implements Handler{
@Autowired // 手动创建LeaderHandler则该属性不会被注入
private UserService userService;
}
このとき、各ノードオブジェクトをSpringに渡して管理し、Springを介してこれらのオブジェクトインスタンスを取得してから、これらのオブジェクトインスタンスを責任の連鎖に配置する必要があります。実際、ほとんどの人がこの方法に触れています。SpringMVCのインターセプターは次のように使用されます。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 获取Bean,添加到责任链中(注意哦,这里是调用的方法来获取对象,而不是new出对象)
registry.addInterceptor(loginInterceptor());
registry.addInterceptor(authInterceptor());
}
// 通过@Bean注解将自定义拦截器交由Spring管理
@Bean
public LoginInterceptor loginInterceptor() {return new LoginInterceptor();}
@Bean
public AuthInterceptor authInterceptor() {return new AuthInterceptor();}
}
InterceptorRegistry
これはチェーンクラスと同等です。オブジェクトはSpringMVCによって渡されるため、インターセプターを追加できます。SpringMVCはそれ自体で責任チェーンを呼び出すため、心配する必要はありません。
他の人のフレームワークによって定義された責任チェーンはフレームワークによって呼び出されますが、カスタム責任チェーンをどのように呼び出すのでしょうか。より便利な方法があります。それは、Beanの依存関係をコレクションに挿入することです。
私たちの日常の開発では、依存性注入を使用して単一のBeanを取得します。これは、宣言するインターフェイスまたは親クラスは通常、ビジネス要件を満たすために1つの実装クラスのみを必要とするためです。現在、カスタムHandlerインターフェースの下に複数の実装クラスがあります。現時点では、一度に複数のBeanを注入できます。前のコードを変換してみましょう。
まず、各Handler実装クラスに@Service
注釈を付け、それをBeanとして宣言します。
@Service
public class LeaderHandler implements Handler{
...
}
@Service
public class ManagerHandler implements Handler{
...
}
@Service
public class BossHandler implements Handler{
...
}
次に、チェーンクラスを変換し、それをBeanとして宣言して@Autowired
から、メンバー変数にアノテーションを直接追加します。これはすべて依存性注入によって実現されるため、責任のあるノードを手動で追加する必要はありません。そのため、ノードを追加する以前の方法を削除します。
@Service
public class HandlerChain {
@Autowired
private List<Handler> handlers;
public void process(Request request) {
// 依次调用Handler
for (Handler handler : handlers) {
// 如果返回为false,中止调用
if (!handler.process(request)) {
break;
}
}
}
}
はい、依存性注入は非常に強力で、単一のオブジェクトだけでなく複数のオブジェクトも注入できます。これは非常に便利です。Handlerインターフェースを実装し、実装クラスをBeanとして宣言するだけで、責任チェーンに自動的に挿入されます。手動で追加する必要もありません。責任チェーンを実行するのも非常に簡単です。HandlerChainを取得してから次を呼び出します。
@Controller
public class UserController {
@Autowired
private HandlerChain chain;
public void process() {
chain.process(new Request("张三", 2));
chain.process(new Request("李四", 5));
chain.process(new Request("王五", 14));
}
}
実装効果は次のとおりです。
---
Boss已审批【张三】的【2】天请假申请
---
Boss已审批【李四】的【5】天请假申请
---
Boss已审批【王五】的【14】天请假申请
ねえ、それらはすべて上司によって承認されましたが、最初の2つのノードが有効にならなかったのはなぜですか?Beanがコレクションに注入される順序を構成していない@Order
ため、Beanのアセンブリ順序を制御するために注釈を追加する必要があります。数値が小さいほど、次のようになります。
@Order(1)
@Service
public class LeaderHandler implements Handler{
...
}
@Order(2)
@Service
public class ManagerHandler implements Handler{
...
}
@Order(3)
@Service
public class BossHandler implements Handler{
...
}
このようにして、カスタム責任チェーンモデルはSpringに完全に統合されています。
戦略モード
暑さに乗って、新モデルをご紹介します!
モードの説明
開発中にこのような要件に遭遇することがよくあります。さまざまな状況に応じてさまざまな操作を実行する必要があります。たとえば、私たちの買い物で最も一般的な郵便料金は、地域や製品によって異なります。需要が次のようになっているとします。
送料無料エリア:10KG以下の商品は送料無料、10KG以上の商品は8元。
近隣地域:10KGを超えない商品の場合は8元、10KGを超える商品の場合は16元。
遠隔地:10KGを超えない商品の場合は16元、10KGと15KGを超える商品の場合は24元、15KGを超える商品の場合は32元。
次に、郵便料金を計算する方法はおおよそ次のようになります。
// 为了方便演示,重量和金额就简单设置为整型
public long calPostage(String zone, int weight) {
// 包邮地区
if ("freeZone".equals(zone)) {
if (weight <= 10) {
return 0;
} else {
return 8;
}
}
// 近距离地区
if ("nearZone".equals(zone)) {
if (weight <= 10) {
return 8;
} else {
return 16;
}
}
// 偏远地区
if ("farZone".equals(zone)) {
if (weight <= 10) {
return 16;
} else if (weight <= 15) {
return 24;
} else {
return 32;
}
}
return 0;
}
このような長いコードは、このようないくつかの郵便料金ルールで記述されており、ルールがもう少し複雑な場合は、さらに長くなります。また、ルールが変更された場合、この大きなコードにパッチを適用する必要があり、コードの保守が非常に困難になります。
私たちが最初に考えた最適化方法は、各計算を1つの方法にカプセル化することでした。
public long calPostage(String zone, int weight) {
// 包邮地区
if ("freeZone".equals(zone)) {
return calFreeZonePostage(weight);
}
// 近距离地区
if ("nearZone".equals(zone)) {
return calNearZonePostage(weight);
}
// 偏远地区
if ("farZone".equals(zone)) {
return calFarZonePostage(weight);
}
return 0;
}
これは本当に良いことで、ほとんどの場合ニーズを満たすことができますが、それでも十分な柔軟性はありません。
これらのルールはメソッドにハードコーディングされているため、呼び出し元が独自のルールを使用したい場合、またはルールを頻繁に変更したい場合はどうなりますか?私たちが書いたコードを常に変更できるとは限りません。送料の計算は注文価格の計算のごく一部にすぎないことを知っておく必要があります。サービスを提供するためにいくつかのルールや式を作成できますが、他の人がルールをカスタマイズできるようにする必要もあります。この時点で、郵便料金の計算操作をインターフェイスに高度に抽象化し、計算ルールが異なる場合は異なるクラスを実装する必要があります。異なるルールは異なる戦略を表します。この方法が私たちの戦略モデルです!特定の文章を見てみましょう:
まず、郵便料金計算インターフェースをカプセル化します。
public interface PostageStrategy {
long calPostage(int weight);
}
次に、これらの地域ルールをさまざまな実装クラスにカプセル化し、出荷エリアの例を取り上げます。
public class FreeZonePostageStrategy implements PostageStrategy{
@Override
public long calPostage(int weight) {
if (weight <= 10) {
return 0;
} else {
return 8;
}
}
}
最後に、戦略を適用するための特別なクラスが必要です。
public class PostageContext {
// 持有某个策略
private PostageStrategy postageStrategy = new FreeZonePostageStrategy();
// 允许调用方设置新的策略
public void setPostageStrategy(PostageStrategy postageStrategy) {
this.postageStrategy = postageStrategy;
}
// 供调用方执行策略
public long calPostage(int weight) {
return postageStrategy.calPostage(weight);
}
}
このようにして、発信者は既存の戦略を使用したり、戦略を非常に便利に変更またはカスタマイズしたりできます。
public long calPrice(User user, int weight) {
PostageContext postageContext = new PostageContext();
// 自定义策略
if ("RudeCrab".equals(user.getName())) {
// VIP客户,20KG以下一律包邮,20KG以上只收5元
postageContext.setPostageStrategy(w -> w <= 20 ? 0 : 5);
return postageContext.calPostage(weight);
}
// 包邮地区策略
if ("freeZone".equals(user.getZone())) {
postageContext.setPostageStrategy(new FreeZonePostageStrategy());
return postageContext.calPostage(weight);
}
// 邻近地区策略
if ("nearZone".equals(user.getZone())) {
postageContext.setPostageStrategy(new NearZonePostageStrategy());
return postageContext.calPostage(weight);
}
...
return 0;
}
単純なロジックはLambda式を直接使用してカスタム戦略を完了します。ロジックが複雑な場合は、新しい実装クラスを直接作成して完了することもできます。
これが戦略モードの魅力であり、発信者はさまざまな戦略を使用してさまざまな結果を得ることができ、最大限の柔軟性を実現できます。
多くのメリットがあるにもかかわらず、戦略モデルのデメリットも明らかです。
- 戦略の種類が多すぎる可能性があります。ルールの種類と同じ数です。
- ストラテジーモードは、ロジックをさまざまな実装クラスに分散するだけ
if、else
で、呼び出し元は誰も削減されません。 - 呼び出し元は、既存のロジックを使用するためにすべての戦略クラスを知っている必要があります。
欠点のほとんどはファクトリモードまたはリフレクションで解決できますが、これによりシステムが複雑になります。複雑にならずに欠点を補うことができる解決策はありますか?もちろんあります。これを次に説明します。ストラテジーモードはSpringフレームワークで機能しますが、モード自体の欠点を補うこともできます。
フィットフレーム
責任連鎖モデルを通じて、いわゆる協力フレームワークは、実際にはオブジェクトを管理のためにSpringに渡し、Springを介してBeanを呼び出すことであることがわかります。ストラテジーモードでは、各ストラテジークラスが手動でインスタンス化されるため、最初に行う必要があるステップは、間違いなく、これらのストラテジークラスをBeanとして宣言することです。
@Service("freeZone") // 注解中的值代表Bean的名称,这里为什么要这样做,等下我会讲解
public class FreeZonePostageStrategy implements PostageStrategy{
...
}
@Service("nearZone")
public class NearZonePostageStrategy implements PostageStrategy{
...
}
@Service("farZone")
public class FarZonePostageStrategy implements PostageStrategy{
...
}
次に、これらのBeanを春に取得します。一部の人々は、当然、これらの実装クラスをコレクションに注入してから、使用をトラバースすると考えるかもしれません。これは本当ですが、それはあまりにも面倒です。依存性注入は非常に強力です。Beanをコレクションに注入できるだけでなく、Mapにも注入できます。
特定の使用法を見てください:
@Controller
public class OrderController {
@Autowired
private Map<String, PostageStrategy> map;
public void calPrice(User user, int weight) {
map.get(user.getZone()).calPostage(weight);
}
}
大声で教えてください、さわやかです!ジェーンは簡潔ではありません!優れているかエレガントではない!
依存性注入により、Beanをマップに注入できます。KeyはBeanの名前、ValueはBeanオブジェクトです。これが、以前に@Service
アノテーションに値を設定した理由です。この方法でのみ、呼び出し元は直接Beanを取得できます。get
メソッドBeanをマップしてから、Beanオブジェクトを使用します。
前のPostageContext
クラスは必要ありません。ストラテジーを呼び出したいときはいつでも、呼び出しサイトに直接マップを挿入できます。
このようにして、戦略パターンをSpringフレームワークに完全に統合するだけでなく、if、else
あまりにも多くの問題を完全に解決しました。新しい戦略を追加する場合は、新しい実装クラスを作成してBeanとして宣言するだけで済み、元の呼び出し元はコード行を変更せずに有効にできます。
ヒント:インターフェースまたは親クラスに複数の実装クラスがあるが、単一のオブジェクトの注入のみに依存したい場合は、
@Qualifier("Bean的名称")
アノテーションを使用して指定されたBeanを取得できます。
総括する
この記事では、3つのデザインパターンと、Springフレームワークでの各デザインパターンの使用方法を紹介します。これら3つのデザインパターンに対応する依存性注入の方法は次のとおりです。
- シングルトンモード:単一オブジェクトの依存性注入
- 責任チェーンパターン:依存性注入コレクション
- 戦略モード:依存性注入マップ
デザインパターンをSpringフレームワークと組み合わせる重要なポイントは、パターン内のオブジェクトをSpring管理に渡す方法です。これがこの記事の核心です。はっきりと考えれば、それぞれのデザインパターンを柔軟に使うことができます。
家族の場合は、問題を修正することを歓迎します。この記事のすべてのコードは、グループ973961276に配置され、複製されて実行され、効果が確認されます。