it-swarm-vi.com

Chức năng mà chỉ đơn giản gọi chức năng khác, lựa chọn thiết kế xấu?

Tôi có một thiết lập của một lớp đại diện cho một tòa nhà. Tòa nhà này có một kế hoạch sàn, trong đó có giới hạn.

Cách tôi thiết lập nó là như thế này:

public struct Bounds {} // AABB bounding box stuff

//Floor contains bounds and mesh data to update textures etc
//internal since only building should have direct access to it no one else
internal class Floor {  
    private Bounds bounds; // private only floor has access to
}

//a building that has a floor (among other stats)
public class Building{ // the object that has a floor
    Floor floor;
}

Những đối tượng này có những lý do độc đáo riêng để tồn tại khi họ làm những việc khác nhau. Tuy nhiên, có một tình huống, nơi tôi muốn có một điểm địa phương đến tòa nhà.

Trong tình huống này, tôi chủ yếu làm:

Building.GetLocalPoint(worldPoint);

Điều này sau đó có:

public Vector3 GetLocalPoint(Vector3 worldPoint){    
    return floor.GetLocalPoint(worldPoint);
}

Điều này dẫn đến chức năng này trong đối tượng Floor của tôi:

internal Vector3 GetLocalPoint(Vector3 worldPoint){
    return bounds.GetLocalPoint(worldPoint);
}

Và dĩ nhiên, đối tượng giới hạn thực sự làm toán.

Như bạn có thể thấy các chức năng này là khá dư thừa khi chúng chỉ chuyển sang một chức năng khác thấp hơn. Điều này không cảm thấy thông minh đối với tôi - nó có mùi giống như mã xấu sẽ cắn vào mông tôi ở một nơi nào đó với dòng mã lộn xộn.

Ngoài ra, tôi viết mã của mình như dưới đây nhưng tôi phải tiết lộ nhiều hơn cho công chúng mà tôi không muốn làm:

building.floor.bounds.GetLocalPoint(worldPoint);

Điều này cũng bắt đầu hơi ngớ ngẩn khi bạn đi đến nhiều đối tượng lồng nhau và dẫn đến các lỗ thỏ lớn để có được chức năng nhất định của bạn và cuối cùng bạn có thể quên mất nó ở đâu - cũng có mùi giống như thiết kế mã xấu.

Cách chính xác để thiết kế tất cả điều này là gì?

53
WDUK

Không bao giờ quên Law of Demeter :

Nguyên tắc của Demeter (LoD) hoặc về kiến ​​thức tối thiểu là một hướng dẫn thiết kế để phát triển phần mềm, đặc biệt là các chương trình hướng đối tượng. Ở dạng chung, LoD là một trường hợp cụ thể của khớp nối lỏng lẻo. Hướng dẫn đã được đề xuất bởi Ian Holland tại Đại học Đông Bắc vào cuối năm 1987 và có thể được tóm tắt ngắn gọn theo từng cách sau: [1]

  • Mỗi đơn vị chỉ nên có kiến ​​thức hạn chế về các đơn vị khác: chỉ các đơn vị "chặt chẽ" liên quan đến đơn vị hiện tại.
  • Mỗi đơn vị chỉ nên nói chuyện với bạn bè của mình; đừng nói chuyện với người lạ.
  • Chỉ nói chuyện với bạn bè trực tiếp của bạn .

Khái niệm cơ bản là một đối tượng nhất định sẽ giả định càng ít càng tốt về cấu trúc hoặc tính chất của bất kỳ thứ gì khác ( bao gồm các thành phần phụ của nó) , phù hợp với nguyên tắc "che giấu thông tin".
[.__.] Nó có thể được xem như là một hệ quả của nguyên tắc đặc quyền tối thiểu, điều này cho thấy rằng một mô-đun chỉ có thông tin và tài nguyên cần thiết cho mục đích hợp pháp của nó.


building.floor.bounds.GetLocalPoint(worldPoint);

Mã này vi phạm LOD. Người tiêu dùng hiện tại của bạn bằng cách nào đó được yêu cầu phải biết:

  • Tòa nhà có floor
  • Rằng sàn có bounds
  • Các giới hạn có phương thức GetLocalPoint

Nhưng trong thực tế, người tiêu dùng của bạn chỉ nên xử lý building, chứ không phải bất cứ thứ gì bên trong tòa nhà (không nên xử lý trực tiếp các thành phần phụ).

