Kırılgan Temel Sınıf Problemini önler . Her sınıf, açık veya kesin garantiler ve değişmezler ile birlikte gelir. Liskov Değişim İlkesi, bu sınıfın tüm alt türlerinin de tüm bu garantileri sağlamaları gerektiğini zorunlu kılar. Ancak, biz kullanmazsak, bunu ihlal etmek gerçekten kolaydır final
. Örneğin, bir şifre kontrol edelim:
public class PasswordChecker {
public boolean passwordIsOk(String password) {
return password == "s3cret";
}
}
Bu sınıfın geçersiz kılınmasına izin verirsek, bir uygulama herkesi kilitleyebilir, diğeri herkese erişim izni verebilir:
public class OpenDoor extends PasswordChecker {
public boolean passwordIsOk(String password) {
return true;
}
}
Bu genellikle normal değildir, çünkü alt sınıfların artık orijinaliyle çok uyumlu olmayan bir davranışı vardır. Sınıfın diğer davranışlarla genişletilmesi niyetinde olursak, bir Sorumluluk Zinciri daha iyi olurdu:
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;
}
}
Daha karmaşık bir sınıf kendi yöntemlerini çağırdığında sorun daha belirgin hale gelir ve bu yöntemler geçersiz kılınabilir. Bazen bir veri yapısını güzel yazdırırken veya HTML yazarken bununla karşılaşıyorum. Her widget bazı widget'lardan sorumludur.
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>");
}
...
}
Şimdi biraz daha stil katan bir alt sınıf oluşturuyorum:
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>");
}
}
Şimdi bunun bir HTML sayfasını oluşturmak için iyi bir yol olmadığını göz ardı ederek, mizanpajı tekrar değiştirmek istersem ne olur? Bir SpiffyPage
şekilde bu içeriği saran bir alt sınıf oluşturmak zorunda kalacağım . Burada görebildiğimiz , şablon yöntemi modelinin yanlışlıkla uygulanmasıdır. Şablon yöntemleri, geçersiz kılınması amaçlanan bir temel sınıftaki iyi tanımlanmış uzatma noktalarıdır.
Ve eğer temel sınıf değişirse ne olur? HTML içeriği çok fazla değişirse, bu, alt sınıfların sağladığı düzeni bozabilir. Bu nedenle temel sınıfı daha sonra değiştirmek gerçekten güvenli değildir. Bu, eğer tüm sınıflarınız aynı projedeyse, ancak, eğer temel sınıf diğer kişilerin üzerine kurulu, yayınlanan bir yazılımın parçasıysa, çok belirgindir.
Bu uzatma stratejisi tasarlandıysa, kullanıcının her bir parçanın nasıl oluşturulduğunu değiştirmesine izin verebilirdik. Her ikisi de, dışarıdan sağlanabilecek her blok için bir Strateji olabilir. Veya, Dekoratörleri yerleştirebiliriz. Bu, yukarıdaki koda eşdeğer, ancak çok daha açık ve çok daha esnek olacaktır:
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);
...
}
}
( Dekoratör zincirinin üstünden geçecek top
çağrıların yapıldığından emin olmak için ek parametre gereklidir writeMainContent
. Bu, açık yineleme adı verilen bir alt sınıflandırma özelliğini taklit eder .)
Birden fazla dekoratörümüz varsa, şimdi onları daha özgürce karıştırabiliriz.
Mevcut işlevselliği biraz uyarlama arzusundan çok daha sık, mevcut bir sınıfın bir bölümünü yeniden kullanma arzusudur. Birinin, eşya ekleyebileceğiniz ve bunların hepsini yineleyebileceğiniz bir sınıf istediği bir durum gördüm. Doğru çözüm şuydu:
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
}
Bunun yerine, bir alt sınıf oluşturdular:
class Thingies extends ArrayList<Thing> {
... // custom methods
}
Bu birdenbire tüm arayüzün bizim arayüzümüzün bir ArrayList
parçası olduğu anlamına gelir . Kullanıcılar bazı şeyleri veya belirli endekslerdeki şeyleri yapabilir . Bu böyle mi planlandı? TAMAM. Ancak çoğu zaman, bütün sonuçları dikkatlice düşünmüyoruz.remove()
get()
Bu nedenle tavsiye edilir
- asla
extend
dikkatli düşüncesiz bir sınıf olmaz.
- Sınıflarınızı her zaman
final
geçersiz kılmayı düşündüğünüz herhangi bir yöntemin kullanılması gibi işaretleyin .
- Bir uygulamayı değiştirmek istediğiniz arayüzleri oluşturun, örneğin ünite testi için.
Bu “kural” ın çiğnenmesi gereken pek çok örnek var, ancak sizi iyi, esnek bir tasarıma yönlendiriyor ve temel sınıflardaki (veya alt sınıfın temel sınıfın bir örneği olarak istenmeyen kullanımları) istenmeyen değişikliklerden kaynaklanan hataları önlüyor. ).
Bazı diller daha katı uygulama mekanizmalarına sahiptir:
- Tüm yöntemler varsayılan olarak kesindir ve açıkça olarak işaretlenmelidir.
virtual
- Arayüzü miras almayan, ancak yalnızca uygulamayı miras alan özel miras sağlarlar.
- Temel sınıf yöntemlerinin sanal olarak işaretlenmesini ve tüm geçersiz kılmaların da işaretlenmesini gerektirir. Bu, bir alt sınıfın yeni bir yöntem tanımladığı, ancak aynı imzalı bir yöntemin daha sonra temel sınıfa eklendiği ancak sanal olarak tasarlanmadığı sorunları önler.
final
? Birçok insan (ben dahil) her soyut olmayan sınıfı yapmanın iyi bir tasarım olduğunu düşünüyorfinal
.