Mengapa kata kunci 'final' bermanfaat?


54

Tampaknya Java memiliki kekuatan untuk mendeklarasikan kelas yang tidak dapat diturunkan untuk waktu yang lama, dan sekarang C ++ juga memilikinya. Namun, mengingat prinsip Open / Close dalam SOLID, mengapa hal itu berguna? Bagi saya, finalkata kunci terdengar seperti friend- itu legal, tetapi jika Anda menggunakannya, kemungkinan besar desainnya salah. Berikan beberapa contoh di mana kelas yang tidak dapat diturunkan akan menjadi bagian dari arsitektur atau pola desain yang hebat.


43
Menurut Anda mengapa kelas dirancang secara salah jika dianotasi dengan final? Banyak orang (termasuk saya) menemukan bahwa ini adalah desain yang bagus untuk membuat setiap kelas non-abstrak final.
Melihat

20
Komposisi nikmat daripada warisan dan Anda dapat memiliki setiap kelas non-abstrak final.
Andy

24
Prinsip buka / tutup dalam arti merupakan anakronisme dari abad ke-20, ketika mantra itu untuk membuat hierarki kelas yang diwarisi dari kelas yang pada gilirannya diwarisi dari kelas lain. Ini bagus untuk mengajar pemrograman berorientasi objek tetapi ternyata menciptakan kusut, kekacauan yang tidak dapat dipelihara ketika diterapkan pada masalah dunia nyata. Merancang kelas agar bisa diperluas itu sulit.
David Hammen

34
@ DavidArno Jangan konyol. Warisan adalah sine qua non dari pemrograman berorientasi objek, dan itu tempat dekat sebagai rumit atau berantakan sebagai individu terlalu-dogmatis tertentu seperti untuk berkhotbah. Ini alat, seperti yang lainnya, dan programmer yang baik tahu cara menggunakan alat yang tepat untuk pekerjaan itu.
Mason Wheeler

48
Sebagai pengembang yang mulai pulih, saya ingat bahwa final adalah alat yang brilian untuk mencegah perilaku berbahaya memasuki bagian penting. Saya juga sepertinya ingat warisan menjadi alat yang kuat dalam berbagai cara. Ini hampir seolah-olah terkesiap alat yang berbeda memiliki pro dan kontra dan kami sebagai insinyur harus menyeimbangkan faktor-faktor tersebut saat kami memproduksi perangkat lunak kami!
corsiKa

Jawaban:


136

final mengekspresikan niat . Ini memberi tahu pengguna kelas, metode atau variabel "Elemen ini tidak seharusnya berubah, dan jika Anda ingin mengubahnya, Anda belum memahami desain yang ada."

Ini penting karena arsitektur program akan sangat, sangat sulit jika Anda harus mengantisipasi bahwa setiap kelas dan setiap metode yang pernah Anda tulis dapat diubah untuk melakukan sesuatu yang sama sekali berbeda dengan subkelas. Adalah jauh lebih baik untuk memutuskan di muka elemen mana yang seharusnya dapat diubah dan mana yang tidak, dan untuk menegakkan ketidakstabilan melalui final.

Anda juga dapat melakukan ini melalui komentar dan dokumen arsitektur, tetapi selalu lebih baik membiarkan kompiler memberlakukan hal-hal yang dapat dilakukannya daripada berharap bahwa pengguna di masa depan akan membaca dan mematuhi dokumentasi.


14
Saya juga. Tetapi siapa pun yang pernah menulis kelas dasar yang digunakan kembali secara luas (seperti kerangka kerja atau perpustakaan media) lebih tahu daripada mengharapkan programmer aplikasi berperilaku dengan cara yang waras. Mereka akan menumbangkan, menyalahgunakan dan mendistorsi penemuan Anda dengan cara yang Anda bahkan tidak pernah berpikir itu mungkin kecuali Anda menguncinya dengan pegangan besi.
Kilian Foth

8
@KilianFoth Ok, tapi jujur, bagaimana itu masalah Anda apa yang dilakukan programmer aplikasi?
coredump

20
@coredump Orang yang menggunakan pustaka saya membuat sistem yang buruk secara buruk. Sistem yang buruk menumbuhkan reputasi buruk. Pengguna tidak akan dapat membedakan kode hebat Kilian dari aplikasi Fred Random yang sangat tidak stabil. Hasil: Saya kehilangan kredibilitas pemrograman dan pelanggan. Membuat kode Anda sulit disalahgunakan adalah masalah uang dan sen.
Kilian Foth

