2010-04-06 12 views
15

Đây là mã của tôi:Object thiết kế theo định hướng đề nghị

class Soldier { 
public: 
    Soldier(const string &name, const Gun &gun); 
    string getName(); 
private: 
    Gun gun; 
    string name; 
}; 

class Gun { 
public: 
    void fire(); 
    void load(int bullets); 
    int getBullets(); 
private: 
    int bullets; 
} 

tôi cần phải gọi tất cả các chức năng thành viên của Gun qua một đối tượng Soldier. Một cái gì đó như:

soldier.gun.fire(); 

hoặc

soldier.getGun().load(15); 

Vì vậy, cái nào là một thiết kế tốt hơn? Ẩn đối tượng súng làm thành viên riêng và truy cập nó bằng hàm getGun(). Hay biến nó thành một thành viên công cộng? Hoặc tôi có thể đóng gói tất cả các chức năng này sẽ làm cho việc triển khai khó khăn hơn:

soldier.loadGun(15); // calls Gun.load() 
soldier.fire(); // calls Gun.fire() 

Vậy bạn nghĩ điều gì là tốt nhất?

+3

Kết hợp câu trả lời từ Stephen và Thất vọng. Có một 'get_gun()' riêng có thể hoạt động cần thiết cho người lính để lấy khẩu súng, như Stephen cho thấy, nhưng nói với * người lính * phải làm gì, không phải khẩu súng, như Frustrated cho thấy. – GManNickG

+0

@GMan - Cảm ơn, tôi đồng ý. Tuy nhiên, thay vì cách tiếp cận @ Frustrated, nói với người lính phải làm gì sẽ là phương pháp tiếp cận của @ Austin? ví dụ. soldier.Attack() thay vì "soldier.loadGun()". – Stephen

+0

@Stephen: Vâng, bạn có thể làm cho đóng gói sâu hơn như bạn muốn. Câu hỏi của tôi chỉ là về phần này. Tất nhiên bạn có thể nói soldier.Attack() và bên trong chức năng này bạn có thể kiểm tra nếu (! Gun.getBullets()) trả lại hoặc các công cụ như thế. – pocoa

Trả lời

21

tôi sẽ nói đi với tùy chọn thứ hai của bạn:

soldier.loadGun(15); // calls Gun.load() 
soldier.fire(); // calls Gun.fire() 

Ban đầu nó là một công việc, nhưng khi hệ thống trở nên phức tạp hơn, bạn có thể thấy rằng một người lính sẽ muốn làm những việc khác trước và sau khi bắn súng của họ (có thể kiểm tra xem họ có đủ đạn và sau đó hét lên "Chết suckers !!" trước khi bắn, và lẩm bẩm "đó là đau đớn" sau đó, và kiểm tra xem họ có cần tải lại hay không). Nó cũng ẩn từ những người sử dụng của lớp Soldier những chi tiết không cần thiết về cách chính xác súng đang được bắn.

+3

+1 cho mô tả của bạn về lớp Người lính: P –

+1

Tôi yêu các ví dụ của bạn :) – Dinah

+2

Ngoài ra, đừng nói loadGun, hãy nói là PrepareWeapon. Bằng cách này khi người lính của bạn đang ở trong một chiếc xe tăng, anh ta không dò dẫm với khẩu súng lục của mình khi anh ta nên quay khẩu pháo xe tăng. – jmucchiello

1

Không có quy tắc vàng nào áp dụng 100% thời gian. Nó thực sự là một cuộc gọi phán xét tùy thuộc vào nhu cầu của bạn.

Tùy thuộc vào số lượng chức năng bạn muốn ẩn/không cho phép súng truy cập vào Bộ xử lý.

Nếu bạn muốn chỉ có quyền truy cập chỉ đọc vào Súng, bạn có thể trả lại tham chiếu const cho thành viên của riêng bạn.

Nếu bạn muốn chỉ hiển thị một số chức năng nhất định, bạn có thể tạo các hàm bao bọc. Nếu bạn không muốn người dùng cố gắng thay đổi cài đặt Gun thông qua Soldier thì hãy thực hiện các hàm wrapper. Mặc dù vậy, tôi thấy Gun là đối tượng riêng của nó và nếu bạn không để lộ tất cả các chức năng của Gun, và đừng bận tâm cho phép mọi thứ được thay đổi thông qua đối tượng Soldier, hãy công khai nó.

Bạn có thể không muốn sao chép súng vì vậy nếu bạn thực hiện phương thức GetGun(), hãy đảm bảo rằng bạn không trả lại bản sao của khẩu súng.

