الشيء الرئيسي في التفاصيل. ماذا تفعل OOP حقا؟





لقد كنت أحفر في OOP ليلا ونهارا لأكثر من عامين حتى الآن. اقرأ كومة كثيفة من الكتب ، وقضى شهورًا طويلة في إعادة بناء الكود من إجرائي إلى كائني التوجه والعكس مرة أخرى. يقول أحد الأصدقاء أنني ربحت OOP من الدماغ. ولكن هل لدي ثقة في أنه يمكنني حل المشكلات المعقدة وكتابة تعليمات برمجية واضحة؟



أنا أحسد الأشخاص الذين يمكنهم بثقة دفع رأيهم الوهمي. خاصة عندما يتعلق الأمر بالتنمية والعمارة. بشكل عام ، ما أسعى إليه بشغف ، لكن ما لدي شكوك لا نهاية لها. لأنني لست عبقريًا ، ولست عضوًا في برنامج FP ، فليس لدي قصة نجاح. لكن اسمحوا لي أن أضع 5 كوبيل.



التغليف ، تعدد الأشكال ، التفكير الشيئي ...؟



هل يعجبك عندما تكون محملاً بالشروط؟ لقد قرأت ما يكفي ، لكن الكلمات أعلاه لا تخبرني بأي شيء على وجه الخصوص. أنا معتاد على شرح الأشياء بلغة أفهمها. مستوى من التجريد ، إذا أردت. ولطالما أردت معرفة الإجابة على سؤال بسيط: "ماذا يعطي OOP؟" يفضل أن يكون مع أمثلة التعليمات البرمجية. واليوم سأحاول الإجابة بنفسي. لكن أولاً ، القليل من التجريد.



تعقيد المهمة



المطور بطريقة أو بأخرى يشارك في حل المشاكل. كل مهمة لها تفاصيل كثيرة. بدءاً من تفاصيل واجهة برمجة التطبيقات للتفاعل مع الكمبيوتر وانتهاءً بتفاصيل منطق الأعمال.



ذات يوم جمعت فسيفساء مع ابنتي. اعتدنا على جمع ألغاز الصور المقطوعة ذات الحجم الكبير ، حرفياً من 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) -> 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> {
    //  ...
  }
}



الآن يتلقى المنشئ البيانات التي يحتاجها فقط ، مثل أجزاء أخرى من النظام. في الوقت نفسه ، لا تعتمد الفئات التي تتلقى المُنشئ من خلال المُنشئ على المعلمات اللازمة لتهيئته. عندما تكون التفاصيل في مكانها الصحيح ، يكون من السهل فهم البرنامج. لكن هناك أيضًا نقطة ضعف.



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);
  }
}



دخلت جميع التفاصيل المرتبطة بالهيكل المعقد للتكوين الأصلي في عملية إنشاء كائن المشروع وتبعياته. عليك أن تدفع مقابل كل شيء. لكن في بعض الأحيان يكون هذا عرضًا مربحًا - للتخلص من الأجزاء الصغيرة في الوحدة بأكملها ، وتركيزها داخل مصنع واحد.



وبالتالي ، يتيح OOP إخفاء التفاصيل ، وتحويلها في وقت إنشاء الكائن. من وجهة نظر التصميم ، هذه قوة عظمى - القدرة على التخلص من التفاصيل غير الضرورية. هذا منطقي إذا كان مجموع التفاصيل في واجهة الكائن أقل من البنية التي يتم تغليفها. وإذا كان بإمكانك فصل إنشاء الكائن واستخدامه في معظم أجزاء النظام.



صلب ، تجريد ، تغليف ...



هناك الكثير من الكتب على OOP. يجرون دراسات متعمقة تعكس تجربة كتابة البرامج الموجهة للكائنات. ولكن كان الإدراك أن OOP يبسط الكود في المقام الأول عن طريق تقييد التفاصيل التي قلبت وجهة نظري للتطوير رأسًا على عقب. وسأكون قطبيًا ... لكن ما لم تتخلص من التفاصيل مع الأشياء ، فأنت لا تستخدم OOP.



يمكنك محاولة الامتثال لـ SOLID ، لكن ليس له معنى كبير إذا لم تخفي تفاصيل ثانوية. من الممكن جعل الواجهات تبدو وكأنها كائنات في العالم الحقيقي ، لكن هذا لا معنى له إذا لم تخفي التفاصيل الصغيرة. يمكنك تحسين الدلالات باستخدام الأسماء في التعليمات البرمجية الخاصة بك ، ولكن ... تحصل على الفكرة.



أجد أن SOLID والأنماط وإرشادات أخرى لكتابة الكائنات هي إرشادات ممتازة لإعادة البناء. بعد الانتهاء من اللغز ، يمكنك رؤية الصورة بأكملها وإبراز الأجزاء الأبسط. بشكل عام ، هذه أدوات ومقاييس مهمة تتطلب الانتباه ، ولكن غالبًا ما ينتقل المطورون إلى التعلم واستخدامها قبل تحويل البرنامج إلى نموذج كائن.



عندما تعرف الحقيقة



OOP هي أداة لحل المشاكل المعقدة. يتم كسب المهام الصعبة عن طريق التقسيم إلى مهام بسيطة عن طريق الحد من التفاصيل. تتمثل إحدى طرق تقليل عدد الأجزاء في استبدال البيانات بمجموعة من العمليات.



الآن بعد أن عرفت الحقيقة ، حاول التخلص من الأشياء غير الضرورية في مشروعك. تطابق الكائنات الناتجة مع SOLID. ثم حاول إحضارهم إلى الأشياء في العالم الحقيقي. ليس العكس. الشيء الرئيسي في التفاصيل.



كتب مؤخرًا امتداد VSCode لإعادة بناء فئة Extract . أعتقد أن هذا مثال جيد للتعليمات البرمجية الموجهة بالكائنات. أفضل ما لدي. سيكون من دواعي سروري أن يكون لدي تعليقات على التنفيذ ، أو اقتراحات لتحسين الكود / الوظيفة. أريد إصدار بيان عام في أبراكادابرا في المستقبل القريب



All Articles