21
Pernyataan "Elemen ini tidak seharusnya berubah, dan jika Anda ingin mengubahnya, Anda belum memahami desain yang ada." sangat sombong dan bukan sikap yang saya inginkan sebagai bagian dari perpustakaan mana pun saya bekerja. Andai saja saya punya uang receh untuk setiap kali perpustakaan yang terlalu banyak dienkapsulasi membuat saya tidak punya mekanisme untuk mengubah bagian dari keadaan batin yang perlu diubah karena penulis gagal mengantisipasi kasus penggunaan yang penting ...
Mason Wheeler

31
@JimmyB Aturan praktis: Jika Anda merasa tahu untuk apa kreasi Anda akan digunakan, Anda sudah salah. Bell memahami telepon sebagai sistem Muzak. Kleenex diciptakan untuk tujuan merias wajah yang lebih nyaman. Thomas Watson, presiden IBM, pernah berkata, "Saya pikir ada pasar dunia untuk mungkin lima komputer." Perwakilan Jim Sensenbrenner, yang memperkenalkan UU PATRIOT pada tahun 2001, mengatakan bahwa itu secara khusus dimaksudkan untuk mencegah NSA melakukan hal-hal yang mereka lakukan "dengan otoritas tindakan PATRIOT." Dan seterusnya ...
Mason Wheeler

59

Ini menghindari Masalah Kelas Basis Fragile . Setiap kelas dilengkapi dengan serangkaian jaminan dan invarian implisit atau eksplisit. Prinsip Substitusi Liskov mengamanatkan bahwa semua subtipe kelas itu juga harus memberikan semua jaminan ini. Namun, sangat mudah untuk melanggar ini jika kita tidak menggunakannya final. Misalnya, mari kita periksa pemeriksa kata sandi:

public class PasswordChecker {
  public boolean passwordIsOk(String password) {
    return password == "s3cret";
  }
}

Jika kita membiarkan kelas itu ditimpa, satu implementasi dapat mengunci semua orang, yang lain mungkin memberi semua orang akses:

public class OpenDoor extends PasswordChecker {
  public boolean passwordIsOk(String password) {
    return true;
  }
}

Ini biasanya tidak OK, karena subclass sekarang memiliki perilaku yang sangat tidak sesuai dengan aslinya. Jika kita benar-benar ingin kelas diperluas dengan perilaku lain, Rantai Tanggung Jawab akan lebih baik:

PasswordChecker passwordChecker =
  new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
  new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
 new DefaultPasswordChecker(
   new OpenDoor(null)
 );

public interface PasswordChecker {
  boolean passwordIsOk(String password);
}

public final class DefaultPasswordChecker implements PasswordChecker {
  private PasswordChecker next;

  public DefaultPasswordChecker(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    if ("s3cret".equals(password)) return true;
    if (next != null) return next.passwordIsOk(password);
    return false;
  }
}

public final class OpenDoor implements PasswordChecker {
  private PasswordChecker next;

  public OpenDoor(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    return true;
  }
}

Masalahnya menjadi lebih jelas ketika kelas yang lebih rumit memanggil metode sendiri, dan metode tersebut dapat diganti. Saya kadang-kadang menghadapi ini ketika cukup mencetak struktur data atau menulis HTML. Setiap metode bertanggung jawab atas beberapa widget.

public class Page {
  ...;

  @Override
  public String toString() {
    PrintWriter out = ...;
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    writeHeader(out);
    writeMainContent(out);
    writeMainFooter(out);
    out.print("</body>");

    out.print("</html>");
    ...
  }

  void writeMainContent(PrintWriter out) {
    out.print("<div class='article'>");
    out.print(htmlEscapedContent);
    out.print("</div>");
  }

  ...
}

Sekarang saya membuat subkelas yang menambahkan sedikit lebih banyak gaya:

class SpiffyPage extends Page {
  ...;


  @Override
  void writeMainContent(PrintWriter out) {
    out.print("<div class='row'>");

    out.print("<div class='col-md-8'>");
    super.writeMainContent(out);
    out.print("</div>");

    out.print("<div class='col-md-4'>");
    out.print("<h4>About the Author</h4>");
    out.print(htmlEscapedAuthorInfo);
    out.print("</div>");

    out.print("</div>");
  }
}

