C++ 数据分组 class 和 const 访问

C++ data grouping class and const access

这个问题涉及 class 设计和一致的界面(我猜)。

假设您有一个小的 class 来表示道路的 "Geometry"...它可能包含许多类似这样的属性和方法...

class RoadMap
{
    private:

    struct RoadPiece
    {
        float x1, y1, x2, y2;
    };  

    std::string name;
    float area_width;
    float area_height;

    std::vector<RoadPiece> pieces;

    public:

    const std::string& get_name() const {return name;}
    float get_width() const {return area_width;}
    float get_height() const {return area_height;}
    float get_area() const {return area_width * area_height;}

    void set_width(float v) {area_width=v;}
    void set_height(float v) {area_height=v;}
    void set_name(const std::string v) {name=v;}

    void add_road_piece(float x1, float y1, float x2, float y2)
    {
        //...
    }
}

如您所见,我们正在混合使用 const 和非 const 方法。没什么大不了的:我们几乎可以像这样编写客户端代码

RoadMap m;

m.set_width(100.0);
m.set_height(150.0);
m.set_name("Northern Hills");

//Tedious code here...

std::cout<<"The area of the map is "<<m.get_area()<<std::endl;

现在,假设我们要向地图添加 "another layer" 不一定属于地图的信息,而是...在客户端代码中对其进行补充...比如说,交通标志

class TrafficSignsMap
{
    private:

    struct Sign
    {
        enum class types {STOP, YIELD, STEP_ON_IT};
        types type;     
        float x;
        float y;
    }

    std::vector<Sign> signs;

    public:

    void add_stop_sign(float x, float y) {/*Blah blah*/}
    void add_yield_sign(float x, float y) {/*Blah blah*/}
    void add_step_on_it_sign(float x, float y) {/*Blah blah*/}

    const std::vector<Sign>& get_all_signs() {return signs;}
    const std::vector<const Sign const  *> get_signs_in_area(float x1, float y1, float x2, float y2)
    {
        //Do some calculations, populate a vector with pointers to signs, return it...
    }
}

同样,我们可以编写各种客户端代码并混合道路和注册。在这一点上,请注意,我并不是真的在做这个应用程序,只是举个例子...

不管怎样,再写一些代码,我就来了第三层数据……这次是"PlacesToEat"。我不会在这里描述它们,但你明白了:它独立存在,但可以与道路或标志共享某个 space(更像是道路,但是......)。通过这第三层也是最后一层,我们找到了一个地方,我们可以在其中获取包含道路、标志和餐饮场所信息的文件。我们认为我们可以写一个 class 来提供文件,它会为我们存储信息。像这样:

class MapData
{
    private:

    RoadMap roads;
    TrafficSignsMap signs;
    PlacesToEatMap places_to_eat;

    public:

    MapData(const std::string& filename)
    {
        std::ifstream(filename);

        //Read the file... populate our properties...
    }

    const RoadMap& get_road_map() const {return roads;} 
    const TrafficSignsMap& get_signs_map() const {return signs;}
    const PlacesToEatMap& get_places_to_eat_map() const {return places_to_eat;}
};

事情是这样的……一旦所有数据都集中在一个大容器中,我们应该同时提供 const 和非 const 访问,对吗?。我想获取所有 const 数据,但我也应该能够添加新的就餐场所,这是我不应该使用当前界面做的事情。

现在我知道我可以使用 MapData class 作为代理(增加它在应用程序中的责任)所以我会去:

MapData MD;
MD.add_stop_sign(10.0, 20.0);   //This, in time, proxies to the inner property.

或者我可以像这样添加 const 和非 const getter(增加我的头痛):

MapData MD;
float area=MD.get_road_map().get_area();
MD.get_non_const_road_map().add_road(/*blah blah*/);

或者我可以搞砸它并使这些属性 public:

public:

RoadMap roads;
TrafficSignsMap signs;
PlacesToEatMap places_to_eat;

或者我可以让 getter 成为非 const,因为我打算修改数据并完成它(这里并不是真正的缺点......我想,假设我得到一个 const MapData 对象,我应该无论如何都不能改变它):

    RoadMap& get_road_map() {return roads;} 
    TrafficSignsMap& get_signs_map() {return signs;}
    PlacesToEatMap& get_places_to_eat_map() {return places_to_eat;}

再次请注意,这个问题已经被编辑了,场景已经编好了(为什么路线图会存储尺寸??)。考虑到这一点,你会如何处理这种情况?我正在寻找一种方法,使 MapData class 尽可能可扩展,以防万一我想添加更多层(应该放弃代理选项)并且尽可能正确。非常感谢。

当然有很多方法可以做到这一点。但从设计的角度来看,保持一致很重要(see here: "consistency coincides with conceptual integrity")。

您对三个容器 classes RoadMapTrafficSignsMapPlacesToEatMap 的方法都共享以下原则:

  • 容器中的对象是私有结构
  • 容器的用户不知道对象,只知道定义对象的数据(例如:路段的四个浮点数)。
  • 使用构成对象的数据在容器中进行插入(以及检索?)。

如果你想保持一致,那么你应该对 MapData 采用相同的方法:使用代理方法(你的第一个选择)。

个人(但这里我们离开 objective 事实并输入主观意见)我认为这种设计没有利用面向对象的设计。我并不是说这不好:这样做可能有充分的理由。但不是最优的。为什么 ?您的 classes 的用户无法操纵您的 class 围绕其设计的应用程序对象:他不使用路段,而只使用路段坐标。例如:如果稍后您认为 float 不够精确,应该使用 double 而不是必须审查每段代码。如果您意识到可能存在隧道并且您的坐标需要是 3D 的,那将是一场真正的维护灾难