.NET中的设计模式三:组合模式 选择自 lane_cn 的 Blog

本文介绍了组合模式,它是一种结构型模式,适用于多个类有相似形式或共同代表完整概念的情况。通过XBox封装GameBox和InternetBox、公司人员组织结构树等例子说明其应用。还阐述了使用接口将界面显示与业务数据分离以提高程序可测试性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 组合模式(Composite)是一种“结构型”模式(Structural)。结构型模式涉及的对象为两个或两个以上,表示对象之间的活动,与对象的结构有关。

先举一个组合模式的小小例子:

如图:系统中有两种Box:Game Box和Internet Box,客户需要了解者两个类的接口分别进行调用。为了简化客户的工作,创建了XBox类,程序代码如下:

GameBox的代码:

    public class GameBox

    {

        public void PlayGame()

        {

            Console.WriteLine("plaly game");

        }

     }

InternetBox的代码:

    public class InternetBox

    {

        public void ConnectToInternet()

        {          

            Console.WriteLine("connect to internet");

        }

        public void GetMail()

        {          

            Console.WriteLine("check email");

        }

    }

XBox的代码:

    public class XBox

    {

        private GameBox mGameBox=null;

        private InternetBox mInternetBox=null;

 

        public XBox()

        {

            mGameBox = new GameBox();

            mInternetBox = new InternetBox();

        }

        public void PlayGame()

        {

            mGameBox.PlayGame();

        }

        public void ConnectToInternet()

        {

            mInternetBox.ConnectToInternet();

        }

        public void GetMail()

        {

            mInternetBox.GetMail();

        }      

    }

XBox中封装了GameBox和InternetBox的方法,这样,用户面对的情况就大大的简化了,调用的代码如下:

    public class CSComposite

    {

        static void Main (string[] args)

        {

            XBox x = new XBox();

            Console.WriteLine("PlayGame!");

            x.PlayGame();

            Console.WriteLine();

 

            Console.WriteLine("Internet Play Game!");

            x.ConnectToInternet();

            x.PlayGame();

            Console.WriteLine();

 

            Console.WriteLine("E-Mail!");

            x.GetMail();

        }      

    }

可以看见,用户只需要了解XBox的接口就可以了。

组合模式的应用例子

组合模式适用于下面这样的情况:两个或者多个类有相似的形式,或者共同代表某个完整的概念,外界的用户也希望他们合而为一,就可以把这几个类“组合”起来,成为一个新的类,用户只需要调用这个新的类就可以了。

下面举一个例子说明Composite模式的一个实际应用。下面的Class视图:

Employee类是AbstractEmployee接口的一个实现,Boss类是Employee的一个子类,EmpNode是从树视图的TreeNode类继承而来的。我们先看看代码:

AbstractEmployee,这是一个接口,提供下列方法:

public interface AbstractEmployee {

    float getSalary(); //get current salary

    string getName(); //get name

    bool isLeaf(); //true if leaf

    void add(string nm, float salary); //add subordinate

    void add(AbstractEmployee emp); //add subordinate

    IEnumerator getSubordinates(); //get subordinates

    AbstractEmployee getChild(); //get child

    float getSalaries(); //get salaries of all

}

Employee类是AbstractEmployee接口的一个实现

public class Employee :AbstractEmployee {

    protected float salary;

    protected string name;

    protected ArrayList subordinates;

 

    //------

    public Employee(string nm, float salry) {

        subordinates = new ArrayList();

        name = nm;

        salary = salry;

    }

 

    //------

    public float getSalary() {

        return salary;

    }

 

    //------

    public string getName() {

        return name;

    }

 

    //------

    public bool isLeaf() {

        return subordinates.Count == 0;

    }

 

    //------

    public virtual void add(string nm, float salary) {

        throw new Exception("No subordinates in base employee class");

    }

 

    //------

    public virtual void add(AbstractEmployee emp) {

        throw new Exception("No subordinates in base employee class");

    }

 

    //------

    public IEnumerator getSubordinates() {

        return subordinates.GetEnumerator ();

    }

 

