#コーディングのガイドライン

0 フォロワー · 4 投稿

コードを記述するための推奨事項とベストプラクティス

記事 Toshihiko Minamoto · 12月 12, 2022 16m read

こんにちは! 今日は、Angular で最も重要なアーキテクチャパターンの 1 つについてお話しします。

パターン自体は直接 Angular に関連していませんが、Angular はコンポーネント駆動のフレームワークであるため、このパターンは最新の Angular アプリケーションを構築するために最も不可欠なものの 1 つです。

「コンテナ・プレゼンテーション」パターン

コンポーネントは、小さい、目的集中型、独立型、テスト可能性、そして一番重要と言える再利用可能性という特性が備わっているのが最適だと考えられています。

コンポーネントに、サーバー呼び出しを行う、ビジネスロジックが含まれている、他のコンポーネントと密に連携している、他のコンポーネントまたはサービスの内部を過度に知っている、という特徴が備わっていれば、より大きく、テスト、拡張、再利用、変更が行いにくいものになってしまします。 「コンテナ・プレゼンテーション」パターンは、これらの問題を解決するために存在します。

一般的にすべてのコンポーネントは、コンテナコンポーネント(スマート)とプレゼンテーションコンポーネント(ダム)の 2 つのグループに分けられます。

コンテナコンポーネントは、サービスからデータを取得し(ただし、サーバー API を直接呼び出さない)、何らかのビジネスロジックを含み、サービスまたは子コンポーネントにデータを配信することができます。 通常、コンテナコンポーネントは、ルーティング構成でルートが指定されたコンポーネントとして指定されるものです(もちろん、必ずしもそうとは限りません)。

プレゼンテーションコンポーネントは、入力としてデータを取り、何らかの方法で画面に表示することのみを行います。 これらのコンポーネントはユーザー入力に反応しますが、それはローカルで独立した状態を変更することでのみ発生するものです。 アプリの残りの部分とのすべてのやり取りは、カスタムイベントを発行することで行われます。 これらのコンポーネントには非常に高い再利用可能性が備わっている必要があります。

もう少し分かりやすいように、コンテナコンポーネントとプレゼンテーションコンポーネントの例をいくつか挙げましょう。

コンテナ: About ページ、ユーザーページ、管理者パネル、注文ページなど。

プレゼンテーション: ボタン、カレンダー、テーブル、モーダルダイアログ、テーブルビューなど。

めちゃくちゃなボタンの例

コンポーネントアプローチが誤用されている例として、実際のプロジェクトにおいて私自身が体験した非常に悪い例を見てみましょう。

@Component({
  selector: 'app-button',
  template: `<button class="className" (click)=onClick()>{{label}}</button>`
})
export class ButtonComponent {
  @Input() action = '';
  @Input() className = '';
  @Input() label = '';

  constructor(
    private router: Router,
    private orderService: OrderService,
    private scanService: ScanService,
    private userService: UserService
  ) {}

  onClick() {
    if (this.action === 'registerUser') {
      const userFormData = this.userService.form.value;
      // some validation of user data
      // ...
      this.userService.registerUser(userFormData);
    } else if (this.action === 'scanDocument') {
      this.scanService.scanDocuments();
    } else if (this.action === 'placeOrder') {
      const orderForm = this.orderService.form.values;
      // some validation and business logic related to order form
      // ...
      this.orderService.placeOrder(orderForm);
    } else if (this.action === 'gotoUserAccount') {
      this.router.navigate('user-account');
    } // else if ...
  }
}

読みやすいように単純化してありますが、実際にはもっとひどいものです。 これは、ユーザーがクリックして呼び出せるすべての可能なアクションが含まれたボタンコンポーネントです。API 呼び出し、フォームの検証、サービスからの情報取得などを行います。 このようなコンポーネントは、比較的小さなアプリケーションで使用されていたとしても、あっと言う間に地獄入りしてしまうのが想像できるでしょう。 私が見つけて(後でリファクタリングした)このようなボタンコンポーネントのコードは、2000 行以上にも及ぶものでした。 あり得ません!

それを書いた開発者に、すべてのロジックを 1 つのコンポーネント収めた理由を尋ねたところ、本人が行ったのは「カプセル化」だという答えが返ってきました。🙀

理想的なコンポーネントに備わっているべき特性について思い出しましょう。

小さい - 2000 行以上のコードでできたこのボタンは小さくありません。 また、他のアクション用に別のボタンが必要という人が現れるたびに、コンポーネントが膨れ上がってしまうでしょう。

目的集中型 - このボタンはまったく無関係な多くのアクションを実行するため、目的集中型とは呼べません。

独立型 - このボタンはいくつかのサービスとフォームと密に連携しているため、いずれかを変更すると、ボタンに影響があります。

テスト可能性 - ノーコメントです。

再利用可能性 - まったく再利用できません。 使用するたびに、含まれていないアクションを追加するためにコンポーネントのコードを変更する必要があります。さらに、このボタンの不要なアクションや依存関係もすべて含まれることになります。

さらに、このボタンコンポーネントは、ネイティブの HTML ボタンを内部的に非表示にするため、開発者はそのプロパティにアクセスできません。 これが、コンポーネントの悪い書き方としての良い例です。

