我的计算器——6 用依赖注入改进

本文介绍了一种使用依赖注入和配置文件来改进计算器程序的方法,通过这种方式可以更好地遵循开放封闭原则,减少代码耦合并简化新增功能的过程。

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

 

之前虽然实现了所有的功能,但对工厂类中的switch语句感到很不满意。每次添加一个计算方法,新建一个继承自TokenRecord的类的同时,还必须在工厂中的两个地方注册。这样就违背了开放封闭原则,而且会使代码不停的增长。参考《大话设计模式》中的例子,这里采用依赖注入的方式将switch语句给替代了。如果需要添加一个计算方法,新建一个继承自TokenRecord的类之后,只需要在配置文件中添加注册信息即可,其他代码根本不需要修改,很好的实现了封装变化。

要实现依赖注入首先必须建立一个配置文件,这里采用XML文件,文件名为TokenRecord.xml,和exe文件存放在同一文件夹,文件内容如下:

<?xml version="1.0" encoding="utf-8" ?>

<TokenRecord>

  <TokenKeyword>

    <!--以下字符串处理函数记号对象-->

    <Token Word="mid" Class="TokenMid" />

    <Token Word="left" Class="TokenLeft" />

    <Token Word="right" Class="TokenRight" />

    <Token Word="string" Class="TokenToString" />

    <!--以下为数学运算记号对象-->

    <Token Word="round" Class="TokenRound" />

    <Token Word="abs" Class="TokenAbs" />

    <Token Word="max" Class="TokenMax" />

    <Token Word="min" Class="TokenMin" />

    <Token Word="sin" Class="TokenSin" />

    <Token Word="cos" Class="TokenCos" />

    <Token Word="mod" Class="TokenMod" />

    <Token Word="pow" Class="TokenPow" />

    <!--以下为逻辑运算记号对象-->

    <Token Word="and" Class="TokenAnd" />

    <Token Word="or" Class="TokenOr" />

    <Token Word="not" Class="TokenNot" />

    <Token Word="xor" Class="TokenXor" />

    <!--以下为常量记号对象-->

    <Token Word="pi" Class="TokenValue" />

    <Token Word="e" Class="TokenValue" />

    <Token Word="true" Class="TokenValue" />

    <Token Word="false" Class="TokenValue" />

    <Token Word="if" Class="TokenIf" />

  </TokenKeyword>

  <TokenSymbol>

    <!--以下为分隔符-->

    <Token Word="(" Class="TokenLeftBracket" />

    <Token Word=")" Class="TokenRightBracket" />

    <Token Word="," Class="TokenComma" />

    <!--以下为数学运算符-->

    <Token Word="+" Class="TokenPlus" />

    <Token Word="-" Class="TokenMinus" />

    <Token Word="*" Class="TokenMultiply" />

    <Token Word="/" Class="TokenDivide" />

    <Token Word="%" Class="TokenMod" />

    <Token Word="^" Class="TokenPow" />

    <!--以下为比较运算符-->

    <Token Word="=" Class="TokenEqual" />

    <Token Word="==" Class="TokenEqual" />

    <Token Word="><" Class="TokenNotEqual" />

    <Token Word="!=" Class="TokenNotEqual" />

    <Token Word=">" Class="TokenGreatThan" />

    <Token Word=">=" Class="TokenGreatOrEqual" />

    <Token Word="<" Class="TokenLessThan" />

    <Token Word="<=" Class="TokenLessOrEqual" />

    <!--以下为逻辑运算符-->

    <Token Word="!" Class="TokenNot" />

    <Token Word="&" Class="TokenAnd" />

    <Token Word="&&" Class="TokenAnd" />

    <Token Word="|" Class="TokenOr" />

    <Token Word="||" Class="TokenOr" />

  </TokenSymbol>

</TokenRecord>

其中分成两类,TokenKeywordTokenSymbol,添加新的计算方法时也必须按分类注册到对应的类型中。Word表示的是计算方法的关键字或者运算符,可能需要用转义字符,比如“>”转义成“&gt;”。Class表示计算方法对应的类名称,这里所有的类均继承自ConExpress.Utility.TokenRecord。反射需要获取类的完整名称,而它们所处的命名空间都是ConExpress.Utility,所以这里可以省略命名空间,在程序内部添加。

 

代码中需要修改的地方主要是工厂类,TokenKeywordFactoryTokenSymbolFactory

