最主要的是细节。OOP真正做什么?





我已经白天和黑夜都在挖掘OOP了两年多了。阅读大量书籍,花了几个月的时间将代码从程序重构为面向对象,然后又重新构建。一位朋友说我已经赚到了脑筋。但是我有信心可以解决复杂的问题并编写清晰的代码吗?



我羡慕能够自信地提出妄想的人。特别是在开发,体系结构方面。总的来说,我渴望实现的目标是什么,但我对此充满无休止的怀疑。因为我不是天才,也不是FP,所以我没有成功的故事。但让我放入5戈比。



封装,多态,对象思考...?



当您满载条款时,您是否喜欢它?我已经读够了,但是上面的单词仍然没有告诉我任何特别的信息。我习惯用我能理解的语言来解释事物。如果可以的话,可以是一个抽象级别。我一直想知道一个简单问题的答案:“ OOP给什么?” 最好带有代码示例。今天,我将尝试自己回答。 但首先,要进行一些抽象。



任务的复杂性



开发人员是解决问题的一种方式。每个任务都有许多细节。从与计算机交互的API的细节开始,以业务逻辑的细节结束。



前几天,我和女儿一起收集了马赛克。我们过去通常从9个部分收集大型拼图游戏。现在,她可以为3岁以上的孩子处理小马赛克。挺有趣的!大脑如何在分散的难题中找到自己的位置。而决定复杂性的是什么?



从儿童的马赛克来看,复杂性主要取决于细节的数量。我不确定这个谜题是否会涵盖整个开发过程。但是您还能在编写函数体时比较算法的诞生吗?在我看来,减少细节量是最重要的简化之一。



为了更清楚地显示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)->,其中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> {
    //  ...
  }
}



现在,生成器仅接收所需的数据,就像系统的其他部分一样。同时,通过构造函数接收构建器的类不依赖于初始化构建器所需的参数。详细信息到位后,将更易于理解程序。但是也有一个弱点。



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对象及其依赖项的过程。你必须付出一切。但是有时候这是一个有利可图的提议-去除整个模块中的次要零件,并将它们集中在一个工厂中。



因此,OOP可以隐藏细节,在创建对象时对其进行移动。从设计的角度来看,这是一种超能力-摆脱不必要的细节的能力。如果对象接口中的细节之和小于其封装的结构中的细节,这是有道理的。并且,如果您可以将对象的创建及其在大多数系统中的使用分开,



SOLID,抽象,封装...



关于OOP的书籍很多。他们进行了深入的研究,反映了编写面向对象程序的经验。但是我认识到OOP主要是通过限制细节来简化代码,这使我对开发的看法被颠倒了。而且我将是两极的……但是除非您摆脱对象的细节,否则您不会使用OOP。



您可以尝试遵守SOLID,但是如果您没有隐藏次要细节,这没有多大意义。使接口看起来像现实世界中的对象是可能的,但是如果您没有隐藏次要细节,那将没有太大意义。您可以通过在代码中使用名词来改善语义,但是……您就明白了。



我发现SOLID,模式和其他对象编写准则是出色的重构准则。完成拼图后,您可以看到整个图片并突出显示较简单的部分。通常,这些是需要注意的重要工具和度量,但是开发人员通常在将程序转换为对象形式之前继续学习和使用它们。



当你知道真相时



OOP是解决复杂问题的工具。通过限制细节将困难的任务划分为简单的任务。减少零件数量的一种方法是用一组操作替换数据。



既然您已经知道了真相,请尝试摆脱项目中不必要的内容。将结果对象匹配到SOLID。然后尝试将它们带入现实世界中的对象。并非相反。最主要的是细节。



最近写了一个用于提取类重构的VSCode扩展我认为这是面向对象代码的一个很好的例子。我有最好的。我很高兴对实现发表评论,或对改进代码/功能提出建议。我想在不久的将来在亚伯拉罕布拉发布公关



All Articles