Sekarang abaikan sejenak bahwa ini bukan cara yang sangat baik untuk menghasilkan halaman HTML, apa yang terjadi jika saya ingin mengubah tata letak lagi? Saya harus membuat SpiffyPagesubclass yang entah bagaimana membungkus konten itu. Apa yang bisa kita lihat di sini adalah aplikasi pola pola templat yang tidak disengaja . Metode templat adalah titik ekstensi yang terdefinisi dengan baik di kelas dasar yang dimaksudkan untuk diganti.

Dan apa yang terjadi jika kelas dasar berubah? Jika konten HTML berubah terlalu banyak, ini bisa merusak tata letak yang disediakan oleh subkelas. Karena itu tidak benar-benar aman untuk mengubah kelas dasar sesudahnya. Ini tidak jelas jika semua kelas Anda berada dalam proyek yang sama, tetapi sangat nyata jika kelas dasar adalah bagian dari beberapa perangkat lunak yang diterbitkan yang dibangun oleh orang lain.

Jika strategi ekstensi ini dimaksudkan, kami dapat memungkinkan pengguna untuk menukar cara masing-masing bagian dihasilkan. Baik, mungkin ada Strategi untuk setiap blok yang dapat disediakan secara eksternal. Atau, kita bisa bersarang Dekorator. Ini akan setara dengan kode di atas, tetapi jauh lebih eksplisit dan jauh lebih fleksibel:

Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());

public interface PageLayout {
  void writePage(PrintWriter out, PageLayout top);
  void writeMainContent(PrintWriter out, PageLayout top);
  ...
}

public final class Page {
  private PageLayout layout = new DefaultPageLayout();

  public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
    layout = wrapper.apply(layout);
  }

  ...
  @Override public String toString() {
    PrintWriter out = ...;
    layout.writePage(out, layout);
    ...
  }
}

public final class DefaultPageLayout implements PageLayout {
  @Override public void writeLayout(PrintWriter out, PageLayout top) {
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    top.writeHeader(out, top);
    top.writeMainContent(out, top);
    top.writeMainFooter(out, top);
    out.print("</body>");

    out.print("</html>");
  }

  @Override public void writeMainContent(PrintWriter out, PageLayout top) {
    ... /* as above*/
  }
}

public final class SpiffyPageDecorator implements PageLayout {
  private PageLayout inner;

  public SpiffyPageDecorator(PageLayout inner) {
    this.inner = inner;
  }

  @Override
  void writePage(PrintWriter out, PageLayout top) {
    inner.writePage(out, top);
  }

  @Override
  void writeMainContent(PrintWriter out, PageLayout top) {
    ...
    inner.writeMainContent(out, top);
    ...
  }
}

( topParameter tambahan diperlukan untuk memastikan bahwa panggilan harus writeMainContentmelalui bagian atas rantai dekorator. Ini mengemulasi fitur subkelas yang disebut rekursi terbuka .)

Jika kita memiliki beberapa dekorator, sekarang kita dapat mencampurnya dengan lebih bebas.

Jauh lebih sering daripada keinginan untuk sedikit menyesuaikan fungsionalitas yang ada adalah keinginan untuk menggunakan kembali beberapa bagian dari kelas yang ada. Saya telah melihat sebuah kasus di mana seseorang menginginkan kelas di mana Anda dapat menambahkan item dan mengulanginya semua. Solusi yang benar adalah:

final class Thingies implements Iterable<Thing> {
  private ArrayList<Thing> thingList = new ArrayList<>();

  @Override public Iterator<Thing> iterator() {
    return thingList.iterator();
  }

  public void add(Thing thing) {
    thingList.add(thing);
  }

  ... // custom methods
}

Sebagai gantinya, mereka membuat subclass:

class Thingies extends ArrayList<Thing> {
  ... // custom methods
}

Ini tiba-tiba berarti bahwa seluruh antarmuka ArrayListtelah menjadi bagian dari antarmuka kami . Pengguna dapat remove()hal - hal, atau get()hal - hal pada indeks tertentu. Ini dimaksudkan seperti itu? BAIK. Tetapi sering kali, kita tidak dengan hati-hati memikirkan semua konsekuensi.

Oleh karena itu disarankan untuk

  • tidak pernah extendkelas tanpa pemikiran yang cermat.
  • selalu tandai kelas Anda finalkecuali kecuali jika Anda bermaksud untuk mengganti metode apa pun.
  • buat antarmuka tempat Anda ingin menukar implementasi, misalnya untuk pengujian unit.

