Hal utama ada pada detailnya. Apa yang sebenarnya dilakukan OOP?





Saya telah menggali OOP siang dan malam selama lebih dari dua tahun sekarang. Membaca setumpuk buku yang tebal, menghabiskan waktu berbulan-bulan untuk refactoring kode dari prosedural ke berorientasi objek dan kembali lagi. Seorang teman mengatakan bahwa saya telah mendapatkan OOP otak. Tetapi apakah saya yakin bahwa saya dapat memecahkan masalah yang kompleks dan menulis kode yang jelas?



Saya iri pada orang yang dengan percaya diri bisa mendorong opini delusi mereka. Terutama dalam hal pembangunan, arsitektur. Secara umum, apa yang saya cita-citakan dengan semangat, tetapi apa yang saya ragukan tanpa akhir. Karena saya bukan seorang jenius, dan saya bukan seorang FP, saya tidak memiliki kisah sukses. Tapi izinkan saya memasukkan 5 kopeck.



Enkapsulasi, polimorfisme, pemikiran objek ...?



Apakah Anda suka jika Anda sarat dengan istilah? Saya sudah cukup membaca, tetapi kata-kata di atas masih belum memberi tahu saya apa-apa secara khusus. Saya terbiasa menjelaskan hal-hal dalam bahasa yang saya mengerti. Tingkat abstraksi, jika Anda mau. Dan untuk waktu yang lama saya ingin tahu jawaban atas pertanyaan sederhana: "Apa yang diberikan OOP?" Lebih disukai dengan contoh kode. Dan hari ini saya akan mencoba menjawabnya sendiri. Tapi pertama-tama, sedikit abstraksi.



Kompleksitas tugas



Pengembang dengan satu atau lain cara terlibat dalam memecahkan masalah. Setiap tugas memiliki banyak detail. Mulai dari spesifik API interaksi dengan komputer, diakhiri dengan detail logika bisnis.



Suatu hari saya mengumpulkan mosaik dengan putri saya. Kami biasa mengumpulkan teka-teki jigsaw dengan ukuran besar, secara harfiah dari 9 bagian. Dan sekarang dia bisa menangani mosaik kecil untuk anak-anak berusia 3 tahun. Ini menarik! Bagaimana otak menemukan tempatnya di antara teka-teki yang tersebar. Dan apa yang menentukan kompleksitasnya?



Dilihat dari mozaik untuk anak-anak, kerumitannya terutama ditentukan oleh jumlah detail. Saya tidak yakin analogi teka-teki akan mencakup seluruh proses pengembangan. Tapi apa lagi yang bisa Anda bandingkan kelahiran algoritma pada saat menulis badan fungsi? Dan menurut saya, mengurangi jumlah detail adalah salah satu penyederhanaan yang paling signifikan.



Untuk lebih jelas menampilkan fitur utama OOP, mari kita bicara tentang tugas, yang jumlah detailnya tidak memungkinkan kita untuk menyusun puzzle dalam waktu yang wajar. Dalam kasus seperti itu, kita membutuhkan dekomposisi.



Penguraian



Seperti yang Anda ketahui dari sekolah, masalah yang kompleks dapat dipecah menjadi masalah yang lebih sederhana untuk diselesaikan secara terpisah. Inti dari pendekatan ini adalah membatasi jumlah bagian.



Kebetulan saat belajar program, kita terbiasa bekerja dengan pendekatan prosedural. Ketika ada sepotong data di input, yang kami ubah, kami membuangnya ke dalam subfungsi, dan memetakannya menjadi hasil. Dan akhirnya, kami membusuk selama refactoring ketika solusinya sudah ada.



Apa masalah dengan dekomposisi prosedural? Di luar kebiasaan, kita membutuhkan data awal, dan sebaiknya dengan struktur yang akhirnya terbentuk. Selain itu, semakin besar tugasnya, semakin kompleks struktur data awal ini, semakin banyak detail yang perlu Anda ingat. Tetapi bagaimana cara memastikan bahwa data awal akan cukup untuk menyelesaikan subtugas, dan pada saat yang sama menyingkirkan jumlah semua detail di tingkat atas?



Mari kita lihat contohnya. Belum lama ini saya menulis skrip yang membuat kumpulan proyek dan melemparkannya ke folder yang diperlukan.



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

  // ...
}



Sepertinya saya telah menerapkan OOP dalam solusi ini. Anda dapat mengganti implementasi layanan, Anda bahkan dapat menguji sesuatu. Namun sebenarnya, ini adalah contoh utama dari pendekatan prosedural.



Lihat antarmuka BuildConfig. Ini adalah struktur yang saya buat di awal penulisan kode. Saya menyadari sebelumnya bahwa saya tidak dapat meramalkan semua parameter sebelumnya, dan hanya menambahkan bidang ke struktur ini sesuai kebutuhan. Di tengah pekerjaan, konfigurasi tersebut ditumbuhi dengan sekumpulan bidang yang digunakan di berbagai bagian sistem. Saya kesal dengan kehadiran "objek" yang harus diselesaikan dengan setiap perubahan. Sulit untuk menavigasi di dalamnya, dan mudah untuk memecahkan sesuatu dengan mengacaukan nama-nama bidang. Namun, semua bagian dari sistem build bergantung pada BuildConfig. Karena tugas ini tidak terlalu banyak dan kritis, tidak ada bencana. Tetapi jelas bahwa jika sistemnya lebih rumit, saya akan mengacaukan proyek tersebut.