Nếu bất kỳ của các lớp cơ bản này thay đổi về mặt cấu trúc, bạn đột nhiên cũng phải thay đổi người tiêu dùng này, mặc dù anh ta có thể tăng vài cấp so với lớp bạn thực sự đã thay đổi.
[.___.] Điều này bắt đầu xâm phạm đến việc tách các lớp bạn có, vì một thay đổi ảnh hưởng đến nhiều lớp (không chỉ là hàng xóm trực tiếp của nó).

public Vector3 GetLocalPoint(Vector3 worldPoint){    
    return floor.GetLocalPoint(worldPoint);
}

Giả sử bạn giới thiệu một loại tòa nhà thứ hai, một loại không có sàn. Tôi không thể nghĩ ra một ví dụ trong thế giới thực, nhưng tôi đang cố gắng hiển thị trường hợp sử dụng tổng quát, vì vậy hãy giả sử rằng EtherealBuilding là một trường hợp như vậy.

Bởi vì bạn có building.GetLocalPoint phương thức, bạn có thể thay đổi hoạt động của nó mà không cần người tiêu dùng tòa nhà của bạn biết về nó, ví dụ:

public class EtherealBuilding : Building {
    public Vector3 GetLocalPoint(Vector3 worldPoint){    
        return universe.CenterPoint; // Just a random example
    }
}

Điều làm cho điều này trở nên khó hiểu hơn là không có trường hợp sử dụng rõ ràng cho một tòa nhà không có sàn. Tôi không biết tên miền của bạn và tôi không thể thực hiện cuộc gọi phán xét nếu/điều đó sẽ xảy ra như thế nào.

Nhưng hướng dẫn phát triển là những cách tiếp cận tổng quát từ bỏ các ứng dụng theo ngữ cảnh cụ thể. Nếu chúng ta thay đổi bối cảnh, ví dụ sẽ trở nên rõ ràng hơn:

// Violating LOD

bool isAlive = player.heart.IsBeating();

// But what if the player is a robot?

public class HumanPlayer : Player {
    public bool IsAlive() {
        return this.heart.IsBeating();
    }
}

public class RobotPlayer : Player {
    public bool IsAlive() {
        return this.IsSwitchedOn();
    }
}

// This code works for both human and robot players, and thus wouldn't need to be changed when new (sub)types of players are developed.

bool isAlive = player.IsAlive();

Điều này chứng minh tại sao phương thức trên lớp Player (hoặc bất kỳ lớp dẫn xuất nào của nó) có một mục đích, ngay cả khi việc triển khai hiện tại của nó là tầm thường .


Sidenote
[.__.] Vì lợi ích của ví dụ, tôi đã bỏ qua một vài cuộc thảo luận tiếp tuyến, chẳng hạn như cách tiếp cận kế thừa. Đây không phải là trọng tâm của câu trả lời.

110
Flater

Nếu bạn có các phương pháp như vậy thỉnh thoảng ở đây và ở đó, nó có thể chỉ là một tác dụng phụ (hoặc giá phải trả, nếu bạn muốn) của một thiết kế phù hợp.

Nếu bạn có rất nhiề trong số họ thì tôi sẽ coi đó là một dấu hiệu cho thấy bản thân thiết kế này có vấn đề.

Trong ví dụ của bạn, có lẽ không nên một cách để "lấy điểm cục bộ vào tòa nhà" từ bên ngoài tòa nhà và thay vào đó, các phương thức của tòa nhà nên ở mức độ trừu tượng cao hơn và làm việc với như vậy điểm chỉ trong nội bộ.

21
Michael Borgwardt

"Law of Demeter" nổi tiếng là một đạo luật quy định loại mã nào để viết, nhưng nó không giải thích bất cứ điều gì hữu ích. Câu trả lời của Flater là tốt vì nó đưa ra ví dụ, nhưng tôi sẽ không gọi đây là "vi phạm/tuân thủ luật pháp của Demeter". Nếu "Luật của Demeter" được thi hành tại nơi bạn đang ở, vui lòng liên hệ với Sở cảnh sát Demeter địa phương của bạn, họ sẽ vui lòng giải quyết các vấn đề với bạn.

Hãy nhớ rằng bạn luôn nắm vững mã bạn viết, và do đó, giữa việc tạo "các hàm ủy nhiệm" và không viết chúng, đó là vấn đề phán đoán của riêng bạn. Không có đường nét rõ ràng, vì vậy không một quy tắc sắc nét nào có thể được xác định. Ngược lại, chúng ta có thể tìm thấy các trường hợp, như Flater đã làm, trong đó việc tạo các hàm như vậy là vô dụng và việc tạo các hàm như vậy là hữu ích. ( Spoiler: Trong trường hợp trước, cách khắc phục là nội tuyến hàm. Trong phần sau, cách khắc phục là tạo hàm)