Ada banyak contoh di mana "aturan" ini harus dilanggar, tetapi biasanya memandu Anda untuk desain yang baik, fleksibel, dan menghindari bug karena perubahan yang tidak diinginkan pada kelas dasar (atau penggunaan subkelas yang tidak diinginkan sebagai turunan dari kelas dasar ).

Beberapa bahasa memiliki mekanisme penegakan yang lebih ketat:

  • Semua metode bersifat final secara default dan harus ditandai secara eksplisit sebagai virtual
  • Mereka memberikan warisan pribadi yang tidak mewarisi antarmuka tetapi hanya implementasinya.
  • Mereka membutuhkan metode kelas dasar untuk ditandai sebagai virtual, dan mengharuskan semua penggantian juga ditandai. Ini menghindari masalah di mana subkelas mendefinisikan metode baru, tetapi metode dengan tanda tangan yang sama kemudian ditambahkan ke kelas dasar tetapi tidak dimaksudkan sebagai virtual.

3
Anda berhak mendapatkan +100 untuk menyebutkan "masalah kelas dasar rapuh". :)
David Arno

6
Saya tidak yakin dengan poin yang dibuat di sini. Ya, kelas dasar yang rapuh adalah masalah, tetapi final tidak menyelesaikan semua masalah dengan mengubah implementasi. Contoh pertama Anda buruk karena Anda menganggap Anda tahu semua kasus penggunaan yang mungkin untuk PasswordChecker ("mengunci semua orang atau mengizinkan semua orang mengakses ... tidak apa-apa" - kata siapa?). Daftar "karena itu disarankan ..." terakhir Anda benar-benar buruk - Anda pada dasarnya menganjurkan untuk tidak memperpanjang apa pun dan menandai semuanya sebagai final - yang sepenuhnya menghilangkan kegunaan OOP, pewarisan dan penggunaan kembali kode.
adelphus

4
Contoh pertama Anda bukan contoh masalah kelas dasar yang rapuh. Dalam masalah kelas dasar yang rapuh, perubahan ke kelas dasar memecah subclass. Tetapi dalam contoh itu, subclass Anda tidak mengikuti kontrak subclass. Ini adalah dua masalah yang berbeda. (Selain itu, sebenarnya masuk akal bahwa dalam keadaan tertentu, Anda dapat menonaktifkan pemeriksa kata sandi (katakan untuk pengembangan))
Winston Ewert

5
"Itu menghindari masalah kelas dasar yang rapuh" - dengan cara yang sama dengan membunuh diri sendiri menghindari kelaparan.
user253751

9
@immibis, ini lebih seperti menghindari makan untuk menghindari mendapatkan makanan poisioning. Tentu, tidak pernah makan akan menjadi masalah. Tetapi makan hanya di tempat-tempat yang Anda percayai sangat masuk akal.
Winston Ewert

33

Saya terkejut bahwa belum ada yang menyebutkan Java Efektif, Edisi ke-2 oleh Joshua Bloch (yang harus dibaca setidaknya untuk setiap pengembang Java setidaknya). Butir 17 dalam buku ini membahas hal ini secara terperinci, dan berjudul: " Desain dan dokumen untuk warisan atau yang dilarang ".

Saya tidak akan mengulangi semua saran bagus dalam buku ini, tetapi paragraf-paragraf khusus ini tampaknya relevan:

Tapi bagaimana dengan kelas beton biasa? Secara tradisional, mereka tidak final atau dirancang dan didokumentasikan untuk subklasifikasi, tetapi keadaan ini berbahaya. Setiap kali perubahan dibuat di kelas seperti itu, ada kemungkinan bahwa kelas klien yang memperpanjang kelas akan rusak. Ini bukan hanya masalah teoretis. Tidak jarang menerima laporan bug yang terkait dengan subkelas setelah memodifikasi internal kelas beton nonfinal yang tidak dirancang dan didokumentasikan untuk pewarisan.

Solusi terbaik untuk masalah ini adalah melarang subclass di kelas yang tidak dirancang dan didokumentasikan untuk subklas aman. Ada dua cara untuk melarang subclassing. Yang lebih mudah dari keduanya adalah mendeklarasikan final kelas. Alternatifnya adalah menjadikan semua konstruktor sebagai pribadi atau paket-privat dan untuk menambah pabrik statis publik sebagai pengganti konstruktor. Alternatif ini, yang menyediakan fleksibilitas untuk menggunakan subclass secara internal, dibahas dalam Butir 15. Baik pendekatan tersebut dapat diterima.


21