TokenKeywordFactory中获取关键字列表的原始代码为:

       /// <summary>

        /// 关键字列表,只允许在GetKeywordList中修改

        /// </summary>

        private static List<string> m_ListKeyword = new List<string>();



        /// <summary>

        /// 获取关键字列表

        /// </summary>

        /// <returns>关键字列表</returns>

        /// <remarks>Author:Alex Leo @ ConExpress</remarks>

        public static List<string> GetKeywordList()

        {

            if (m_ListKeyword.Count == 0)

            {

                string[] ArrayString = new string[] { "mid", "left", "right", "string" };//字符串处理函数

                string[] ArrayArithmetic = new string[] { "abs", "round", "max", "min", "sin", "cos", "mod", "pow" };//数学运算函数

                string[] ArrayLogic = new string[] { "and", "or", "not", "xor" };//逻辑运算符号

                string[] ArrayConstant = new string[] { "pi", "e", "true", "false" };//常量符号

                string[] ArrayOther = new string[] { "if" };//其他关键字



                m_ListKeyword.AddRange(ArrayString);

                m_ListKeyword.AddRange(ArrayArithmetic);

                m_ListKeyword.AddRange(ArrayLogic);

                m_ListKeyword.AddRange(ArrayConstant);

                m_ListKeyword.AddRange(ArrayOther);

            }



            return m_ListKeyword;

        }
改进后的代码为:
        private static Dictionary<string, string> m_DictionaryKeyword = new Dictionary<string, string>();

        /// <summary>

        /// 获取关键字字典

        /// </summary>

        /// <returns>关键字字典</returns>

        /// <remarks>Author:Alex Leo @ ConExpress; Date:2008-5-19; Remark:基于本地文件TokenRecord.xml;</remarks>

        private static Dictionary<string, string> GetKeywordDictionary()

        {

            if (m_DictionaryKeyword.Count == 0)

            {

                XmlDocument myDoc = new XmlDocument();

                myDoc.Load("./TokenRecord.xml");

                XmlNodeList KeywordList = myDoc.SelectNodes("TokenRecord/TokenKeyword/Token");



                foreach (XmlNode Node in KeywordList)

                {

                    m_DictionaryKeyword.Add(Node.Attributes["Word"].Value, Node.Attributes["Class"].Value);

                }

            }



            return m_DictionaryKeyword;

        }



 

 
 
采用配置文件后就可以将变化转移到配置文件中,程序里只需要读取配置文件即可。但必须保证配置文件处在程序的运行目录下,而且要保证文件格式和内容的正确性。当然,对保证配置文件的正确性是很容易做到的。
TokenKeywordFactory中产生TokenRecord对象的方法原始代码为:
        /// <summary>

        /// 产生记号对象

        /// </summary>

        /// <param name="TokenWord">分析得到的单词</param>

        /// <param name="Index">当前序号</param>

        /// <returns>记号对象</returns>

        /// <remarks>Author:Alex Leo @ ConExpress</remarks>

        protected static new TokenRecord ProduceToken(string TokenWord, int Index)

        {

            TokenRecord Token;



            //判断是否是关键字

            if (GetKeywordList().Contains(TokenWord.ToLower()))

            {

                //判断关键字类型

                switch (TokenWord.ToLower())

                {

                    case "if"://以下为其他关键字

                        Token = new TokenIf(Index + 1);

                        break;

                    case "mid"://以下字符串处理函数记号对象

                        Token = new TokenMid(Index + 1);

                        break;

                    case "left":

                        Token = new TokenLeft(Index + 1);

                        break;

                    case "right":

                        Token = new TokenRight(Index + 1);

                        break;

                    case "string":

                        Token = new TokenToString(Index + 1);

                        break;

                    case "round"://以下为数学运算记号对象

                        Token = new TokenRound(Index + 1);

                        break;

                    case "abs":

                        Token = new TokenAbs(Index + 1);

                        break;

                    case "max":

                        Token = new TokenMax(Index + 1);

                        break;

                    case "min":

                        Token = new TokenMin(Index + 1);

                        break;

                    case "sin":

                        Token = new TokenSin(Index + 1);

                        break;

                    case "cos":

                        Token = new TokenCos(Index + 1);

                        break;

                    case "mod":

                        Token = new TokenMod(Index + 1);

                        break;

                    case "pow":

                        Token = new TokenPow(Index + 1);

                        break;

                    case "and"://以下为逻辑运算记号对象

                        Token = new TokenAnd(Index + 1);

                        break;

                    case "or":

                        Token = new TokenOr(Index + 1);

                        break;

                    case "not":

                        Token = new TokenNot(Index + 1);

                        break;

                    case "xor":

                        Token = new TokenXor(Index + 1);

                        break;

                    case "pi"://以下为常量记号对象

                        Token = new TokenValue(Index + 1);

                        Token.TokenValueType = TokenValueTypeEnum.Number;

                        Token.TokenNumber = Math.PI;

                        break;

                    case "e":

                        Token = new TokenValue(Index + 1);

                        Token.TokenValueType = TokenValueTypeEnum.Number;

                        Token.TokenNumber = Math.E;

                        break;

                    case "true":

                        Token = new TokenValue(Index + 1);

                        Token.TokenValueType = TokenValueTypeEnum.Number;

                        Token.TokenNumber = 1;

                        break;

                    case "false":

                        Token = new TokenValue(Index + 1);

                        Token.TokenValueType = TokenValueTypeEnum.Number;

                        Token.TokenNumber = 0;

                        break;

                    default:

                        throw new Exception(string.Format("语法错误,列{0},未知表达式:{1}。", Convert.ToString(Index + 1), TokenWord));

                }//switch

            }//if

            else

            {

                //错误字符串,抛出错误,语法错误

                throw new Exception(string.Format("语法错误,列{0},未知表达式:{1}。", Convert.ToString(Index + 1), TokenWord));

            }



            return Token;

        }//ProduceToken

 