Các ví dụ trong đó không có ích khi xác định hàm ủy nhiệm bao gồm khi lý do duy nhất sẽ là:

  • Để truy cập một thành viên của một đối tượng được trả về bởi một thành viên, khi thành viên đó không phải là một chi tiết thực hiện cần được gói gọn.
  • Thành viên giao diện của bạn được triển khai chính xác bởi .NET's thực hiện gần đúng
  • Để được tuân thủ Demeter

Các ví dụ hữu ích khi tạo chức năng ủy nhiệm bao gồm:

  • Bao thanh toán một chuỗi cuộc gọi được lặp đi lặp lại
  • Khi ngôn ngữ buộc bạn phải, ví dụ: triển khai một thành viên giao diện bằng cách ủy quyền cho một thành viên khác hoặc chỉ cần gọi một chức năng khác
  • Khi chức năng bạn gọi không ở cùng cấp độ khái niệm với các cuộc gọi khác ở cùng cấp độ (ví dụ: cuộc gọi LoadAssembly ở cùng cấp độ so với hướng nội của plugin)
1
Laurent LA RIZZA

Quên rằng bạn biết việc triển khai Xây dựng trong giây lát. Một số người khác đã viết nó. Có thể một nhà cung cấp chỉ cung cấp cho bạn mã biên dịch. Hoặc một số nhà thầu thực sự bắt đầu viết nó vào tuần tới.

Tất cả những gì bạn biết là giao diện cho Tòa nhà và các cuộc gọi bạn thực hiện với giao diện đó. Tất cả đều trông khá hợp lý, vì vậy bạn ổn.

Bây giờ bạn mặc một chiếc áo khoác khác và đột nhiên bạn là người thực hiện Tòa nhà. Bạn không biết việc triển khai Tầng, bạn chỉ biết giao diện. Bạn sử dụng giao diện Tầng để triển khai lớp Xây dựng của mình. Bạn biết giao diện cho Tầng và các cuộc gọi bạn thực hiện với giao diện đó để triển khai lớp Xây dựng của bạn và tất cả chúng đều trông khá hợp lý, vì vậy bạn sẽ ổn trở lại.

Tất cả trong tất cả, không có vấn đề. Mọi thứ đều ổn.

1
gnasher729

building.floor.bound.GetLocalPoint (worldPoint);

là xấu.

Các đối tượng chỉ nên đối phó với hàng xóm trực tiếp của họ vì hệ thống của bạn sẽ RẤT khó thay đổi.

0
kiwicomb123

Chỉ cần gọi các chức năng là được. Có nhiều sự kiện mẫu thiết kế đang sử dụng kỹ thuật đó, ví dụ như bộ chuyển đổi và mặt tiền nhưng cũng có một số mẫu mở rộng như trang trí, proxy và nhiều mẫu khác.

Đó là tất cả về mức độ trừu tượng. Bạn không nên kết hợp các khái niệm từ các mức độ trừu tượng khác nhau. Để làm như vậy đôi khi bạn cần thực hiện cuộc gọi đến các đối tượng bên trong để khách hàng của bạn sẽ không bị buộc phải tự làm điều đó.

Ví dụ (Ví dụ về xe hơi sẽ đơn giản hơn):

Bạn có các đối tượng Driver, Car và Wheel. Trong thế giới thực, để lái xe, bạn có tài xế làm gì đó trực tiếp bằng bánh xe hay anh ta chỉ tương tác với toàn bộ xe?

Làm thế nào để biết rằng một cái gì đó KHÔNG ổn:

  • Đóng gói bị hỏng, các đối tượng bên trong có sẵn trong API công khai. (ví dụ: mã như xe hơi.Wheel.Move ()).
  • Nguyên tắc SRP bị hỏng, các đối tượng đang làm nhiều việc khác nhau (ví dụ: chuẩn bị văn bản thông điệp email và thực sự gửi nó trong cùng một đối tượng).
  • Rất khó để kiểm tra đơn vị lớp cụ thể (ví dụ: có nhiều phụ thuộc).
  • Có các chuyên gia tên miền khác nhau (hoặc bộ phận công ty) xử lý những việc bạn xử lý trong cùng một lớp (ví dụ: bán hàng và giao hàng trọn gói).

Các vấn đề tiềm ẩn khi vi phạm Luật Demeter:

  • Kiểm tra đơn vị cứng.
  • Sự phụ thuộc vào cấu trúc bên trong của các đối tượng khác.
  • Khớp nối cao giữa các vật thể.
  • Phơi bày dữ liệu nội bộ.
0
0lukasz0