Salah satu alasan finalberguna adalah memastikan bahwa Anda tidak dapat membuat subkelas kelas dengan cara yang akan melanggar kontrak kelas induk. Subclassing seperti itu akan menjadi pelanggaran SOLID (terutama "L") dan membuat kelas finalmencegahnya.

Salah satu contoh umum adalah membuatnya tidak mungkin untuk subkelas kelas yang tidak dapat diubah dengan cara yang akan membuat subkelas bisa berubah. Dalam kasus-kasus tertentu, perubahan perilaku semacam itu dapat menyebabkan efek yang sangat mengejutkan, misalnya ketika Anda menggunakan sesuatu sebagai kunci dalam peta yang berpikir bahwa kunci tersebut tidak dapat diubah sementara pada kenyataannya Anda menggunakan subkelas yang dapat diubah.

Di Jawa, banyak masalah keamanan yang menarik dapat diperkenalkan jika Anda dapat membuat subkelas Stringdan membuatnya bisa berubah (atau membuatnya menelepon kembali ke rumah ketika seseorang memanggil metode-metodenya, sehingga mungkin menarik data sensitif dari sistem) ketika benda-benda ini dilewatkan sekitar beberapa kode internal yang terkait dengan pemuatan kelas dan keamanan.

Final juga terkadang membantu dalam mencegah kesalahan sederhana seperti menggunakan kembali variabel yang sama untuk dua hal dalam suatu metode, dll. Dalam Scala, Anda dianjurkan untuk hanya menggunakan valyang secara kasar sesuai dengan variabel final di Jawa, dan sebenarnya setiap penggunaan varatau variabel non-final dipandang dengan kecurigaan.

Akhirnya, kompiler dapat, setidaknya secara teori, melakukan beberapa optimasi tambahan ketika mereka tahu bahwa suatu kelas atau metode adalah final, karena ketika Anda memanggil metode pada kelas akhir, Anda tahu persis metode mana yang akan dipanggil dan tidak harus pergi melalui tabel metode virtual untuk memeriksa warisan.


6
Akhirnya, kompiler dapat, paling tidak dalam teori => Saya pribadi telah meninjau pass devirtualization Clang, dan saya mengkonfirmasi itu digunakan dalam praktek.
Matthieu M.

Tetapi tidak dapatkah kompiler mengatakan sebelumnya bahwa tidak ada yang mengesampingkan kelas atau metode terlepas dari apakah itu ditandai final atau tidak?
JesseTG

3
@ JesseTG Jika memiliki akses ke semua kode sekaligus, mungkin. Bagaimana dengan kompilasi file terpisah?
Pasang kembali Monica

3
@JesseTG devirtualization atau (monomorphic / polymorphic) inline caching adalah teknik yang umum dalam kompiler JIT, karena sistem tahu kelas mana yang saat ini dimuat, dan dapat deoptimisasi kode jika asumsi tidak ada metode override ternyata salah. Namun, kompiler sebelumnya tidak bisa. Ketika saya mengkompilasi satu kelas Java dan kode yang menggunakan kelas itu, saya nanti bisa mengkompilasi sebuah subclass dan meneruskan sebuah instance ke kode konsumsi. Cara paling sederhana adalah menambahkan toples lain ke bagian depan classpath. Kompiler tidak dapat mengetahui semua itu, karena ini terjadi pada saat run time.
amon

5
@MTilsted Anda dapat mengasumsikan bahwa string tidak dapat diubah karena string yang bermutasi melalui refleksi dapat dilarang melalui a SecurityManager. Sebagian besar program tidak menggunakannya, tetapi mereka juga tidak menjalankan kode yang tidak tepercaya. +++ Anda harus mengasumsikan bahwa string tidak dapat diubah karena jika tidak, Anda tidak akan mendapatkan keamanan nol dan banyak bug sewenang-wenang sebagai bonus. Setiap programmer dengan asumsi string Java dapat berubah dalam kode produktif pasti gila.
maaartinus

7

Alasan kedua adalah kinerja . Alasan pertama adalah karena beberapa kelas memiliki perilaku atau keadaan penting yang tidak seharusnya diubah untuk memungkinkan sistem bekerja. Sebagai contoh jika saya memiliki kelas "Kata Sandi" dan untuk membangun kelas itu saya telah menyewa tim ahli keamanan dan kelas ini berkomunikasi dengan ratusan ATM dengan procol yang dipelajari dan didefinisikan dengan baik. Izinkan orang baru yang direkrut keluar dari universitas untuk membuat kelas "TrustMePasswordCheck" yang memperluas kelas di atas bisa sangat berbahaya bagi sistem saya; metode-metode itu tidak seharusnya diganti, itu saja.