Nếu bạn muốn giữ mã của mình đơn giản thì hãy nhờ người lính chịu trách nhiệm xử lý súng. Mã khác của bạn có cần phải làm việc trực tiếp với súng không? Hay một người lính có thể biết cách làm việc/nạp lại khẩu súng của chính mình?

+0

súng là riêng tư trong ví dụ trên, nó sẽ cần phải được thực hiện hoặc công khai hoặc một phương pháp accessor viết như getGun() – Fermin

+0

@ Brian: Tôi cần phải truy cập tất cả các thành viên công cộng của các đối tượng Gun từ Soldier. – pocoa

+0

@pocoa: Sau đó trả lại một tham chiếu đến nó thông qua GetGun() hoặc chỉ làm cho súng công khai. Đó có lẽ là dễ nhất. Khi bạn muốn thay đổi giao diện Gun sau này, bạn sẽ không cần phải thay đổi giao diện của lớp lính. –

1

Cung cấp "getGun()" hoặc đơn giản là "gun()".

Hãy tưởng tượng một ngày nào đó bạn có thể cần phải thực hiện phương pháp phức tạp hơn:

Gun* getGun() { 
    if (!out_of_bullets_) { 
    return &gun_; 
    } else { 
    PullPieceFromAnkle(); 
    return &secret_gun_; 
    } 
} 

Ngoài ra, bạn có thể muốn cung cấp một accessor const để mọi người có thể sử dụng một khẩu súng const trên một người lính const:

const Gun &getGun() const { return gun_; } 
7

Luật Demeter sẽ nói để đóng gói các chức năng.

http://en.wikipedia.org/wiki/Law_of_Demeter

Bằng cách này, nếu bạn muốn có một số loại tương tác giữa người lính và súng, bạn có một không gian để chèn mã.

Chỉnh sửa: Tìm thấy bài viết có liên quan từ liên kết Wikipedia: http://www.ccs.neu.edu/research/demeter/demeter-method/LawOfDemeter/paper-boy/demeter.pdf Ví dụ về giấy trắng rất giống với ví dụ về người lính bạn đăng.

+3

