主なものは細部にあります。OOPは実際に何をしますか?





私は2年以上前から昼夜を問わずOOPを掘り下げてきました。大量の本を読み、コードを手続き型からオブジェクト指向にリファクタリングし、また元に戻すのに何ヶ月も費やしました。友人は私が脳のOOPを獲得したと言います。しかし、私は複雑な問題を解決し、明確なコードを書くことができると確信していますか?



妄想的な意見を自信を持って押し込める人がうらやましいです。特に開発、アーキテクチャに関しては。一般的に、私が情熱的に努力していることですが、私が無限に疑問を持っていること。私は天才ではなく、FPでもないので、サクセスストーリーはありません。しかし、5つのコペックを入れさせてください。



カプセル化、多形性、オブジェクト思考...?



あなたが用語を積んでいるとき、あなたはそれが好きですか?私は十分に読みましたが、上記の言葉はまだ私に特に何も教えてくれません。私は自分が理解できる言語で物事を説明することに慣れています。必要に応じて、抽象化のレベル。そして、私は長い間、「OOPは何を与えるのか」という簡単な質問に対する答えを知りたいと思っていました。できればコード例を使用してください。そして今日は自分で答えようと思います。 しかし、最初に、少し抽象化します。



タスクの複雑さ



開発者は、何らかの形で問題の解決に取り組んでいます。各タスクには多くの詳細があります。コンピューターとの対話のAPIの詳細から始まり、ビジネスロジックの詳細で終わります。



先日、娘と一緒にモザイクを集めました。私たちは、文字通り9つのパーツから大きなサイズのジグソーパズルを収集していました。そして今、彼女は3歳からの子供のための小さなモザイクを扱うことができます。それは面白いです!散らばったパズルの中で脳がその場所を見つける方法。そして、何が複雑さを決定しますか?



子供向けのモザイクから判断すると、複雑さは主に詳細の数によって決まります。パズルのアナロジーが開発プロセス全体をカバーするかどうかはわかりません。しかし、関数本体を作成するときに、アルゴリズムの誕生を他に何を比較できますか?そして、詳細の量を減らすことは、最も重要な単純化の1つであるように私には思えます。



OOPの主な特徴をより明確に示すために、タスクについて話しましょう。タスクのパーツの数が多いため、妥当な時間内にパズルを組み立てることができません。そのような場合、分解が必要です。



分解



学校でご存知のように、複雑な問題は、個別に解決するために、より単純な問題に分解することができます。このアプローチの本質は、部品の数を制限することです。



たまたま、プログラミングを学びながら、手続き型のアプローチで作業することに慣れています。入力に変換するデータがある場合、それをサブ関数にスローし、結果にマップします。最終的には、ソリューションがすでに存在する場合、リファクタリング中に分解を行います。



手続き型分解の問題は何ですか?習慣から、初期データが必要であり、できれば最終的に形成された構造のものが必要です。さらに、タスクが大きいほど、これらの初期データの構造が複雑になるほど、覚えておく必要のある詳細が多くなります。しかし、サブタスクを解決するのに十分な初期データがあることを確認し、同時にトップレベルのすべての詳細の合計を取り除くにはどうすればよいですか?



例を見てみましょう。少し前に、プロジェクトのアセンブリを作成して必要なフォルダーにスローするスクリプトを作成しました。



interface BuildConfig {
  id: string;
  deployPath: string;
  options: BuildOptions;
  // ...
}

interface TestService {
  runTests(buildConfigs: BuildConfig[]): Promise<void>;
}

interface DeployService {
  publish(buildConfigs: BuildConfig[]): Promise<void>;
}

class Builder {
  constructor(
    private testService: TestService,
    private deployService: DeployService
  ) // ...
  {}

  async build(buildConfigs: BuildConfig[]): Promise<void> {
    await this.testService.runTests(buildConfigs);
    await this.build(buildConfigs);
    await this.deployService.publish(buildConfigs);
    // ...
  }

  // ...
}



このソリューションでOOPを適用したように見えるかもしれません。サービスの実装を置き換えることができ、何かをテストすることもできます。しかし実際には、これは手続き型アプローチの代表的な例です。



BuildConfigインターフェイスを見てください。これは、コードを書き始めたときに作成した構造です。事前にすべてのパラメーターを予測することはできないことに気づき、必要に応じてこの構造にフィールドを追加するだけでした。作業の途中で、構成はシステムのさまざまな部分で使用される一連のフィールドで大きくなりすぎました。変更のたびに完成させる必要のある「オブジェクト」の存在に悩まされました。ナビゲートするのは難しく、フィールドの名前を混同して何かを壊すのは簡単です。それでも、ビルドシステムのすべての部分はBuildConfigに依存しています。このタスクはそれほど大規模で重要ではないため、災害は発生しませんでした。しかし、システムがもっと複​​雑だったら、私はプロジェクトを台無しにしていたことは明らかです。



オブジェクト



手続き型アプローチの主な問題は、データ、その構造、および量です。複雑なデータ構造により、タスクを理解しにくくする詳細が導入されます。今、あなたの手を見てください、ここに欺瞞はありません。



覚えておきましょう、なぜデータが必要なのですか?それらに対して操作を実行し、結果を取得します。多くの場合、どのサブタスクを解決する必要があるかはわかっていますが、これに必要なデータの種類がわかりません。



注意!事前にデータを所有していることを知って操作して実行することができます。



オブジェクトを使用すると、データセットを一連の操作に置き換えることができます。そして、それが部品の数を減らすならば、それは仕事の一部を単純化します!



// ,     / 
interface BuildConfig {
  id: string;
  deployPath: string;
  options: BuildOptions;
  // ...
}

// vs