このボタンコンポーネントを使用する非常に単純なコンポーネントの例を示し、コンテナ・プレゼンテーションパターンでリファクタリングしてみましょう。

@Component({
  selector: 'app-registration-form',
  template: `<form [formGroup]="userService.form">
  <input type="text" [formControl]="userService.form.get('username')" placeholder="Nickname">
  <input type="password" [formControl]="userService.form.get('password')" placeholder="Password">
  <input type="password" [formControl]="userService.form.get('passwordConfirm')" placeholder="Confirm password">
  <app-button className="button accent" label="Register" action="registerUser"></app-button>
</form>
`
})
export class RegistrationFormComponent {
  constructor(public userService: UserService) {}
}

このコンポーネントの中には何もロジックがありません。フォームはサービスに格納されており、ボタンにはクリックによるすべてのロジックの呼び出しが含まれます。 現時点では、ボタンにその動作に関係のないすべてのロジックが含まれており、ボタンが処理するアクションに直接関係している親コンポーネントよりスマートです。

コンテナ・プレゼンテーションパターンでボタンコンポーネントをリファクタリングする

これらの 2 つのコンポーネントの関数を分離しましょう。 ボタンはプレゼンテーションコンポーネント、つまり小さく再利用可能である必要があります。 このボタンを含む登録フォームは、ビジネスロジックやサーバーレイヤーとのやり取りを保持するコンテナコンポーネントとすることができます。

ネイティブボタンを表示する部分は説明せずに(これについては、おそらく今後の記事で話すことにします)、主に 2 つのコンポーネントのアーキテクチャ上の関係に焦点を当てます。

リファクタリングしたボタンコンポーネント(プレゼンテーション)

@Component({
  selector: 'app-button',
  template: `<button class="className" (click)=onClick()>{{label}}</button>`
})
export class ButtonComponent {
  @Input() className = '';
  @Input() label = '';

  @Output() click: EventEmitter = new EventEmitter();

  onClick() {
     this.click.emit();
  }
}

ご覧のとおり、リファクタリングしたボタンは非常に簡潔です。 2 つの入力と 1 つの出力しか含まれません。 入力は、親コンポーネントからデータを取ってユーザーにそれを表示するために使用します(ボタンの外観はクラスと表示ボタンのラベルで変更します)。 出力は、ユーザーがボタンをクリックするたびに発行されるカスタムイベントに使用されます。

このコンポーネントは小さく、目的集中型、独立型、テスト可能性、および再利用可能性の特性を備えています。 コンポーネント自体の動作に無関係なロジックは含まれていません。 アプリケーションの残りの内部動作についてまったく認識しておらず、アプリケーションのあらゆる部分または他のアプリケーションにも安全にインポートして使用することが可能です。

リファクタリングした登録フォームコンポーネント(コンテナ)

@Component({
  selector: 'app-registration-form',
  template: `<form [formGroup]="userService.form">
  <input type="text" [formControl]="userService.form.get('username')" placeholder="Nickname">
  <input type="password" [formControl]="userService.form.get('password')" placeholder="Password">
  <input type="password" [formControl]="userService.form.get('passwordConfirm')" placeholder="Confirm password">
  <app-button className="button accent" label="Register" (click)="registerUser()"></app-button>
</form>
`
})
export class RegistrationFormComponent {
  constructor(public userService: UserService) {}

  registerUser() {
    const userFormData = this.userService.form.value;
    // some validation of user data
    // ...
    this.userService.registerUser(userFormData);
  }
}

この登録フォームは、ボタンを使用し、ボタンクリックに反応して registerUser メソッドを呼び出すようになりました。 このメソッドのロジックはこのフォームに密に関連しているため、それを配置する最適な場所と言えます。

これは非常に単純な例であり、コンポーネントツリーにあるレベルは 2 つだけです。 このパターンには、コンポーネントツリーのレベルが多い場合に落とし穴があります。

より高度な例

これは実際の例ではありませんが、このパターンに潜在する問題を理解する上で役立つと思います。

以下のようなコンポーネントツリーがあるとします(上から下)。

user-orders - トップレベルのコンポーネント。 サービスレイヤーに話しかけ、ユーザーとその注文に関するデータを受け取り、それを後続のツリーに渡して注文のリストをレンダリングするコンテナコンポーネントです。

user-orders-summary - 中間レベルのコンポーネント ユーザーの注文リストの上に合計注文数を表示するバーをレンダリングするプレゼンテーションコンポーネントです。

cashback - 最下レベル(リーフ/葉)コンポーネント。 ユーザーの合計キャッシュバック額を表示し、銀行口座に引き落とすためのボタンを持つプレゼンテーションコンポーネントです。

トップレベルのコンテナコンポーネント

トップレベルの user-orders コンテナコンポーネントを見てみましょう。

@Component({
  selector: 'user-orders',
  templateUrl: './user-orders.component.html'
})
export class UserOrdersComponent implements OnInit {
  user$: Observable<User>;
  orders$: Observable<Order[]>;

  constructor(
    private ordersService: OrdersService,
    private userService: UserService
  ) {}