Và hãy nhớ để tránh rơi vào "Luật số điểm thấp" (http://haacked.com/archive/2009/07/14/law-of-demeter-dot-counting.aspx) –

+0

Liên kết tuyệt vời Martinho ! Hoàn toàn tóm tắt cảm xúc của tôi về LOD. –

+0

Trong trường hợp này, cho "Gun" là một tham số hàm tạo, tôi không nghĩ rằng điều này được áp dụng. Nó không phá vỡ đóng gói vì lớp client cần khởi tạo súng để bắt đầu. Đó không phải là để nói rằng một phương pháp Soldier :: Attack (Target * t) không phải là một cách tiếp cận tốt hơn. – Stephen

5

Thật vậy, nó phụ thuộc rất nhiều vào mức độ kiểm soát bạn muốn có.

Để mô hình hóa thế giới thực, bạn thậm chí có thể muốn đóng gói hoàn toàn đối tượng súng và chỉ cần có phương thức soldier.attack(). Phương thức soldier.attack() sau đó sẽ xem liệu người lính có mang theo súng hay không, và trạng thái của khẩu súng là gì, và bắn hoặc nạp lại nó khi cần thiết. Hoặc có thể ném súng vào mục tiêu và bỏ chạy, nếu không có đủ đạn cho một trong hai hoạt động ...

11

Trước hết, bạn sẽ vi phạm Law of Demeter bằng cách truy cập Gun từ bên ngoài lớp Soldier.

tôi sẽ xem xét các phương pháp như thế này thay vì:

soldier.ArmWeapon(...); 
soldier.Attack(...); 

Bằng cách này bạn cũng có thể thực hiện nắm tay, dao, lựu đạn, bóng chày dơi, mèo tia laser của bạn, vv

+1

+1 cho mèo laser! – FrustratedWithFormsDesigner

2

Thông thường quyết định của tôi dựa trên bản chất của lớp container (trong trường hợp này là Soldier). Hoặc là hoàn toàn là POD hoặc không phải là POD. Nếu nó không phải là một POD, tôi làm cho tất cả các thành viên dữ liệu riêng tư và cung cấp các phương thức truy cập. Lớp này chỉ là POD nếu nó không có bất biến (tức là không có cách nào mà một tác nhân bên ngoài có thể làm cho trạng thái của nó không nhất quán bằng cách sửa đổi các thành viên của nó). Lớp lính của bạn trông giống như một POD không với tôi, vì vậy tôi sẽ đi đến tùy chọn phương thức accessor. Nếu nó sẽ trả về một tham chiếu const hoặc một tham chiếu thường xuyên là quyết định của riêng bạn, dựa trên hành vi của lửa() và các phương pháp khác (nếu họ sửa đổi trạng thái của súng hay không).

BTW, Bjarne Stroustrup nói một chút về vấn đề này vào trang web của mình: http://www.artima.com/intv/goldilocks3.html

Một sidenote: Tôi biết đó không phải là chính xác những gì bạn hỏi, nhưng tôi muốn khuyên bạn cũng phải xem xét nhiều đề cập đến việc thực hiện trong câu trả lời khác cho pháp luật của Demeter: để lộ các phương pháp hành động (hành động trên súng) thay vì toàn bộ đối tượng súng thông qua một phương thức getter. Vì người lính "có" khẩu súng (nó nằm trong tay anh ta và anh ta bóp cò), có vẻ như tự nhiên hơn với tôi rằng các diễn viên khác "yêu cầu" người lính bắn. Tôi biết điều này có thể tẻ nhạt nếu súng có nhiều phương pháp để hành động, nhưng cũng có thể chúng có thể được nhóm lại thành nhiều hành động cấp cao hơn mà người lính vạch trần.

+0

Không, Người lính không chỉ là một lớp container. Cảm ơn các liên kết. – pocoa

3

Nếu bạn tiếp xúc với súng, bạn cho phép điều vượt ra ngoài chức năng thành viên của Gun, mà có lẽ không phải là một ý tưởng tốt:

soldier.gun = anotherGun; // where did you drop your old gun? 

Nếu bạn sử dụng getGun(), các cuộc gọi tìm kiếm một chút xấu xí, nhưng bạn có thể thêm các chức năng vào Gun mà không sửa đổi Soldier.

Nếu bạn đóng gói các chức năng (mà tôi đề nghị), bạn có thể sửa đổi Súng hoặc giới thiệu các loại súng khác (có nguồn gốc) mà không thay đổi giao diện cho Người lính.

0

Đóng gói các chức năng để cung cấp giao diện nhất quán ngay cả khi sau này bạn thay đổi logic.Quy ước đặt tên tùy thuộc vào bạn, nhưng tôi thường không sử dụng "getFoo()", nhưng chỉ "foo()" làm người truy cập và "setFoo()" làm người định cư.

  • trả về tham chiếu đến khi bạn có thể (Mục C++ hiệu quả # 3).
  • thích consts, enums, và inlines để sử dụng số cứng mã hoá (Khoản 4 #)
  • cung cấp quy ước đặt tên độc đáo cho các thành viên riêng để phân biệt với đối số
  • Sử dụng giá trị unsigned nơi họ làm cho tinh thần để di chuyển lỗi để biên dịch thời gian
  • Khi giá trị const, như số lượng tối đa, áp dụng cho toàn bộ lớp học. Làm cho chúng tĩnh.
  • Nếu bạn có kế hoạch để kế thừa, đảm bảo hàm hủy của bạn là ảo
  • khởi tạo tất cả các thành viên mặc định lành mạnh

Đây là cách các lớp chăm sóc đó. CodePad

#include <iostream> 
#include <string> 
#include <stdint.h> 

using namespace std; 

class Gun 
{ 
public: 
    Gun() : _bullets(0) {} 
    virtual ~Gun() {} 
    void fire() {cout << "bang bang" << endl; _bullets--;} 
    void load(const uint16_t bullets) {_bullets = bullets;} 
    const int bullets() const {return _bullets;} 

    static const uint16_t MAX_BULLETS = 17; 

protected: 
    int _bullets; 
}; 

class Soldier 
{ 
public: 
    Soldier(const string &name, const Gun &gun) : _name(name), _gun(gun) {} 
    virtual ~Soldier() {} 
    const string& name() const; 
    Gun& gun() {return _gun;} 

protected: 
    string _name; 
    Gun _gun; 
}; 


int main (int argc, char const *argv[]) 
{ 
    Gun gun; // initialize 
    string name("Foo"); 
    Soldier soldier(name, gun); 

    soldier.gun().load(Gun::MAX_BULLETS); 

    for(size_t i = 0; i < Gun::MAX_BULLETS; ++i) 
    { 
    soldier.gun().fire(); 
    cout << "I have " << soldier.gun().bullets() << " left!" << endl; 
    } 
    return 0; 
}