Nói tóm lại, đừng thiết kế phần mềm của bạn để có thể sử dụng lại vì không người dùng cuối quan tâm nếu các chức năng của bạn có thể được sử dụng lại. Thay vào đó, kỹ sư về tính dễ hiểu thiết kế - mã của tôi có dễ cho người khác hoặc bản thân hay quên trong tương lai không? - và thiết kế linh hoạt- khi tôi chắc chắn phải sửa lỗi, thêm tính năng hoặc sửa đổi chức năng, mã của tôi sẽ chống lại các thay đổi bao nhiêu? Điều duy nhất khách hàng của bạn quan tâm là bạn có thể phản hồi nhanh như thế nào khi cô ấy báo cáo lỗi hoặc yêu cầu thay đổi. Đặt những câu hỏi về thiết kế của bạn một cách tình cờ có xu hướng dẫn đến mã có thể tái sử dụng, nhưng cách tiếp cận này giúp bạn tập trung vào việc tránh các vấn đề thực sự bạn sẽ gặp phải trong vòng đời của mã đó để bạn có thể phục vụ người dùng cuối tốt hơn thay vì theo đuổi cao cả, không thực tế "kỹ thuật" lý tưởng để làm hài lòng những người yêu râu xanh.
Đối với một cái gì đó đơn giản như ví dụ bạn đã cung cấp, việc triển khai ban đầu của bạn vẫn ổn vì nó nhỏ, nhưng thiết kế đơn giản này sẽ trở nên khó hiểu và dễ vỡ nếu bạn cố gắng đưa quá nhiều tính linh hoạt chức năng (trái với tính linh hoạt của thiết kế) vào một thủ tục. Dưới đây là lời giải thích của tôi về cách tiếp cận ưa thích của tôi để thiết kế các hệ thống phức tạp để dễ hiểu và linh hoạt mà tôi hy vọng sẽ chứng minh những gì tôi muốn nói với họ. Tôi sẽ không sử dụng chiến lược này cho một cái gì đó có thể được viết dưới 20 dòng trong một quy trình duy nhất bởi vì một cái gì đó quá nhỏ đã đáp ứng các tiêu chí của tôi về tính dễ hiểu và linh hoạt.
Đối tượng, không thủ tục
Thay vì sử dụng các lớp như các mô-đun trường học cũ với một loạt các thói quen bạn gọi để thực hiện những việc mà phần mềm của bạn nên làm, hãy xem xét mô hình hóa miền như các đối tượng tương tác và hợp tác để hoàn thành nhiệm vụ trong tay. Các phương thức trong mô hình hướng đối tượng ban đầu được tạo ra là tín hiệu giữa các đối tượng để Object1
có thể nói Object2
để thực hiện công việc của nó, bất kể đó là gì và có thể nhận được tín hiệu trả về. Điều này là do mô hình hướng đối tượng vốn dĩ là về mô hình hóa các đối tượng miền của bạn và các tương tác của chúng chứ không phải là một cách thức lạ mắt để tổ chức các chức năng và quy trình cũ của mô hình mệnh lệnh. Trong trường hợpvoid destroyBaghdad
ví dụ, thay vì cố gắng viết một phương pháp chung chung theo ngữ cảnh để xử lý sự phá hủy Baghdad hoặc bất kỳ thứ gì khác (có thể nhanh chóng phát triển phức tạp, khó hiểu và dễ vỡ), mọi thứ có thể bị phá hủy phải chịu trách nhiệm về cách hiểu để tự hủy diệt Ví dụ: bạn có một giao diện mô tả hành vi của những thứ có thể bị phá hủy:
interface Destroyable {
void destroy();
}
Sau đó, bạn có một thành phố thực hiện giao diện này:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Không có gì kêu gọi phá hủy một trường hợp City
sẽ quan tâm đến việc điều đó xảy ra như thế nào, vì vậy không có lý do gì để mã đó tồn tại ở bất cứ đâu bên ngoài City::destroy
, và thực sự, kiến thức sâu sắc về hoạt động City
bên trong của chính nó sẽ được kết hợp chặt chẽ làm giảm felxibility vì bạn phải xem xét các yếu tố bên ngoài mà bạn cần phải sửa đổi hành vi của City
. Đây là mục đích thực sự đằng sau việc đóng gói. Hãy nghĩ về nó giống như mọi đối tượng có API riêng của nó sẽ cho phép bạn làm bất cứ điều gì bạn cần với nó để bạn có thể để nó lo lắng về việc thực hiện các yêu cầu của bạn.
Đoàn, không "Kiểm soát"
Bây giờ, cho dù lớp thực hiện của bạn là City
hay Baghdad
phụ thuộc vào quá trình phá hủy thành phố chung chung như thế nào. Trong tất cả các khả năng, một City
mảnh sẽ bao gồm các mảnh nhỏ hơn sẽ cần phải bị phá hủy riêng lẻ để hoàn thành việc phá hủy toàn bộ thành phố, vì vậy trong trường hợp đó, mỗi mảnh đó cũng sẽ thực hiện Destroyable
và mỗi thứ sẽ được hướng dẫn City
phá hủy chính họ cũng giống như ai đó từ bên ngoài yêu cầu City
tự hủy diệt.
interface Part extends Destroyable {
...part-specific methods
}
class Building implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class Street implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class City implements Destroyable {
public List<Part> parts() {...}
@Override
public void destroy() {
parts().forEach(Destroyable::destroy);
}
}
Nếu bạn muốn trở nên thực sự điên rồ và thực hiện ý tưởng Bomb
về một địa điểm bị rơi trên một địa điểm và phá hủy mọi thứ trong một bán kính nhất định, nó có thể trông giống như thế này:
class Bomb {
private final Integer radius;
public Bomb(final Integer radius) {
this.radius = radius;
}
public void drop(final Grid grid, final Coordinate target) {
new ObjectsByRadius(
grid,
target,
this.radius
).forEach(Destroyable::destroy);
}
}
ObjectsByRadius
đại diện cho một tập hợp các đối tượng được tính toán Bomb
từ đầu vào vì Bomb
không quan tâm đến việc tính toán đó được thực hiện như thế nào miễn là nó có thể hoạt động với các đối tượng. Điều này có thể tái sử dụng một cách tình cờ, nhưng mục tiêu chính là tách biệt tính toán khỏi các quá trình thả Bomb
và phá hủy các vật thể để bạn có thể hiểu từng mảnh và cách chúng khớp với nhau và thay đổi hành vi của một mảnh riêng lẻ mà không phải định hình lại toàn bộ thuật toán .
Tương tác, không phải thuật toán
Thay vì cố gắng đoán đúng số lượng tham số cho một thuật toán phức tạp, sẽ hợp lý hơn khi mô hình hóa quá trình như một tập hợp các đối tượng tương tác, mỗi đối tượng có vai trò cực kỳ hẹp, vì nó sẽ cho bạn khả năng mô hình hóa độ phức tạp của bạn xử lý thông qua các tương tác giữa các đối tượng được xác định rõ ràng, dễ hiểu và gần như không thay đổi. Khi được thực hiện một cách chính xác, điều này làm cho ngay cả một số sửa đổi phức tạp nhất cũng không quan trọng bằng việc thực hiện một hoặc hai giao diện và làm lại các đối tượng nào được khởi tạo trong main()
phương thức của bạn .
Tôi sẽ đưa cho bạn một cái gì đó với ví dụ ban đầu của bạn, nhưng thực lòng tôi không thể hiểu ý nghĩa của việc "in ... Tiết kiệm ánh sáng ban ngày". Điều tôi có thể nói về loại vấn đề đó là bất cứ khi nào bạn thực hiện phép tính, kết quả của nó có thể được định dạng theo một số cách, cách ưa thích của tôi để phá vỡ nó là như sau:
interface Result {
String print();
}
class Caclulation {
private final Parameter paramater1;
private final Parameter parameter2;
public Calculation(final Parameter parameter1, final Parameter parameter2) {
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public Result calculate() {
...calculate the result
}
}
class FormattedResult {
private final Result result;
public FormattedResult(final Result result) {
this.result = result;
}
@Override
public String print() {
...interact with this.result to format it and return the formatted String
}
}
Vì ví dụ của bạn sử dụng các lớp từ thư viện Java không hỗ trợ thiết kế này, bạn chỉ có thể sử dụng API ZonedDateTime
trực tiếp. Ý tưởng ở đây là mỗi phép tính được gói gọn trong đối tượng của chính nó. Nó không đưa ra giả định về việc nên chạy bao nhiêu lần hoặc định dạng kết quả như thế nào. Nó chỉ liên quan đến việc thực hiện các hình thức tính toán đơn giản nhất. Điều này làm cho nó dễ hiểu và linh hoạt để thay đổi. Tương tự như vậy, Result
đặc biệt quan tâm đến việc đóng gói kết quả của phép tính và FormattedResult
đặc biệt quan tâm đến việc tương tác với Result
định dạng của nó theo các quy tắc chúng tôi xác định. Theo cách này,chúng ta có thể tìm thấy số lượng đối số hoàn hảo cho mỗi phương thức của mình vì mỗi phương thức đều có một nhiệm vụ được xác định rõ . Việc sửa đổi chuyển tiếp về phía trước cũng đơn giản hơn nhiều, miễn là các giao diện không thay đổi (điều mà chúng không có khả năng thực hiện nếu bạn giảm thiểu trách nhiệm của các đối tượng của mình). main()
Phương phápcủa chúng tôicó thể trông như thế này:
class App {
public static void main(String[] args) {
final List<Set<Paramater>> parameters = ...instantiated from args
parameters.forEach(set -> {
System.out.println(
new FormattedResult(
new Calculation(
set.get(0),
set.get(1)
).calculate()
).print()
);
});
}
}
Như một vấn đề thực tế, Lập trình hướng đối tượng được phát minh cụ thể như một giải pháp cho vấn đề phức tạp / linh hoạt của mô hình mệnh lệnh vì không có câu trả lời hay (dù sao ai cũng có thể đồng ý hoặc đến độc lập) bằng cách nào để tối ưu hóa chỉ định các hàm và thủ tục bắt buộc trong thành ngữ.