//  ,          
interface Project {
  test(): Promise<void>;
  build(): Promise<void>;
  publish(): Promise<void>;
}



変換は非常に簡単です:f(x)-> of()、ここでoはx未満ですオブジェクト内に隠れたセカンダリ。設定を含むコードをある場所から別の場所に転送するとどのような影響があるように思われますか?しかし、この変革には広範囲にわたる影響があります。プログラムの残りの部分でも同じトリックを実行できます。



// project.ts
// ,   Project      .
class Project {
  constructor(
    private buildTester: BuildTester,
    private builder: Builder,
    private buildPublisher: BuildPublisher
  ) {}

  async test(): Promise<void> {
    await this.buildTester.runTests();
  }

  async build(): Promise<void> {
    await this.builder.build();
  }

  async publish(): Promise<void> {
    await this.buildPublisher.publish();
  }
}

// builder.ts

export interface BuildOptions {
  baseHref: string;
  outputPath: string;
  configuration?: string;
}

export class Builder {
  constructor(private options: BuildOptions) {}

  async build(): Promise<void> {
    //  ...
  }
}



これで、ビルダーは、システムの他の部分と同様に、必要なデータのみを受け取ります。同時に、コンストラクターを介してBuilderを受け取るクラスは、Builderを初期化するために必要なパラメーターに依存しません。詳細が整っていると、プログラムを理解しやすくなります。しかし、弱点もあります。



export interface ProjectParams {
  id: string;
  deployPath: Path | string;
  configuration?: string;
  buildRelevance?: BuildRelevance;
}

const distDir = new Directory(Path.fromRoot("dist"));

const buildRecordsDir = new Directory(Path.fromRoot("tmp/builds-manifest"));

export function createProject(params: ProjectParams): Project {
  return new ProjectFactory(params).create();
}

class ProjectFactory {
  private buildDir: Directory = distDir.getSubDir(this.params.id);
  private deployDir: Directory = new Directory(
    Path.from(this.params.deployPath)
  );

  constructor(private params: ProjectParams) {}

  create(): Project {
    const builder = this.createBuilder();
    const buildPublisher = this.createPublisher();
    return new Project(this.params.id, builder, buildPublisher);
  }

  private createBuilder(): NgBuilder {
    return new NgBuilder({
      baseHref: "/clientapp/",
      outputPath: this.buildDir.path.toAbsolute(),
      configuration: this.params.configuration,
    });
  }

  private createPublisher(): BuildPublisher {
    const buildHistory = this.getBuildsHistory();
    return new BuildPublisher(this.buildDir, this.deployDir, buildHistory);
  }

  private getBuildsHistory(): BuildsHistory {
    const buildRecordsFile = this.getBuildRecordsFile();
    const buildRelevance = this.params.buildRelevance ?? BuildRelevance.Default;
    return new BuildsHistory(buildRecordsFile, buildRelevance);
  }

  private getBuildRecordsFile(): BuildRecordsFile {
    const buildRecordsPath = buildRecordsDir.path.join(
      `${this.params.id}.json`
    );
    return new BuildRecordsFile(buildRecordsPath);
  }
}



元の構成の複雑な構造に関連するすべての詳細は、Projectオブジェクトとその依存関係を作成するプロセスに入りました。あなたはすべての代金を払わなければなりません。しかし、これは儲かる提案である場合もあります。モジュール全体のマイナーなパーツを取り除き、それらを1つの工場に集中させることです。



したがって、OOPを使用すると、詳細を非表示にして、オブジェクトの作成時に詳細をシフトできます。設計の観点からは、これは超能力であり、不要な詳細を取り除く機能です。これは、オブジェクトのインターフェイスの詳細の合計が、カプセル化する構造の詳細の合計よりも少ない場合に意味があります。そして、オブジェクトの作成とほとんどのシステムでのその使用を分離できるかどうか。



SOLID、抽象化、カプセル化..。



OOPに関する本はたくさんあります。彼らは、オブジェクト指向のプログラムを書いた経験を反映した詳細な研究を行っています。しかし、OOPは主に詳細を制限することによってコードを単純化するという認識によって、開発に対する私の見方はひっくり返りました。そして、私は極地になります...しかし、オブジェクトの詳細を削除しない限り、OOPを使用していません。



SOLIDに準拠することはできますが、細部を隠していない場合はあまり意味がありません。インターフェイスを現実の世界のオブジェクトのように見せることは可能ですが、細部を隠していない場合はあまり意味がありません。コードで名詞を使用することでセマンティクスを改善できますが、...あなたはその考えを理解します。



SOLID、パターン、およびその他のオブジェクト作成ガイドラインは、優れたリファクタリングガイドラインであることがわかりました。パズルを完了すると、全体像が表示され、より単純な部分を選択できます。一般に、これらは注意が必要な重要なツールとメトリックですが、多くの場合、開発者はプログラムをオブジェクト形式に変換する前に、それらの学習と使用に移ります。



あなたが真実を知っているとき



OOPは、複雑な問題を解決するためのツールです。難しいタスクは、詳細を制限して単純なタスクに分割することで勝ちます。パーツの数を減らす方法は、データを一連の操作に置き換えることです。



真実がわかったので、プロジェクトの不要なものを取り除いてみてください。結果のオブジェクトをSOLIDに一致させます。次に、それらを現実世界のオブジェクトに持ってくるようにしてください。その逆ではありません。主なものは細部にあります。



最近、Extractクラスリファクタリング用のVSCode拡張機能を作成しましたこれはオブジェクト指向のコードの良い例だと思います。私が持っている最高のもの。実装に関するコメントや、コード/機能を改善するための提案をいただければ幸いです。近い将来、アブラカダブラでPRを発行したい



All Articles