JVM cukup pintar untuk memperlakukan kelas sebagai final jika mereka tidak memiliki subclass, bahkan jika mereka tidak dinyatakan final.
user253751

Turun karena klaim 'kinerja' telah dibantah berkali-kali.
Paul Smith

7

Ketika saya membutuhkan kelas, saya akan menulis kelas. Jika saya tidak membutuhkan subclass, saya tidak peduli dengan subclass. Saya memastikan bahwa kelas saya berperilaku seperti yang dimaksudkan, dan tempat-tempat di mana saya menggunakan kelas menganggap bahwa kelas berperilaku sebagaimana dimaksud.

Jika ada yang ingin subkelas kelas saya, saya ingin sepenuhnya menolak tanggung jawab atas apa yang terjadi. Saya mencapainya dengan membuat kelas "final". Jika Anda ingin subkelasnya, ingatlah bahwa saya tidak memperhitungkan subklasifikasi saat saya menulis kelas. Jadi Anda harus mengambil kode sumber kelas, menghapus "final", dan sejak saat itu apa pun yang terjadi adalah sepenuhnya tanggung jawab Anda .

Anda pikir itu "tidak berorientasi objek"? Saya dibayar untuk membuat kelas yang melakukan apa yang seharusnya dilakukan. Tidak ada yang membayar saya untuk membuat kelas yang bisa disubklasifikasikan. Jika Anda dibayar untuk membuat kelas saya dapat digunakan kembali, Anda dapat melakukannya. Mulailah dengan menghapus kata kunci "final".

(Selain itu, "final" sering memungkinkan optimisasi substansial. Misalnya, dalam Swift "final" pada kelas publik, atau pada metode kelas publik, berarti kompiler dapat sepenuhnya mengetahui kode apa yang akan dijalankan oleh pemanggilan metode, dan dapat mengganti pengiriman dinamis dengan pengiriman statis (keuntungan kecil) dan sering mengganti pengiriman statis dengan inlining (mungkin manfaat besar)).

adelphus: Apa yang sulit dipahami tentang "jika Anda ingin mensubklasifikasikan, ambil kode sumber, hapus 'final', dan itu tanggung jawab Anda"? "final" sama dengan "peringatan adil".

Dan saya tidak dibayar untuk membuat kode yang dapat digunakan kembali. Saya dibayar untuk menulis kode yang melakukan apa yang seharusnya dilakukan. Jika saya dibayar untuk membuat dua bit kode yang serupa, saya mengekstrak bagian-bagian umum karena itu lebih murah dan saya tidak dibayar untuk membuang waktu saya. Membuat kode yang dapat digunakan kembali yang tidak digunakan kembali adalah buang-buang waktu saya.

M4ks: Anda selalu menjadikan segala sesuatu pribadi yang tidak seharusnya diakses dari luar. Sekali lagi, jika Anda ingin subkelas, Anda mengambil kode sumber, ubah hal-hal menjadi "dilindungi" jika Anda perlu, dan bertanggung jawab atas apa yang Anda lakukan. Jika Anda merasa perlu mengakses hal-hal yang saya tandai pribadi, Anda lebih baik tahu apa yang Anda lakukan.

Keduanya: Subclassing adalah bagian kecil dari kode penggunaan kembali. Membuat blok bangunan yang dapat diadaptasi tanpa subklasifikasi jauh lebih kuat dan sangat bermanfaat dari "final" karena pengguna blok dapat bergantung pada apa yang mereka dapatkan.


4
-1 Jawaban ini menjelaskan segala sesuatu yang salah dengan pengembang perangkat lunak. Jika seseorang ingin menggunakan kembali kelas Anda dengan mensubclassing, biarkan mereka. Mengapa menjadi tanggung jawab Anda bagaimana mereka menggunakan (atau menyalahgunakan) itu? Pada dasarnya, Anda menggunakan final seperti Anda juga, Anda tidak menggunakan kelas saya. * "Tidak ada yang membayar saya untuk membuat kelas yang bisa disubklasifikasikan" . Apakah kamu serius? Itulah mengapa insinyur perangkat lunak dipekerjakan - untuk membuat kode yang solid dan dapat digunakan kembali.
adelphus

4
-1
Jadikan