Sebuah Objek



Masalah utama dari pendekatan prosedural adalah data, struktur dan kuantitasnya. Struktur data yang kompleks memperkenalkan detail yang membuat tugas sulit dipahami. Sekarang, jaga tanganmu, tidak ada penipuan di sini.



Ingat, mengapa kita membutuhkan data? Untuk melakukan operasi pada mereka dan mendapatkan hasilnya. Seringkali kita mengetahui subtugas mana yang perlu diselesaikan, tetapi tidak memahami jenis data apa yang diperlukan untuk ini.



Perhatian! Kami dapat memanipulasi operasi dengan mengetahui bahwa mereka memiliki data sebelumnya untuk mengeksekusinya.



Objek memungkinkan Anda mengganti kumpulan data dengan satu set operasi. Dan jika itu mengurangi jumlah bagian, maka itu menyederhanakan bagian tugas!



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

// vs

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



Transformasinya sangat sederhana: f (x) -> dari (), di mana o kurang dari x . Sekunder bersembunyi di dalam objek. Tampaknya, apa efek mentransfer kode dengan konfigurasi dari satu tempat ke tempat lain? Tetapi transformasi ini memiliki implikasi yang luas. Kami dapat melakukan trik yang sama untuk sisa program.



// 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> {
    //  ...
  }
}



Sekarang Builder hanya menerima data yang dibutuhkannya, seperti bagian lain dari sistem. Pada saat yang sama, kelas yang menerima Builder melalui konstruktor tidak bergantung pada parameter yang diperlukan untuk memulainya. Ketika detailnya ada, lebih mudah untuk memahami programnya. Tapi ada juga titik lemahnya.



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



Semua detail yang terkait dengan struktur kompleks dari konfigurasi asli masuk ke dalam proses pembuatan objek Project dan dependensinya. Anda harus membayar semuanya. Tapi terkadang ini adalah tawaran yang menguntungkan - untuk menyingkirkan bagian kecil di seluruh modul, dan memusatkannya di dalam satu pabrik.



Jadi, OOP memungkinkan untuk menyembunyikan detail, menggesernya pada saat pembuatan objek. Dari sudut pandang desain, ini adalah kekuatan super - kemampuan untuk menghilangkan detail yang tidak perlu. Ini masuk akal jika jumlah detail dalam antarmuka objek lebih sedikit daripada dalam struktur yang dienkapsulasi. Dan jika Anda dapat memisahkan pembuatan objek dan penggunaannya di sebagian besar sistem.



SOLID, abstraksi, enkapsulasi ...



Ada banyak sekali buku tentang OOP. Mereka melakukan studi mendalam yang mencerminkan pengalaman menulis program berorientasi objek. Tetapi, kesadaran bahwa OOP menyederhanakan kode terutama dengan membatasi detail yang mengubah pandangan saya tentang pengembangan menjadi terbalik. Dan saya akan menjadi kutub ... tetapi kecuali Anda menghilangkan detail dengan objek, Anda tidak menggunakan OOP.



Anda dapat mencoba untuk mematuhi SOLID, tetapi tidak masuk akal jika Anda tidak menyembunyikan detail kecil. Dimungkinkan untuk membuat antarmuka terlihat seperti objek di dunia nyata, tetapi itu tidak masuk akal jika Anda belum menyembunyikan detail kecil. Anda dapat meningkatkan semantik dengan menggunakan kata benda dalam kode Anda, tetapi ... Anda mengerti.



Saya menemukan SOLID, pola, dan pedoman penulisan objek lainnya sebagai pedoman refactoring yang sangat baik. Setelah menyelesaikan teka-teki, Anda dapat melihat keseluruhan gambar dan dapat menyoroti bagian yang lebih sederhana. Secara umum, ini adalah alat dan metrik penting yang membutuhkan perhatian, tetapi sering kali pengembang beralih ke pembelajaran dan menggunakannya sebelum mengonversi program ke bentuk objek.



Saat kamu tahu yang sebenarnya



OOP adalah alat untuk memecahkan masalah yang kompleks. Tugas yang sulit dimenangkan dengan membaginya menjadi tugas sederhana dengan membatasi detail. Salah satu cara untuk mengurangi jumlah bagian adalah dengan mengganti data dengan satu set operasi.



Sekarang Anda tahu yang sebenarnya, cobalah untuk menyingkirkan yang tidak perlu dalam proyek Anda. Cocokkan objek yang dihasilkan ke SOLID. Kemudian coba bawa mereka ke objek di dunia nyata. Bukan sebaliknya. Hal utama ada pada detailnya.



Baru-baru ini menulis ekstensi VSCode untuk refactoring kelas Ekstrak . Saya rasa ini adalah contoh yang baik dari kode berorientasi objek. Yang terbaik yang saya miliki. Saya akan senang menerima komentar tentang penerapannya, atau saran untuk meningkatkan kode / fungsionalitas. Saya ingin menerbitkan PR di Abracadabra dalam waktu dekat



All Articles