  ngOnInit() {
      this.user$ = this.userService.user$;
      this.orders$ = this.ordersService.getUserOrders();
  }

onRequestCashbackWithdrawal() {
    this.ordersService.requestCashbackWithdrawal()
      .subscribe(() => /* notification to user that cashback withdrawal has been requested */);
    }
}
<div class="orders-container">
    <user-orders-summary
        [orders]="orders$ | async"
        [cashbackBalanace]="(user$  | async).cashBackBalance"
        (requestCashbackWithdrawal)="onRequestCashbackWithdrawal($event)"
    >
    </user-orders-summary>

    <div class="orders-list">
      <div class="order" *ngFor="let order of (orders$ | async)"></div>
    </div>
</div>

ご覧のとおり、user-orders コンポーネントは、user$orders$ の 2 つの Obeservable を定義し、非同期パイプをテンプレートに使用してそれらを購読しています。 このコンポーネントはデータをプレゼンテーションコンポーネントの user-orders-summary に渡し、注文リストをレンダリングします。 また、user-orders-summary から発行される requestCashbackWithdrawal カスタムイベントに反応してサービスレイヤーに話しかけます。

中間レベルのプレゼンテーションコンポーネント

@Component({
  selector: 'user-orders-summary',
  template: `
    <div class="total-orders">Total orders: {{orders?.length}}</div>
    <cashback [balance]="cashbackBalanace" (requestCashbackWithdrawal)="onRequestCashbackWithdrawal($event)"></cashback>
    `, 
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserOrdersSummaryComponent {
    @Input() orders: Order[];
    @Input() cashbackBalanace: string;

    @Output() requestCashbackWithdrawal = new EventEmitter();

    onRequestCashbackWithdrawal() {
        this.requestCashbackWithdrawal.emit();
    }
}

このコンポーネントは、上記でリファクタリングしたボタンコンポーネントに非常に似ています。 その入力から受け取ったデータをレンダリングし、何らかのユーザーアクションでカスタムイベントを発行します。 サービスの呼び出しは行わず、ビジネスロジックも含まれません。 そのため、これは別のプレゼンテーション cashback コンポーネントを使用する純粋なプレゼンテーションコンポーネントです。

最下レベルのプレゼンテーションコンポーネント

@Component({
    selector: 'cashback',
    template: `
<div class="cashback">
  <span class="balance">Your cashback balance: {{balance}}</span>
  <button class="button button-primary" (click)="onRequestCashbackWithdrawal()">Withdraw to Bank Account</button>
</div>
`,
    styleUrls: ['./cashback.component.css']
})
export class CashackComponent {
    @Input() balance: string;

    @Output() requestCashbackWithdrawal = new EventEmitter();

    onRequestCashbackWithdrawal() {
        this.requestCashbackWithdrawal.emit();
    }
}

これは、入力からデータを受け取って出力でイベントをスローするだけの、もう 1 つのプレゼンテーションコンポーネントです。 非常に単純で再利用可能ですが、コンポーネントツリーに問題があります

user-orders-summary コンポーネントと cashback コンポーネントには、類似する入力(cashbackBalanacebalance)と同じ出力(requestCashbackWithdrawal)があることに気づいたことでしょう。 これは、コンテナコンポーネントが一番深いプレゼンテーションコンポーネントから遠すぎるためです。 この設計ではツリーのレベル数が増えるほど、問題が悪化してしまいます。 この問題について詳しく見てみましょう。

問題 1 - 中間レベルのプレゼンテーションコンポーネントにある無関係なプロパティ

user-orders-summarycashbackBalanace の入力を受け取って、さらにツリーの下の方に渡すだけであり、それ自体がその入力を使用することはありません。 このような状況に直面した場合、コンポーネントツリーの設計に欠陥がある可能性があります。 実際の状況で使用するコンポーネントには多数の入力と出力があるため、この設計では多数の「プロキシ」入力が含まれてしまい、中間レベルのコンポーネントの再利用可能性が薄れてしまいます(子コンポーネントに密に連携させる必要があるため)。また、繰り返し可能なコードが多数含まれてしまいます。

問題 2 - 下方レベルからトップレベルのコンポーネントに湧き上がるカスタムイベント

この問題は、前の問題に非常に似ていますが、コンポーネントの出力に関連しています。 ご覧のとおり、requestCashbackWithdrawal カスタムイベントは、cashbackuser-orders-summary コンポーネントで繰り返されています。 これもやはり、コンテナコンポーネントが一番深いプレゼンテーションコンポーネントから遠すぎるために起きていることです。 また、中間コンポーネント自体を再利用できないものにしてしまっています。

これらの問題に対する潜在的な解決策は少なくとも 2 つあります。

1 - ngTemplateOutlet を使用して、中間レベルのコンポーネントをよりコンテンツに依存しないコンポーネントにし、より深いコンポーネントをコンテナコンポーネントに直接公開する。 この解決策には専用の記事にするだけの価値があるため、今回はこれについて説明しないでおきます。

2 - コンポーネントツリーを再設計する。

コンポーネントツリーをリファクタリングする

作成したコードをリファクタリングして、無関係なプロパティと中間レベルのコンポーネントで湧いているイベントの問題を解決できるか見てみましょう。

リファクタリングしたトップレベルのコンポーネント

@Component({
  selector: 'user-orders',
  templateUrl: './user-orders.component.html'
})
export class UserOrdersComponent implements OnInit {
  orders$: Observable<Order[]>;

  constructor(
    private ordersService: OrdersService,
  ) {}

  ngOnInit() {
      this.orders$ = this.ordersService.getUserOrders();
  }
}
<div class="orders-container">
    <user-orders-summary [orders]="orders$ | async"></user-orders-summary>

    <div class="orders-list">
      <div class="order" *ngFor="let order of (orders$ | async)"></div>
    </div>
</div>

トップレベルのコンテナコンポーネントから user$ Observable と onRequestCashbackWithdrawal() メソッドを削除しました。 非常に単純化され、user-orders-summary コンポーネント自体をレンダリングするために必要なデータのみを渡し、その子コンポーネントの cashback には渡さないようになっています。

リファクタリングした中間レベルのコンポーネント

@Component({
  selector: 'user-orders-summary',
  template: `
    <div class="total-orders">Total orders: {{orders?.length}}</div>
    <cashback></cashback>
    `, 
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserOrdersSummaryComponent {
    @Input() orders: Order[];
}

これもかなり単純化されています。 入力は 1 つだけで、合計注文数をレンダリングするようになっています。

リファクタリングした最下レベルのコンポーネント

@Component({
    selector: 'cashback',
    template: `
<div class="cashback">
  <span class="balance">Your cashback balance: {{ (user$ | async).cashbackBalance }}</span>
  <button class="button button-primary" (click)="onRequestCashbackWithdrawal()">Withdraw to Bank Account</button>
</div>
`,
    styleUrls: ['./cashback.component.css']
})
export class CashackComponent implements OnInit {
  user$: Observable<User>;

  constructor(
     private ordersService: OrdersService,
     private userService: UserService
  ) {}

  ngOnInit() {
    this.user$ = this.userService.user$;
  }

  onRequestCashbackWithdrawal() {
    this.ordersService.requestCashbackWithdrawal()
      .subscribe(() => /* notification to user that cashback withdrawal has been requested */);
    }
  }
}

すごいです。 ご覧のとおり、プレゼンテーションコンポーネントではなくなっています。 トップレベルのコンポーネントに非常に似ており、コンポーネントツリーの下にあるコンテナコンポーネントになりました。 このようにリファクタリングすることで、コンポーネントツリー全体の設計と API、およびコンポーネントツリーの上位にある 2 つのコンポーネントのロジックを単純化できました。

新しい cashback コンポーネントの再利用可能性についてはどうでしょうか? まだ、再利用可能なままです。それには、コンポーネント自体に関連するロジックのみが含まれているため、以前として独立性を維持しています。

新しいコンポーネントツリーの設計は、管理性、合理性、および細分化がさらに改善されているようです。 湧き上がってくるイベントが無くなり、コンポーネントツリー全体で繰り返し可能な入力もありません。総合的な設計もはるかに単純になっています。 これは、コンポーネントツリーの下にコンテナコンポーネントを追加することで達成しました。 この手法は、コンポーネントツリーの設計を単純化するために使用できますが、再利用可能性を大幅に失うことなく、ツリー内のどのコンポーネントをコンテナにするのが適しており、どれが純粋なプレゼンテーションコンポーネントであるべきかを十分に理解しておくことが必要です。 これはバランスと設計の選択に関する課題であり、アプリのアーキテクチャを作成するときには必ず発生するものです。

「コンテナ・プレゼンテーション」パターンは非常に誤解しやすいもので、コンテナコンポーネントは必ずトップレベルコンポーネントであるべきだと考えがちです(直感的に、コンテナはローカルコンポーネントツリー内のその他すべてのコンポーネントを格納するものです)。 しかし、そうではありません。コンテナコンポーネントはコンポーネントツリーのあらゆるレベルに配置することができます。上記で見たように、リーフレベルにも配置可能です。 私はコンテナコンポーネントをスマート(賢い)コンポーネントと呼んでいます。私にとって、これらのコンポーネントがビジネスロジックを含み、コンポーネントツリーのあらゆる場所に配置できるということが非常に明白であるためです。

後書き

ここまでで、「コンテナ・プレゼンテーション」パターンの概要とその実装における潜在的な問題について理解できたことと思います。

できるだけ単純に維持するよう努めましたが、このパターンに関しては非常に多くの情報が存在します。

ご質問やメッセージがございましたら、お気軽にコメント欄に投稿してください。

次回の記事では、Angular における changeDetectionStrategy についてお話しします(この記事に非常に関連する内容です)。

それではまた!

0
0 764
記事 Toshihiko Minamoto · 11月 29, 2022 11m read

中・上級トピックに進む前に、より一般的なポイントについてまとめておきたいと思います。 これはもちろん主観的な内容であるため、他の意見やさらに良い根拠をお持ちであれば、それについて喜んでお聞きします。

このリストは包括的ではありません。一部のトピックは今後の記事で対応するつもりなので、意図的にそうしています。

ヒント 1. 公式スタイルガイドに従う

Angular は、アプリケーションに使用可能なアーキテクチャを制限するという点で非常に厳格ですが、それでも独自の方法で行えることはたくさんあります。 開発者の想像力は無限ではありますが、そのために、あなたと、またはあなたを引き継いでプロジェクトに携わる人の作業が困難になってしまうことがあります。

Angular チームは、Angular アーキテクチャ自体とそのライブラリをうまく管理しているため、安定した対応可能なコードベースを作成する方法をよく理解しています。

したがって、公式スタイルガイドに従い、そのとおりに動作しない場合にのみ他の方法を取るようにすることをお勧めします。 こうすることで、新しいプロジェクトに参加する際や、他の人がプロジェクトに参加する際に、事がより簡単に進められるようになります。

コードとアプリアーキテクチャが対応可能で安定しており、理解しやすい方が、賢明でありながらも暗号的なソリューションを作るよりも重要です。誰も追いつけなくなってしまいます(開発者自身が後に追いつけなくなる可能性もあります)。

Angular 公式スタイルガイド: https://angular.io/guide/styleguide

ヒント 2. Angular の Ninja 本を購入する

宣伝と思われるかもしれませんが、つまりこういうことです。Angular のすべての主要概念がカバーされた非常に良い本で、価格はあなたが決められます。 また、価格のどれくらいが作者に支払われ、どれくらいが慈善活動に募金されるかを決めることもできます。

この本の作者の 1 人は Angular チームのメンバーであるため、公式ドキュメントの次に最も信頼できる Angular の情報源であることは間違いありません。 本の章構成は、本のページで確認できるようになっているため、サンプルページを読んでから購入する価値があるかを決めることができます。

さらに、この本は Angular の新しいバージョンのリリースで更新されており、本のすべての更新内容を無料で得られます。

Ninja Squad ブログ自体も Angular に関する非常に優れた情報源で、新しいバージョンに関する記事や最新情報、ベストプラクティス、実験的機能などが掲載されています。

Angular の Ninja 本: https://books.ninja-squad.com/angular

ヒント 3. 公式ドキュメントを読む

アプリのコードを書き始める前、特に、以前に使用したことのないバージョンの Angular でプロジェクトに取り組む場合には、公式ドキュメントとガイドに目を通すことをお勧めします。 廃止機能は常に増えており、インターネット上のチュートリアルとガイドが古くなっている可能性があります。そのため、新しいベストプラクティスや機能を使用する代わりに、技術的負債が増え続けてしまうことになりかねません。

ガイドがどのバージョンを対象にして書かれているのかを確認するのも良い習慣です。 使用しているのが 2 つ以上前のバージョンであれば、それが書かれた当時から変更されたことがないかを確認することをお勧めします。

公式ドキュメント: https://angular.io

ヒント 4. Angular Material を使用しなくても、Angular CDK の使用を検討する

これについては今後の記事に取り上げるのが良いと思いますが、私は、多くの Angular 開発者は Angular CDK の存在すらも知らないことを知っています。

Angular CDK は、より優れたアプリケーションの開発を支援できる便利なディレクティブと基底クラスが集められたライブラリです。 たとえば、FocusTrap、Drag & Drop、VirtualScroll などが含まれており、コンポーネントに簡単に追加することができます。

Angular CDK: https://material.angular.io/cdk/categories

ヒント 5. package.json 内の依存関係を修正する

特に Angular に関連することではありませんし、どんなプロジェクトでも重要なことかもしれません。 npm install --save <something> を実行すると、パッケージバージョンの先頭に ^ または ~ 付きで package.json に追加されます。 つまり、プロジェクトはその依存関係と同じメジャーバージョンのあらゆるマイナー/パッチバージョンを使用できるということになります。 この場合、将来的にほぼ 100% の確率で問題となります。 私はさまざまなプロジェクトにおいて何度もこの問題に直面しました。 時間が経過し、ある依存関係の新しいマイナーバージョンが作成されれば、アプリはビルドできなくなります。 この依存関係の作成者がそれをマイナー(またはパッチ)バージョンで更新したために、ビルドと競合するようになってしまうこともあるでしょう。 また、依存関係の新しいマイナーバージョンで新しいバグが見つかることもあります。自分のコードでは問題がなくても(新しいバージョンがリリースされる前に依存関係をインストールしたため)、リポジトリから自分のプロジェクトをインストールしてビルドしようとしている人たちが、自分が気付いていないバグに直面することもあります(また、自分のマシンでそのバグを再現することもできません)。

package-lock.json は、この問題を解決し、複数の開発者がプロジェクト全体で同じ依存関係セットを使用できるようにするために存在します。 リポジトリにコミットすることが理想的ですが、このファイルを .gitignore に追加している開発者がたくさんいます。 それでは意味がなく、結局、上記と同じ問題が起きてしまいます。 そのため、依存関係の解決を盲目的に信頼して特定のバージョンに固定するのではなく、定期的に脆弱性をスキャンして(npm audit を使用)、手動で更新してテストすることをお勧めします。

ヒント 6. できるだけ早期にアプリを新しい Angular バージョンにアップグレードするようにする

Angular は絶えず進化しており、約半年ごとに新しいバージョンがリリースされています。 アプリを最新状態に維持することで、ビルドの高速化やその他の最適化を含むすべての新機能を使用することができます。 この場合には、Angular の次期メジャーバージョンへのアップグレードは大した問題ではないでしょう。 ただし、Angular には、Angular の中間バージョンをスキップしてアプリをアップグレードするためのスケマティックがないため、5 バージョン前の Angular を使用しており、アプリケーションの規模が中~大である場合には、最新バージョンへの更新プロセスが困難になる可能性があります。 各バージョンでアプリケーションが動作することを確認しながら、すべての中間バージョンを 1 つずつアップグレードすることが必要になるでしょう。 Angular CLI のスケマティックを使わずに直接アップデートすることは可能ですが、コツが必要となるため、常にアプリを最新状態に維持しておくことをお勧めします。

ヒント 7. 依存関係リストをできるだけ短くする

新機能が必要となるたびに新しい依存関係をアプリに採り入れるという習慣に陥りやすいものですが、 依存関係ごとに、アプリの複雑性が増し、技術的負債が突然雪崩れてしまうリスクが高まります。

アプリに新しい依存関係を追加すると決めた場合は、以下のことを検討してください。

  • どれほど十分に依存関係がサポートされているか?
  • どれくらいの人が使用しているか?
  • 開発チームはどれくらいの規模か?
  • どれくらい迅速に GitHub 課題をクローズしているか?
  • どれくらい十分に依存関係のドキュメントが書かれているか?
  • Angular の新しいメジャーバージョンがリリースされてからどれくらい迅速に更新されているか?
  • この依存関係によるアプリケーションのパフォーマンスとバンドルサイズへのインパクトはどれくらいか?
  • 将来的に放置または廃止された場合に、この依存関係を入れ替えるのがどれくらい困難になるか?

この質問のいくつかに否定的な回答がある場合、その依存関係を排除するか、少なくとももっと成熟して十分にサポートされているものに交換することを検討してください。

ヒント 8. 「暫定的な」型として <any> を使用しない

繰り返しますが、適切な型を書くには時間がかかるものであり、このスプリントでタスクを完了させる必要があるため、ビジネスロジックを書く際に簡単に any を使いがちです。 もちろん、型は後で追加できますが、 技術的負債がすぐに積もる危険があります。

アプリと型のアーキテクチャは、ビジネスロジックを書く前に定義しておく必要があります。 どのようなオブジェクトがあり、どこで使用されるかを明確に理解しておくかなければなりません。 コードの前に仕様書、ですよね? (Tom Demarco は、この考えが主流になる前にこれについての本を書いています: https://www.amazon.com/Deadline-Novel-About-Project-Management/dp/0932633390

事前に定義された型を使わずにコードを書いている場合、非常に似ているようで異なるオブジェクトを使用する、質の悪いアプリアーキテクチャと関数が出来上がってしまう可能性があります。 そのため、関数ごとに異なる型を作成する(余計に悪くなってしまいます)か、仕様書、型、およびリファクタリングを書くことに時間を費やす必要があるでしょう。後者は、前もってやっていれば時間の無駄になりません。

ヒント 9. ビルドプロセスを理解するのに時間をかける

Angular は、プロジェクトの構築に関する開発者の作業をうまく容易にしていますが、 デフォルトのオプションは、すべての場合に必ずしも最適ではありません。

Angular のビルドプロセスの仕組み、開発ビルドと本番ビルドの違い、Angular で提供されているビルドのオプション(最適化、ソースマップ、バンドル化など)を理解する時間を取りましょう。

ヒント 10. バンドル内のものを調べる

すべてのライブラリがツリーシェイキングを提供しているわけでも、必ずしもすぐにインポートするわけでもありません。そのため、冗長するものがアプリにバンドルされる可能性が必ずあります。

したがって、たまにバンドルの中身を調べる習慣をもつことをお勧めします。

webpack-bundle-analyzer を使ったプロセスをうまく説明した記事がいくつかあるため、ここでは説明しませんが、その中の 1 つのリンクをご覧ください: https://www.digitalocean.com/community/tutorials/angular-angular-webpack-bundle-analyzer

このトピックについては、連載の後の方でより詳しく説明します。

ヒント 11. モジュールにサービスをインポートする代わりに、サービスの providedIn プロパティを使用する

インターネットに公開されている Angular コードサンプルの多くは、モジュールへのサービスのインポートを使用しています。 ただし、Angular 6 または 7 以降では、これはサービスを宣言する方法として推奨されていません。 ツリーシェイキングを有効にし、アプリケーションのバンドル化を改善するには、@Injectable デコレータのプロパティ providedIn を使用する必要があります。 Angular は、どのバンドルにサービスを含める必要があるのか、いつ初期化すべきなのか、サービスのインスタンスをいくつ作成する必要があるのかを理解できるほどスマートなフレームワークです。

providedIn が受け入れる値には 3 つあります。 ほとんどの場合は root で十分ですが、そのほかに 2 つあります。

  • root: サービスアプリケーションの範囲でシングルトンになります。
  • any: すべての Eager ロードされたモジュールにサービスのインスタンスが 1 つ作成され、遅延モジュールごとに別のインスタンスが作成されます。
  • platform: 同じページで実行するすべてのアプリケーションに対し、サービスはシングルトンになります。

ヒント 12. 主要なパフォーマンスルールを忘れないこと

  • 再描画操作と JavaScript 実行を減らすには、コレクションに trackBy を使用する
  • 使用できるすべての場所に onPush 変更検出ストラテジーを使用する(これについては、専用の記事で説明します)
  • 高い処理能力を必要とする計算は、ngZone の外で実行する
  • イベントにスロットル・デバウンスパターンを使用して、不要なサーバー呼び出しとイベントの大量送信を防止する
  • ビッグデータセットの表示には仮想スクロールを使用する
  • テンプレートにデータ変換用の純粋なパイプを使用する
  • AoT コンパイルを使用する
  • アプリケーションの起動には必要ないアプリの部分を遅延読み込みする
  • テンプレートでは計算と条件付き関数呼び出しを使わない(関数呼び出しはイベントのみに使用します)

お読みいただきありがとうございました! いくつかのヒントがお役に立ちますように。 コメントやご意見がございましたら、コメント欄でお知らせください。喜んでお伺いします 😃

それではまた!

0
0 141
記事 Hiroshi Sato · 10月 18, 2021 1m read

これはInterSystems FAQ サイトの記事です。
命名規約については、それぞれ下記のドキュメントページをご確認ください。

テーブル名(クラス名)について


識別子のルールとガイドライン - クラス

カラム名(プロパティ名)について


識別子のルールとガイドライン - クラス・メンバ


こちらに記載しておりますように、カラム名(プロパティ名)には英数文字およびASCII 128 よりUnicodeコードポイントの大きな文字のみ使用可能です。

  • 名前は、英字、ASCII 128 よりUnicodeコードポイントの大きな文字かパーセント記号 (%) で始まる必要があります。
  • 残りの文字は、英字、ASCII 128 よりUnicodeコードポイントの大きな文字または数字にする必要があります。

また、「リリース2012.2以降〜」の部分にありますように、

Property "My Property" As %String;

のように " で囲むことで、使用できない記号等も使用できるようになります。

これとは別に、プロパティ名は英数字のみを使用して、SQLフィールド名のみ、別途指定することもできます。

Property iscname As %String [ SqlFieldName = isc_name ];
0
0 841
記事 Toshihiko Minamoto · 11月 10, 2020 8m read

最近行われたディスカッションの中で、Caché ObjectScript における for/while loop のパフォーマンンスが話に出ましたので、意見やベストプラクティスをコミュニティの皆さんと共有したいと思います。 これ自体が基本的なトピックではありますが、他の点では合理的と言える方法のパフォーマンスが意味する内容を見逃してしまうことがよくあります。 つまり、$ListNext を使って$ListBuild リストをイテレートするループ、または $Order を使ってローカル配列をイテレートするループが最も高速な選択肢ということです。

興味深い例として、コンマ区切りの文字列をループするコードについて考えます。

そのようなループをできるだけ手短に書くと、次のようになります。

For i=1:1:$Length(string,",") {
    Set piece = $Piece(string,",",i)
    //piece を使って何らかの処理を実行する...
}

とても分かりやすいですね。でも、多くのコーディングスタイルガイドラインは次のようなコードを提案するかもしれません。

Set n = $Length(string,",")
For i=1:1:n {
    Set piece = $Piece(string,",",i)
    //piece を使って何らかの処理を実行する...
}

各イテレーションで終了条件は評価されていないので、この 2 つのコードにパフォーマンス面での違いはありません。 (初めは誤解していましたが、これはパフォーマンスの問題ではなく、単にスタイルの違いだということを Mark が指摘してくれました。)

コンマ区切りの文字列の場合は、このパフォーマンスを高めることが可能です。 文字列が長くなるにつれ、$Piece(string,",",i) は、string を i 個目の piece の終わりまで処理することになるので、どんどん重くなっていきます。 これを改善するには $ListBuild リストを使用します。 例えば、$ListFromString$ListLength、および $List を使うと、以下のようなコードを書けます。

Set list = $ListFromString(string,",")
Set n = $ListLength(list)
For i=1:1:n {
    Set piece = $List(list,i)
    //piece を使って何らかの処理を実行する...
}

この方が、特に piece が長い場合は、$Length/$Piece を使うよりもパフォーマンスが良くなります。 $Length/$Piece を使った方法では、n の各イテレーションで piece の最初の i に渡される文字がスキャンされています。 一方の $ListFromString/$ListLength/$List を使った方法では、n の各イテレーションで $ListBuild 構造の i ポインターを追いかけています。 この方が高いパフォーマンスを得られますが、それでも実行時間は O(n2) のままです。 loop によりリストの内容が変更されないことを想定した場合、$ListNext を使えば O(n) を改善することができます。

Set list = $ListFromString(string,",")
Set pointer = 0
While $ListNext(list,pointer,piece) {
    //piece を使って何らかの処理を実行する...
}

$List のように、毎回、次のポインタはリストの先頭から i個目の piece までを移動するのではなく、変数「pointer」がリスト内の現在位置を把握しています。 したがって、合計 n(n+1)/2 回 ($Listではn 回のイテレーションでそれぞれ i 回) の「次のポインタ」操作は行わずに、単純に操作を n 回 ($ListNextではイテレーションごとに 1 回) 実行するということになります。

最後に、文字列を整数の添え字が付いた配列に変換すると良いかもしれません。一般的に、$Order を使ってローカル配列をイテレートすると、$ListNext を使った場合よりも処理速度が少し、または大幅に改善します (リスト要素の長さによる)。 もちろん、カンマ区切りの文字列の場合は、配列に変換するのに少し手間がかかります。 繰り返しイテレートする場合や、リストを部分的に変更する必要がある場合、または逆方向にイテレートする必要がある場合は、手間をかけてでも行う価値があるでしょう。

以下は、異なる入力サイズごとに示した実行時間のサンプルです (必要な変換をすべて含む)。

これらの数字は以下から得ています。

USER>d ##class(DC.LoopPerformance).Run(10000,20,100)
Iterating 10000 times over all the pieces of a string with 100 ,-delimited pieces of length 20:
Using $Length/$Piece (hardcoded delimiter): .657383 seconds
Using $Length/$Piece: 1.083932 seconds
Using $ListFromString/$ListLength/$List (hardcoded delimiter): .189867 seconds
Using $ListFromString/$ListLength/$List: .189938 seconds
Using $ListFromString/$ListNext (hardcoded delimiter): .089618 seconds
Using $ListFromString/$ListNext: .089242 seconds
Using $Order over an equivalent local array with integer subscripts: .072485 seconds
****************************************************
Using $ListFromString/$ListNext (not including conversion to $ListBuild list): .058329 seconds
Using one-argument $Order over an equivalent local array with integer subscripts: .060327 seconds
Using three-argument $Order over an equivalent local array with integer subscripts: .069508 seconds
 
USER>d ##class(DC.LoopPerformance).Run(2,1000,2000)
Iterating 2 times over all the pieces of a string with 2000 ,-delimited pieces of length 1000:
Using $Length/$Piece (hardcoded delimiter): 3.372927 seconds
Using $Length/$Piece: 11.739316 seconds
Using $ListFromString/$ListLength/$List (hardcoded delimiter): 1.004757 seconds
Using $ListFromString/$ListLength/$List: .997821 seconds
Using $ListFromString/$ListNext (hardcoded delimiter): .010489 seconds
Using $ListFromString/$ListNext: .010268 seconds
Using $Order over an equivalent local array with integer subscripts: .000839 seconds
****************************************************
Using $ListFromString/$ListNext (not including conversion to $ListBuild list): .003053 seconds
Using one-argument $Order over an equivalent local array with integer subscripts: .000938 seconds
Using three-argument $Order over an equivalent local array with integer subscripts: .000677 seconds

コード (DC.LoopPerformance) を Gist で表示する

追加

ディスカッションの際に、他の方法でも良いパフォーマンスが得られることが判明しましたので、お互いを比較しておく価値があるでしょう。 RunLinearOnly メソッドとテストを実施した様々な実装を最新版の Gist でご覧ください。

USER>d ##class(DC.LoopPerformance).RunLinearOnly(100000,20,100)
Iterating 100000 times over all the pieces of a string with 100 ,-delimited pieces of length 20:
Using $ListFromString/$ListNext (While): .781055 seconds
Using $ListFromString/$ListNext (For/Quit): .8438 seconds
Using $ListFromString/$ListNext (While, not including conversion to $ListBuild list): .37448 seconds
Using $Find/$Extract (Do...While): .675877 seconds
Using $Find/$Extract (For/Quit): .746064 seconds
Using one-argument $Order (For): .589697 seconds
Using one-argument $Order (While): .570996 seconds
Using three-argument $Order (For): .688088 seconds
Using three-argument $Order (While): .617205 seconds
 
USER>d ##class(DC.LoopPerformance).RunLinearOnly(200,2000,1000)
Iterating 200 times over all the pieces of a string with 1000 ,-delimited pieces of length 2000:
Using $ListFromString/$ListNext (While): .913844 seconds
Using $ListFromString/$ListNext (For/Quit): .925076 seconds
Using $ListFromString/$ListNext (While, not including conversion to $ListBuild list): .21842 seconds
Using $Find/$Extract (Do...While): .572115 seconds
Using $Find/$Extract (For/Quit): .610531 seconds
Using one-argument $Order (For): .044251 seconds
Using one-argument $Order (While): .04467 seconds
Using three-argument $Order (For): .043631 seconds
Using three-argument $Order (While): .042568 seconds

以下のチャートは、これらのメソッドの While/Do...While をそれぞれ比較したものです。 特に、$ListFromString/$ListNext と $Extract/$Find、および文字列から $ListBuild リストへの変換をせずに使用する場合の $ListNext と整数の添え字を付けたローカル配列で使用する $Order を比較した相対的なパフォーマンスに注目してください。

まとめ

  • コンマ区切りの文字列から始める場合は、$ListFromString/$ListNext を使った方がわずかにより直感的なコードを書けますが、パフォーマンスの面では $Find/$Extract が最良な選択肢となります。
  • データ構造のチョイスを考慮した場合、 $ListBuild リストのトラバーサルは、入力の小さい同等のローカル配列よりもわずかにパフォーマンスが優れているように思える一方で、入力が大きい場合はローカル配列の方がかなり高いパフォーマンスを提供します。 パフォーマンスにそれほど大きな違いがある理由は分かっていません。 (これに関連して、整数の添え字を付けたローカル配列と$ListBuild リストにおけるランダムな挿入と削除のコストを比較する価値はあるでしょう。このような配列では、set $list を使った方が、わざわざ値を移動させるよりも処理が速くなると思います。)
  • 引数なしの同等の for loop と比較すると、While loop または Do...While loop の方が わずかに 高いパフォーマンスを発揮します。
0
1 564