3
@adelphus Sementara kata-kata dari jawaban ini blak-blakan, berbatasan dengan kasar, itu bukan sudut pandang "salah". Sebenarnya itu adalah sudut pandang yang sama dengan mayoritas jawaban atas pertanyaan ini sejauh ini hanya dengan nada yang kurang klinis.
NemesisX00

+1 untuk menyebutkan bahwa Anda dapat menghapus 'final'. Adalah sombong untuk mengklaim mengetahui semua kemungkinan penggunaan kode Anda. Namun sangat rendah hati untuk menjelaskan bahwa Anda tidak dapat mempertahankan beberapa kegunaan yang mungkin, dan bahwa penggunaan tersebut akan memerlukan pemeliharaan garpu.
gmatht

4

Mari kita bayangkan bahwa SDK untuk platform mengirim kelas berikut:

class HTTPRequest {
   void get(String url, String method = "GET");
   void post(String url) {
       get(url, "POST");
   }
}

Aplikasi subkelas kelas ini:

class MyHTTPRequest extends HTTPRequest {
    void get(String url, String method = "GET") {
        requestCounter++;
        super.get(url, method);
    }
}

Semua baik-baik saja dan baik-baik saja, tetapi seseorang yang bekerja pada SDK memutuskan bahwa melewatkan metode getadalah konyol, dan membuat antarmuka lebih baik memastikan untuk menerapkan kompatibilitas ke belakang.

class HTTPRequest {
   @Deprecated
   void get(String url, String method) {
        request(url, method);
   }

   void get(String url) {
       request(url, "GET");
   }
   void post(String url) {
       request(url, "POST");
   }

   void request(String url, String method);
}

Semuanya tampak baik-baik saja, sampai aplikasi dari atas dikompilasi ulang dengan SDK baru. Tiba-tiba, metode get overriden tidak dipanggil lagi, dan permintaan tidak dihitung.

Ini disebut masalah kelas dasar yang rapuh, karena perubahan yang tampaknya tidak berbahaya menghasilkan pemecah subclass. Setiap kali perubahan metode apa yang dipanggil di dalam kelas dapat menyebabkan subclass rusak. Itu cenderung berarti bahwa hampir semua perubahan dapat menyebabkan subclass rusak.

Final mencegah siapa pun dari subclass kelas Anda. Dengan begitu, metode mana di dalam kelas yang dapat diubah tanpa khawatir bahwa di suatu tempat seseorang bergantung pada pemanggilan metode mana yang dilakukan.


1

Final secara efektif berarti bahwa kelas Anda aman untuk diubah di masa depan tanpa memengaruhi kelas berbasis warisan hilir (karena tidak ada kelas), atau masalah apa pun di sekitar keamanan utas kelas (Saya pikir ada kasus di mana kata kunci akhir pada suatu bidang mencegah beberapa thread berbasis jinx tinggi).

Final berarti Anda bebas mengubah cara kerja kelas Anda tanpa ada perubahan perilaku yang tidak disengaja merayap ke dalam kode orang lain yang bergantung pada Anda sebagai basis.

Sebagai contoh, saya menulis kelas yang disebut HobbitKiller, yang hebat, karena semua hobbit adalah penipu dan mungkin harus mati. Gosok itu, mereka semua pasti harus mati.

Anda menggunakan ini sebagai kelas dasar dan menambahkan metode baru yang hebat untuk menggunakan penyembur api, tetapi gunakan kelas saya sebagai basis karena saya memiliki metode yang hebat untuk menargetkan para hobbit (selain menjadi tricksie, mereka cepat), yang Anda gunakan untuk membantu mengarahkan penyembur api Anda.

Tiga bulan kemudian saya mengubah implementasi metode penargetan saya. Sekarang, di beberapa titik di masa depan ketika Anda memutakhirkan pustaka Anda, tanpa Anda ketahui, implementasi runtime aktual kelas Anda telah berubah secara mendasar karena perubahan dalam metode kelas super yang Anda andalkan (dan umumnya tidak mengontrol).

Jadi bagi saya untuk menjadi pengembang yang teliti, dan memastikan kelancaran kematian hobbit ke masa depan menggunakan kelas saya, saya harus sangat, sangat berhati-hati dengan perubahan apa pun yang saya buat untuk setiap kelas yang dapat diperluas.

Dengan menghilangkan kemampuan untuk memperluas kecuali dalam kasus-kasus di mana saya secara khusus berniat untuk memperpanjang kelas, saya menyelamatkan diri saya (dan semoga yang lain) banyak sakit kepala.


