Bất cứ khi nào tôi thấy một phương thức trong đó hành vi chuyển sang loại tham số của nó, tôi ngay lập tức xem xét đầu tiên nếu phương thức đó thực sự thuộc về tham số phương thức. Ví dụ: thay vì có một phương thức như:
public void sort(List values) {
if (values instanceof LinkedList) {
// do efficient linked list sort
} else { // ArrayList
// do efficient array list sort
}
}
Tôi sẽ làm điều này:
values.sort();
// ...
class ArrayList {
public void sort() {
// do efficient array list sort
}
}
class LinkedList {
public void sort() {
// do efficient linked list sort
}
}
Chúng tôi di chuyển hành vi đến nơi biết khi nào nên sử dụng nó. Chúng tôi tạo ra một sự trừu tượng thực sự nơi bạn không cần phải biết các loại hoặc các chi tiết của việc thực hiện. Đối với tình huống của bạn, có thể có ý nghĩa hơn khi di chuyển phương thức này từ lớp ban đầu (mà tôi sẽ gọi O
) để nhập A
và ghi đè nó theo kiểu B
. Nếu phương thức được gọi doIt
trên một số đối tượng, di chuyển doIt
đến A
và ghi đè với hành vi khác nhau trong B
. Nếu có các bit dữ liệu từ nơi doIt
được gọi ban đầu hoặc nếu phương thức được sử dụng ở những nơi đủ, bạn có thể để phương thức ban đầu và ủy quyền:
class O {
int x;
int y;
public void doIt(A a) {
a.doIt(this.x, this.y);
}
}
Chúng tôi có thể lặn sâu hơn một chút, mặc dù. Thay vào đó, hãy xem gợi ý sử dụng tham số boolean và xem những gì chúng ta có thể tìm hiểu về cách suy nghĩ của đồng nghiệp. Đề xuất của ông là để làm:
public void doIt(A a, boolean isTypeB) {
if (isTypeB) {
// do B stuff
} else {
// do A stuff
}
}
Điều này trông rất giống như instanceof
tôi đã sử dụng trong ví dụ đầu tiên của mình, ngoại trừ việc chúng tôi đang thực hiện kiểm tra đó. Điều này có nghĩa là chúng ta sẽ phải gọi nó theo một trong hai cách:
o.doIt(a, a instanceof B);
hoặc là:
o.doIt(a, true); //or false
Theo cách thứ nhất, điểm gọi không biết A
nó thuộc loại nào. Vì vậy, chúng ta có nên vượt qua booleans tất cả các cách? Đó thực sự là một mô hình mà chúng tôi muốn trên tất cả các cơ sở mã? Điều gì xảy ra nếu có một loại thứ ba chúng ta cần phải tính đến? Nếu đây là cách gọi phương thức, chúng ta nên di chuyển nó sang kiểu và để hệ thống chọn cách thực hiện cho chúng ta một cách đa hình.
Theo cách thứ hai, chúng ta phải biết loại a
tại điểm gọi. Thông thường điều đó có nghĩa là chúng ta hoặc đang tạo cá thể ở đó hoặc lấy một thể hiện của loại đó làm tham số. Tạo một phương thức trên O
đó sẽ có hiệu quả B
ở đây. Trình biên dịch sẽ biết nên chọn phương pháp nào. Khi chúng ta đang lái xe qua những thay đổi như thế này, việc sao chép sẽ tốt hơn là tạo ra sự trừu tượng sai , ít nhất là cho đến khi chúng ta tìm ra nơi chúng ta thực sự sẽ đi. Tất nhiên, tôi đề nghị rằng chúng ta không thực sự hoàn thành cho dù chúng ta đã thay đổi đến thời điểm này.
Chúng ta cần xem xét kỹ hơn về mối quan hệ giữa A
và B
. Nói chung, chúng ta được bảo rằng chúng ta nên ưu tiên thành phần hơn thừa kế . Điều này không đúng trong mọi trường hợp, nhưng nó đúng trong một số trường hợp đáng ngạc nhiên khi chúng ta khai thác. B
Kế thừa từ đó A
, có nghĩa là chúng ta tin B
là một A
. B
nên được sử dụng giống như A
, ngoại trừ việc nó hoạt động hơi khác một chút. Nhưng những khác biệt đó là gì? Chúng ta có thể cho sự khác biệt một cái tên cụ thể hơn? Nó không phải B
là một A
, nhưng thực sự A
có một X
cái có thể A'
hay B'
? Mã của chúng ta sẽ trông như thế nào nếu chúng ta làm điều đó?
Nếu chúng ta chuyển phương thức lên A
như đã đề xuất trước đó, chúng ta có thể đưa một thể hiện X
vào A
và ủy thác phương thức đó cho X
:
class A {
X x;
A(X x) {
this.x = x;
}
public void doIt(int x, int y) {
x.doIt(x, y);
}
}
Chúng tôi có thể thực hiện A'
và B'
, và thoát khỏi B
. Chúng tôi đã cải thiện mã bằng cách đặt tên cho một khái niệm có thể ẩn ý hơn và cho phép chúng tôi thiết lập hành vi đó trong thời gian chạy thay vì thời gian biên dịch. A
thực sự đã trở nên ít trừu tượng hơn. Thay vì một mối quan hệ thừa kế mở rộng, nó đang gọi các phương thức trên một đối tượng được ủy quyền. Đối tượng đó là trừu tượng, nhưng chỉ tập trung hơn vào sự khác biệt trong thực hiện.
Có một điều cuối cùng để xem xét mặc dù. Hãy quay trở lại đề xuất của đồng nghiệp của bạn. Nếu tại tất cả các trang web cuộc gọi mà chúng tôi biết rõ loại hình A
chúng tôi có, thì chúng tôi sẽ thực hiện các cuộc gọi như:
B b = new B();
o.doIt(b, true);
Chúng tôi giả định trước khi soạn mà A
có X
nghĩa là một trong hai A'
hoặc B'
. Nhưng có lẽ ngay cả giả định này cũng không đúng. Đây có phải là nơi duy nhất mà sự khác biệt giữa A
và B
vấn đề này? Nếu có, thì có lẽ chúng ta có thể có một cách tiếp cận hơi khác. Chúng tôi vẫn có một X
đó là một trong hai A'
hoặc B'
, nhưng nó không thuộc về A
. Chỉ O.doIt
quan tâm đến nó, vì vậy chúng ta chỉ chuyển nó cho O.doIt
:
class O {
int x;
int y;
public void doIt(A a, X x) {
x.doIt(a, x, y);
}
}
Bây giờ trang web cuộc gọi của chúng tôi trông như:
A a = new A();
o.doIt(a, new B'());
Một lần nữa, B
biến mất, và sự trừu tượng di chuyển vào tập trung hơn X
. Lần này, mặc dù, A
thậm chí còn đơn giản hơn bằng cách biết ít hơn. Nó thậm chí còn ít trừu tượng hơn.
Điều quan trọng là giảm sự trùng lặp trong một cơ sở mã, nhưng chúng ta phải xem xét tại sao sự trùng lặp xảy ra ở nơi đầu tiên. Sao chép có thể là một dấu hiệu trừu tượng sâu hơn đang cố gắng thoát ra.