    public virtual AbstractEmployee getChild() {

        return null;

    }

 

    //------

    public float getSalaries() {

        float sum;

        AbstractEmployee esub;

        //get the salaries of the boss and subordinates

        sum = getSalary();

        IEnumerator enumSub = subordinates.GetEnumerator() ;

        while (enumSub.MoveNext()) {

            esub = (AbstractEmployee)enumSub.Current;

            sum += esub.getSalaries();

        }

        return sum;

    }

}

从Employee接口和他的一个实现来看,下面很可能要将这个类型的数据组合成一个树的结构。

Boss类是Employee类的派生,他重载了Employee类的add和getChild方法:

public class Boss:Employee

{

    public Boss(string name, float salary):base(name,salary) {

    }

 

    //------

    public Boss(AbstractEmployee emp):base(emp.getName() , emp.getSalary()) {

    }

 

    //------

    public override void add(string nm, float salary) {

        AbstractEmployee emp = new Employee(nm,salary);

        subordinates.Add (emp);

    }

 

    //------

    public override void add(AbstractEmployee emp){

        subordinates.Add(emp);

    }

 

    //------

    public override AbstractEmployee getChild() {

        bool found;

        AbstractEmployee tEmp = null;

        IEnumerator esub ;

 

        if (getName().Equals (getName()))

            return this;

        else {

            found = false;

            esub = subordinates.GetEnumerator ();

            while (! found && esub.MoveNext()) {

                tEmp = (AbstractEmployee)esub.Current;

                found = (tEmp.getName().Equals(name));

                if (! found) {

                    if (! tEmp.isLeaf()) {

                        tEmp = tEmp.getChild();

                        found = (tEmp.getName().Equals(name));

                    }

                }

            }

            if (found)

                return tEmp;

            else

                return new Employee("New person", 0);

        }

    }

}

getChild方法是一个递归调用,如果Child不是Leaf,就继续调用下去。上面几个类表达了一个树的结构,表示出了公司中的领导和雇员的级别关系。

现在我们看一下这个程序需要达到的目标,程序运行后显示下面的界面:

界面上有一个树图,树上显示某公司的人员组织结构,点击这些雇员,会在下面出现这个人的工资。现在程序中有两棵树:一棵是画面上实际的树,另一个是公司中雇员的虚拟的树。画面上的树节点是TreeNode类型,雇员的虚拟树节点是AbstractEmployee类型。我们可以采用组合模式,创造一种新的“节点”,组合这两种节点的特性,简化窗体类需要处理的情况,请看下面的代码:

public class EmpNode:TreeNode {

    private AbstractEmployee emp;

 

    public EmpNode(AbstractEmployee aemp ):base(aemp.getName ()) {

        emp = aemp;

    }

 

    //-----

    public AbstractEmployee getEmployee() {

        return emp;

    }

}

EmpNode类是TreeNode类的子类,他具有TreeNode类的所有特性,同时他也组合了AbstractEmployee类型的特点。这样以来调用者的工作就简化了。下面是Form类的代码片断,我把自动生成的代码省略了一部分:

public class Form1 : System.Windows.Forms.Form {

    private System.Windows.Forms.Label lbSalary;

 

    /// <summary>

    /// Required designer variable.

    /// </summary>

    private System.ComponentModel.Container components = null;

    AbstractEmployee prez, marketVP, salesMgr;

    TreeNode rootNode;

    AbstractEmployee advMgr, emp, prodVP, prodMgr, shipMgr;

    private System.Windows.Forms.TreeView EmpTree;

    private Random rand;

 

    private void init() {

        rand = new Random ();

        buildEmployeeList();

        buildTree();

    }

 

    //---------------