2
Jika Anda menargetkan metode akan berubah mengapa Anda pernah menjadikannya publik? Dan jika Anda mengubah perilaku suatu kelas secara signifikan, Anda memerlukan versi lain dan tidak terlalu protektiffinal
M4ks

Saya tidak tahu mengapa ini akan berubah. Hobbit adalah tricksie dan perubahan diperlukan. Intinya adalah bahwa jika saya membangunnya sebagai final, saya mencegah warisan yang melindungi orang lain agar perubahan saya tidak mempengaruhi kode mereka.
Scott Taylor

0

Penggunaan finaltidak dengan cara apa pun merupakan pelanggaran prinsip SOLID. Sayangnya, sangat umum untuk menafsirkan Prinsip Terbuka / Tertutup ("entitas perangkat lunak harus terbuka untuk ekstensi tetapi ditutup untuk modifikasi") sebagai makna "daripada memodifikasi kelas, mensubklasifikasikan, dan menambahkan fitur baru". Ini bukan yang semula dimaksudkan olehnya, dan umumnya dianggap bukan menjadi pendekatan terbaik untuk mencapai tujuannya.

Cara terbaik untuk mematuhi OCP adalah merancang titik ekstensi ke dalam kelas, dengan secara khusus memberikan perilaku abstrak yang diparameterisasi dengan menyuntikkan ketergantungan pada objek (misalnya menggunakan pola desain Strategi). Perilaku ini harus dirancang untuk menggunakan antarmuka sehingga implementasi baru tidak bergantung pada warisan.

Pendekatan lain adalah mengimplementasikan kelas Anda dengan API publiknya sebagai kelas abstrak (atau antarmuka). Anda kemudian dapat menghasilkan implementasi yang sama sekali baru yang dapat dihubungkan ke klien yang sama. Jika antarmuka baru Anda memerlukan perilaku yang mirip dengan aslinya, Anda dapat:

  • menggunakan pola desain Dekorator untuk menggunakan kembali perilaku asli yang ada, atau
  • refactor bagian perilaku yang ingin Anda pertahankan menjadi objek helper dan gunakan helper yang sama dalam implementasi baru Anda (refactoring bukan modifikasi).

0

Bagi saya ini masalah desain.

Anggaplah saya memiliki program yang menghitung gaji untuk karyawan. Jika saya memiliki kelas yang mengembalikan jumlah hari kerja antara 2 tanggal berdasarkan negara (satu kelas untuk masing-masing negara), saya akan meletakkan final itu, dan memberikan metode bagi setiap perusahaan untuk menyediakan hari gratis hanya untuk kalender mereka.

Mengapa? Sederhana. Katakanlah seorang pengembang ingin mewarisi kelas dasar WorkingDaysUSA di kelas WorkingDaysUSAmyCompany perusahaan dan memodifikasinya untuk mencerminkan bahwa perusahaannya akan ditutup untuk mogok / pemeliharaan / apa pun alasan 2 Mars.

Perhitungan untuk pesanan dan pengiriman klien akan mencerminkan keterlambatan dan bekerja sesuai ketika di runtime mereka memanggil WorkingDaysUSAmyCompany.getWorkingDays (), tetapi apa yang terjadi ketika saya menghitung waktu liburan? Haruskah saya menambahkan 2nd mars sebagai hari libur untuk semua orang? Tidak. Tetapi karena programmer menggunakan warisan dan saya tidak melindungi kelas ini dapat menyebabkan kebingungan.

Atau katakanlah mereka mewarisi dan memodifikasi kelas untuk mencerminkan bahwa perusahaan ini tidak bekerja pada hari Sabtu di mana di negara mereka bekerja setengah waktu pada hari Sabtu. Kemudian gempa bumi, krisis listrik atau keadaan tertentu membuat presiden menyatakan 3 hari tidak bekerja seperti yang terjadi baru-baru ini di Venezuela. Jika metode kelas warisan sudah dikurangi setiap hari Sabtu, modifikasi saya pada kelas asli dapat menyebabkan mengurangi hari yang sama dua kali. Saya harus pergi ke setiap subclass pada setiap klien dan memverifikasi semua perubahan yang kompatibel.

Larutan? Jadikan kelas final dan sediakan metode addFreeDay (companyID mycompany, Date freeDay). Dengan begitu Anda yakin bahwa ketika Anda memanggil kelas WorkingDaysCountry itu kelas utama Anda dan bukan subkelas

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.