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 SpiffyPage
subclass 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);
...
}
}
( top
Parameter tambahan diperlukan untuk memastikan bahwa panggilan harus writeMainContent
melalui 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 ArrayList
telah 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
extend
kelas tanpa pemikiran yang cermat.
- selalu tandai kelas Anda
final
kecuali 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.
final
? Banyak orang (termasuk saya) menemukan bahwa ini adalah desain yang bagus untuk membuat setiap kelas non-abstrakfinal
.