    private void buildEmployeeList() {

        prez = new Boss("CEO", 200000);

        marketVP = new Boss("Marketing VP", 100000);

        prez.add(marketVP);

        salesMgr = new Boss("Sales Mgr", 50000);

        advMgr = new Boss("Advt Mgr", 50000);

        marketVP.add(salesMgr);

        marketVP.add(advMgr);

        prodVP = new Boss("Production VP", 100000);

        prez.add(prodVP);

        advMgr.add("Secy", 20000);

 

        //add salesmen reporting to sales manager

        for (int i = 1; i<=5; i++){

            salesMgr.add("Sales" + i.ToString(), rand_sal(30000));

        }

        prodMgr = new Boss("Prod Mgr", 40000);

        shipMgr = new Boss("Ship Mgr", 35000);

        prodVP.add(prodMgr);

        prodVP.add(shipMgr);

 

        for (int i = 1; i<=3; i++){

            shipMgr.add("Ship" + i.ToString(), rand_sal(25000));

        }

        for (int i = 1; i<=4; i++){

            prodMgr.add("Manuf" + i.ToString(), rand_sal(20000));

        }

    }

 

    //-----

    private void buildTree() {

        EmpNode nod;

        nod = new EmpNode(prez);

        rootNode = nod;

        EmpTree.Nodes.Add(nod);

        addNodes(nod, prez);

    }

 

    //------

    private void getNodeSum(EmpNode node) {

        AbstractEmployee emp;

        float sum;

        emp = node.getEmployee();

        sum = emp.getSalaries();

        lbSalary.Text = sum.ToString ();

    }

 

    //------

    private void addNodes(EmpNode nod, AbstractEmployee emp) {

        AbstractEmployee newEmp;

        EmpNode newNode;

        IEnumerator empEnum;

        empEnum = emp.getSubordinates();

 

        while (empEnum.MoveNext()) {

            newEmp = (AbstractEmployee)empEnum.Current;

            newNode = new EmpNode(newEmp);

            nod.Nodes.Add(newNode);

            addNodes(newNode, newEmp);

        }

    }

 

    //------

    private float rand_sal(float sal) {

        float rnum = rand.Next ();

        rnum = rnum / Int32.MaxValue;

        return rnum * sal / 5 + sal;

    }

 

    //------

    public Form1() {

        //

        // Required for Windows Form Designer support

        //

        InitializeComponent();

        init();

        //

        // TODO: Add any constructor code after InitializeComponent call

        //

    }

 

    /// <summary>

    /// Clean up any resources being used.

    /// </summary>

    protected override void Dispose( bool disposing ) {

        if( disposing ) {

            if (components != null) {

                components.Dispose();

            }

        }

        base.Dispose( disposing );

    }

    /// <summary>

    /// The main entry point for the application.

    /// </summary>

    [STAThread]

    static void Main() {

        Application.Run(new Form1());

    }

 

    private void EmpTree_AfterSelect(object sender, TreeViewEventArgs e) {

        EmpNode node;

        node = (EmpNode)EmpTree.SelectedNode;

        getNodeSum(node);

    }

}

EmpTree_AfterSelect方法是树图点击节点事件的响应方法,用户点击节点后在文本栏里显示相应的工资。组合模式已经介绍完了,下面的东西和组合模式没有什么关系。

为什么用interface

为什么要在程序中创建雇员的interface呢?我们可以创建一个class Employee,再派生出Boss,一样可以实现上面的功能嘛。

使用interface是为了将画面上的显示程序与后台的业务数据程序分离开。画面的显示程序只需要关心“雇员”提供哪些接口就可以工作了,而不去过问具体的细节,比如工资的计算规则。如果需要对界面类和数据类分别进行单元测试,这样的做法也提供了可能(也就是说,这个程序是可测试的)。测试画面的时候可以在雇员接口上实现一些虚假的雇员类,其中的方法和属性都是为了测试而假造的,这样就可以测试界面的显示是否正确。一般说来程序如果要进行单元测试,应该从设计阶段就考虑程序的“可测试性”,其中重要的一点是:将界面表示与业务逻辑分离开。

关于如何提高程序的可测试性,以后有时间我会整理一些心得体会。