改进后的代码为:

 

        /// <summary>

        /// 产生记号对象

        /// </summary>

        /// <param name="TokenWord">分析得到的单词</param>

        /// <param name="Index">当前序号</param>

        /// <returns>记号对象</returns>

        /// <remarks>Author:Alex Leo @ ConExpress</remarks>

        protected static new TokenRecord ProduceToken(string TokenWord, int Index)

        {

            TokenRecord Token;



            if (GetKeywordDictionary().ContainsKey(TokenWord.ToLower()))

            {

                string strFullClassName = "ConExpress.Calculator." + GetKeywordDictionary()[TokenWord.ToLower()];

                Type TokenType = Type.GetType(strFullClassName);

                Token = (TokenRecord)Activator.CreateInstance(TokenType, new object[] { Index + 1 });

                //对常数的特殊处理

                switch (TokenWord.ToLower())

                {

                    case "pi"://以下为常量记号对象

                        Token.TokenValueType = TokenValueTypeEnum.Number;

                        Token.TokenNumber = Math.PI;

                        break;

                    case "e":

                        Token.TokenValueType = TokenValueTypeEnum.Number;

                        Token.TokenNumber = Math.E;

                        break;

                    case "true":

                        Token.TokenValueType = TokenValueTypeEnum.Number;

                        Token.TokenNumber = 1;

                        break;

                    case "false":

                        Token.TokenValueType = TokenValueTypeEnum.Number;

                        Token.TokenNumber = 0;

                        break;

                    default:

                        break;

                }

            }

            else

            {

                //错误字符串,抛出错误,语法错误

                throw new Exception(string.Format("语法错误,列{0},未知表达式:{1}。", Convert.ToString(Index + 1), TokenWord));

            }



            return Token;

        }//ProduceToken

 

通过这个改进可以很明显的看到,改进后的代码行数减少很多,而且还可以避免新增关键字时忘记注册case

TokenSymbolFactory的修改和TokenKeywordFactory的修改类似,这里就不贴出详细代码了,具体的改动在程序里都会保留。

 

最近又对程序进行了一些改进,添加了一个SyntaxException类,用于发生错误时给调用程序提供诸如错误信息,错误操作符起始位置,错误操作符长度等,这样就可以在界面上选中发生错误的操作符了,更加人性化。为了配合SyntaxException类,对程序中的其他相关之处也进行了修改,比如TokenRecord的构造函数添加了一个参数,这样就导致其他构造都需要发生变化,但并不影响理解。

对主界面也进行了完善,可以选择多行执行还是单行执行。选择单行执行时,输入完成后直接回车即可进行计算,并且会把输入框中的代码合并成一行进行计算。如果选择多行,则将每一行进行单独的计算,计算结果在输出框中按多行显示,此时回车为换行操作,需要手动点击计算按钮。而且,这里模仿SQL Server,可以选中要计算的表达式,对部分代码进行计算。

此外还添加了一些TokenRecord,主要是三角函数,其实就是对System.Math中的一些方法的封装。如果需要也可以添加更多的方法,甚至把System.Math中的所有方法都实现。

最近在看委托,觉得可以用委托进一步重构。因为很多TokenRecord代码都一样,只不过实际的算法不同,差异只有一行而已。而采用委托就可以将算法动态改变,似乎是一个更理想的方法。

 

所有分析就到这里了,这只是一个练习的小程序,并没有做漂亮的外观和强大的功能。如果需要添加其他算法,只需要参考响应的算法,从TokenRecord及其子类继承即可。如果发现程序中有什么问题,也欢迎指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值