<think>嗯,用户遇到了一个C++编译错误,错误信息是“error: extra qualification ‘ft::per_env::MapperViewer::’ on member ‘draw_lane_info’ [-fpermissive]”,在代码的第42行。我需要先理解这个错误的具体原因,然后给出解决方案。 首先,我应该回忆一下C++中类成员函数的定义和声明方式。通常,在类的内部声明成员函数时,不需要加上类的作用域限定符。例如,在类MapperViewer的内部声明成员函数draw_lane_info,应该是直接写返回类型、函数名和参数,而不需要写MapperViewer::。 用户提供的代码片段显示的是在类定义内部使用了void MapperViewer::draw_lane_info(...),这显然是不正确的。因为当在类内部声明成员函数时,函数名前面不需要加上类名和作用域解析运算符。正确的声明方式应该是void draw_lane_info(...);。 这个错误通常发生在开发者误将函数定义的方式用在了函数声明上。在类定义外部定义成员函数时,才需要使用类名加作用域限定符,例如void ft::per_env::MapperViewer::draw_lane_info(...) { ... }。但在类内部的声明部分,这样写就是多余的,导致编译器报错。 另外,错误提示中的[-fpermissive]说明这个错误属于权限问题,编译器在严格模式下不允许这种写法,但可能有些编译器在较宽松的模式下会警告而不是错误。不过用户应该按照标准写法修正。 接下来,我需要检查用户提供的代码结构,假设MapperViewer是在ft::per_env命名空间中定义的,那么在类内部声明成员函数时,正确的做法是省略类限定符。例如: namespace ft { namespace per_env { class MapperViewer { public: void draw_lane_info(visualization_msgs::MarkerArray& LaneInfo); // 正确声明 }; } // namespace per_env } // namespace ft 而如果函数定义在类外部,则需要完整的限定: void ft::per_env::MapperViewer::draw_lane_info(visualization_msgs::MarkerArray& LaneInfo) { // 实现代码 } 用户可能在类的声明中错误地加上了MapperViewer::,导致编译器认为这是在类外定义成员函数,从而报错。 总结问题原因:在类内部声明成员函数时错误地加上了类的作用域限定符,导致编译器无法识别,从而产生extra qualification的错误。 解决方案就是让用户检查类定义内部的成员函数声明,去掉多余的类限定符,只保留函数名即可。同时,确认函数定义在类外部时正确使用了作用域限定符。 另外,可能需要提醒用户注意命名空间和类的作用域嵌套是否正确,确保所有限定符的使用位置正确,避免类似的错误再次发生。</think>你遇到的编译错误是因为在类成员函数的**声明处**错误地添加了类作用域限定符。让我们逐步分析问题: ```cpp // 错误示例 class MapperViewer { public: void MapperViewer::draw_lane_info(...); // 错误的限定符 }; ``` 🛑 错误原因: 1. 在类定义**内部**声明成员函数时,直接使用函数名即可 2. `MapperViewer::` 是多余的类作用域限定符(称为 "extra qualification") ✅ 正确写法: ```cpp // 正确声明(类定义内部) class MapperViewer { public: void draw_lane_info(visualization_msgs::MarkerArray& LaneInfo); // 去掉类名限定 }; // 正确实现(类定义外部) void ft::per_env::MapperViewer::draw_lane_info(visualization_msgs::MarkerArray& LaneInfo) { // 具体实现 } ``` 💡 关键区别: - 类内部声明:不要加 `ClassName::` - 类外部定义:必须加完整的命名空间和类限定符 🔍 典型错误场景: ```cpp namespace ft::per_env { class MapperViewer { public: // 错误!类内部声明不应加限定 void MapperViewer::draw_lane_info(...); // 正确声明 void correct_declaration(...); }; } // 正确的外部定义 void ft::per_env::MapperViewer::correct_declaration(...) { // 实现代码 } ``` 📌 建议检查: 1. 所有类内成员函数声明是否有多余的类限定符 2. 确认命名空间嵌套关系是否正确 3. 当出现多个嵌套命名空间时,注意作用域解析符的使用顺序 这个错误常见于从类外定义复制函数声明到类内部时,忘记删除类限定符的情况。保持声明和定义的格式区分可以有效避免此类问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值