記一次.NET代碼重構


    好久沒寫代碼了,終於好不容易接到了開發任務,一看時間還挺充足的,我就慢慢整吧,若是遇上趕進度,基本上直接是功能優先,完全不考慮設計。你可以認為我完全沒有追求,當身后有鞭子使勁趕的時候,神馬設計都是浮雲,按時上線才是王道,畢竟領導是不會關注過程和代碼質量的,領導只看結果,這也許就是我等天朝碼農的悲哀。

    需求:是這樣的,要開發一個短信發送的模板,不同客戶可能會使用不同的模板,而不同的客戶使用的變量參數也是不同的。之前為了應急,線上已經完成了一個短信模板發送短信的功能,短信模板表也創建了,而且在表中已經新增了一條記錄。我只需要做一個短信模板的增刪改查界面就可以了,看上去我的任務挺簡單的,老司機應該知道,接了個爛攤子。

    下圖所示是原來已經創建好了的表

   SQL創建腳本如下:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[MessageModule](
    [Id] [uniqueidentifier] NOT NULL,
    [Name] [nvarchar](50) NULL,
    [Type] [nvarchar](50) NULL,
    [TypeNo] [nvarchar](50) NULL,
    [Channel] [nvarchar](50) NULL,
    [Param] [nvarchar](50) NULL,
    [Content] [nvarchar](max) NULL,
    [CreatedBy] [uniqueidentifier] NULL,
    [CreatedOn] [datetime] NULL,
    [ModifiedBy] [uniqueidentifier] NULL,
    [ModifiedOn] [datetime] NULL,
    [IsDeleted] [bit] NULL,
    [TypeId] [uniqueidentifier] NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

    在這之前是已經開發了一個發送短信的API接口供客戶調用了的,也就是說調用方(客戶),不會修改代碼,只能我這邊來修改。雖然極不情願接做了一半的任務,但是沒辦法,不可能給你的開發任務都是從頭開始的。

實體類代碼如下:

     [Table("dbo.MessageModule")]
    public class MessageModule : DTO
    {
        public string Type { get; set; } //業務類型
        public string TypeNo { get; set; } //業務編號
        public string Channel { get; set; } //使用渠道
        public string Name { get; set; } //名稱模版
        public string Content { get; set; } //短信內容
    }

    DOT類:

    public class DTO
    {
        public virtual Guid Id { get; set; }
        public virtual DateTime? CreatedOn { get; set; }
        public virtual Guid? CreatedBy { get; set; }
        public virtual DateTime? ModifiedOn { get; set; }
        public virtual Guid? ModifiedBy { get; set; }
        public virtual bool IsDeleted { get; set; }
    }

    這是之前的代碼,業務實體類MessageModuleBusiness.cs代碼如下:

    public class MessageModuleBusiness : GenericRepository<Model.MessageModule>
    {
        private UnitOfWork.UnitOfWork unitOfWork = new UnitOfWork.UnitOfWork();

        #region old code
        /// <summary>
        /// 獲取模版內容
        /// </summary>
        /// <param name="crowd"></param>
        /// <returns></returns>
        public string GetContent(MessageContext messageContext)
        {
            string messageContent = "";
            string TypeCode = string.IsNullOrEmpty(messageContext.serviceCode) ? "001" : messageContext.serviceCode;
            try
            {
                var Module = unitOfWork.MessageModule.Get(c => c.Type == messageContext.channel && c.TypeNo == TypeCode).FirstOrDefault();
//Content的內容:【一應生活】您有一件單號為expressNumbers company,已到communityName收發室,請打開一應生活APP“收發室”獲取取件碼進行取件。點擊下載http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life
if (!string.IsNullOrEmpty(Module.Content)) { var content = Module.Content; content = content.Replace("company", messageContext.company); content = content.Replace("expressNumbers", messageContext.expressNumbers); content = content.Replace("communityName", messageContext.communityName); content = content.Replace("Id", messageContext.Id); content = content.Replace("receiveTime", messageContext.receiveTime); content = content.Replace("fetchCode", messageContext.fetchCode); messageContent = content; } return messageContent; } catch (Exception ex) { } return ""; } #endregion }

    MessageContext類,這個是客戶端傳輸過來調用的一個實體對象。對象里面存在許多類似於短信的動態標簽變量。

    public class MessageContext
    {
        /// <summary>
        /// 手機號碼
        /// </summary>
        public string phone { get; set; }
        /// <summary>
        /// 發送信息
        /// </summary>
        public string message { get; set; }
        /// <summary>
        /// 簽名
        /// </summary>
        public string sign { get; set; }
        /// <summary>
        /// 渠道
        /// </summary>
        public string channel { get; set; }
        /// <summary>
        /// 內容
        /// </summary>
        public string content { get; set; }
        /// <summary>
        /// 取件碼
        /// </summary>
        public string fetchCode { get; set; }
        /// <summary>
        /// 快遞公司
        /// </summary>
        public string company { get; set; }
        /// <summary>
        /// 快遞單號
        /// </summary>
        public string expressNumbers { get; set; }
        /// <summary>
        /// 社區名稱
        /// </summary>
        public string communityName { get; set; }
        /// <summary>
        /// 到件時間
        /// </summary>
        public string receiveTime { get; set; }
        /// <summary>
        /// 序號
        /// </summary>
        public string Id { get; set; }
        /// <summary>
        /// 業務代碼
        /// </summary>
        public string serviceCode { get; set; }
    }

    控制器方法externalMerchantSendMessage,這是供外部調用的

        /// <summary>
        /// 外部商戶發送信息
        /// </summary>
        /// <returns></returns>
        public ActionResult externalMerchantSendMessage(MessageContext param)
        {
            logger.Info("[externalMerchantSendMessage]param:" + param);
            bool isAuth = authModelBusiness.isAuth(param.channel, param.phone, param.sign);
            if (!isAuth)
            {
                return Json(new Result<string>()
                {
                    resultCode = ((int)ResultCode.NoPermission).ToString(),
                    resultMsg = "簽名或無權限訪問"
                }, JsonRequestBehavior.AllowGet);
            }
            var meaage = messageModuleBusiness.GetContent(param);

            if (string.IsNullOrEmpty(meaage))
            {
                return Json(new Result<string>()
                {
                    resultCode = ((int)ResultCode.failure).ToString(),
                    resultMsg = "發送失敗"
                }, JsonRequestBehavior.AllowGet);
            }

            SMSHelper helper = new SMSHelper();
            helper.SendSMS(meaage, param.phone);
            return Json(new Result<string>()
            {
                resultCode = ((int)ResultCode.success).ToString(),
                resultMsg = "發送成功"
            }, JsonRequestBehavior.AllowGet);
        }

    以上是我接收開發任務之前已經實現了的功能。看上去我的任務挺簡單的,可是多年的開發經驗告訴我,這里需要重構,如果我現在啥都不管,就只管做一個短信模板的增刪改查界面的話,后面維護的人一定會抓狂。

    看出什么問題沒有?

    這個接口方法externalMerchantSendMessage是給所有客戶調用,而不同客戶使用不同的短信模板,不同的模板,又存在不同的變量參數。而現在所有的變量參數都封裝在了類MessageContext中,問題是我們無法一下子把所有的變量參數全部確定下來,並保持不變。那么,也就是說一旦需要添加變量參數,類MessageContext中的代碼就必須修改,而且GetContent方法中的代碼是硬編的,一樣需要跟着修改。這樣就形成了一個循環,不斷加變量參數,不斷改代碼,不斷發布接口版本.......

    時間充裕的情況下,我自然是一個有節操的程序猿,那么就開始重構吧。

    在重構之前,在腦海浮現的並不是各種設計模式,而是面向對象設計的基本原則。各種設計模式就好比各種武學套路或者招式,習武之人應該像張無忌練習太極劍一樣,先學會各種套路,然后忘記所有套路,從而融會貫通。因為招式是死的,人是活得,有招就有破綻,根本沒有必勝招式存在,就好像沒有萬能的設計模式一樣,任何設計模式都存在缺點。

    面向對象設計的核心思想就是封裝變化,那么先找出變化點。從上面的分析中,我們已經發現了變化點,那就是短信模板中的變量參數,而這些變量參數都是客戶調用方傳過來的,不同客戶傳遞的參數變量又可能是不一樣的。我們先來看一下,客戶傳遞過來的是什么?我們看下客戶調用代碼,這里有Get和Post兩種調用方式。

        function sendMsg() {
            //var appParam ="phone=15914070649&sign=78a7ce797cf757916c2c7675b6865b54&channel=weijiakeji&content=&fetchCode=1
&company=%E9%A1%BA%E4%B8%B0%E5%BF%AB%E9%80%92&expressNumbers=123456&communityName=%E9%95%BF%E5%9F%8E%E4%B8%80%E8%8A%B1%E5%9B%AD&receiveTime=5&Id=1231";
//Get("/Message/externalMerchantSendMessage?" + appParam, {}); var data = { "phone": "15914070649", "sign": "78a7ce797cf757916c2c7675b6865b54", "channel": "weijiakeji", "fetchCode": 1, "company": "%E9%A1%BA%E4%B8%B0%E5%BF%AB%E9%80%92", "Id": "1231" }; Post('/Message/externalMerchantSendMessage', data); }
//WebAPI Post方法 function Post(url, data) { $.ajax({ url: url, contentType:
"application/json", type: "POST", dataType: "json", async: true, cache: false, data: JSON.stringify(data), success: function (response) { $('#response').text(JSON.stringify(response)); }, error: function (XMLHttpRequest, textStatus, errorThrown) { alert(textStatus); } }); }; //// WebApi Get方法 function Get(url, data) { $.ajax({ url: url, contentType: "application/json", type: "GET", dataType: "json", async: true, cache: false, //data: JSON.stringify(data), success: function (response) { $('#response').text(JSON.stringify(response)); }, error: function (XMLHttpRequest, textStatus, errorThrown) { alert(textStatus); } }); };

    可見客戶傳遞的是一個鍵值對集合,就是一個JSON格式的對象。根據前面的代碼 bool isAuth = authModelBusiness.isAuth(param.channel, param.phone, param.sign);,可以分析出有三個參數是所有調用客戶都必須傳遞過來的,那就是:channel,phone,sign,而其它的參數就是短信模板的變量參數和參數值。那么方法externalMerchantSendMessage(MessageContext param)中的參數就是一個可變對象。在C#4.0種存在一個dynamic不正是用來描述可變對象嗎?

那么第一步修改傳入參數類型,之前是硬編碼的強類型MessageContext,現在不依賴此類,而是動態解析,修改externalMerchantSendMessage方法代碼如下:

                dynamic param = null;
                string json = Request.QueryString.ToString();

                if (Request.QueryString.Count != 0) //ajax get請求
                {
                    //兼容舊的客戶調用寫法,暫時硬編了
                    if (json.Contains("param."))
                    {
                        json = json.Replace("param.", "");
                    }
                    json = "{" + json.Replace("=", ":'").Replace("&", "',") + "'}";
                }
                else  //ajax Post請求
                {
                    Request.InputStream.Position = 0; //切記這里必須設置流的起始位置為0,否則無法讀取到數據
                    json = new StreamReader(Request.InputStream).ReadToEnd();
                }
                var serializer = new JavaScriptSerializer();
                serializer.RegisterConverters(new[] { new DynamicJsonConverter() });
                param = serializer.Deserialize(json, typeof(object));

DynamicJsonConverter的作用是將JSON字符串轉為Object對象,代碼如下:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Dynamic;
using System.Linq;
using System.Text;
using System.Web.Script.Serialization;

public sealed class DynamicJsonConverter : JavaScriptConverter
{
    public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
    {
        if (dictionary == null)
            throw new ArgumentNullException("dictionary");

        return type == typeof(object) ? new DynamicJsonObject(dictionary) : null;
    }

    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override IEnumerable<Type> SupportedTypes
    {
        get { return new ReadOnlyCollection<Type>(new List<Type>(new[] { typeof(object) })); }
    }

    #region Nested type: DynamicJsonObject

    private sealed class DynamicJsonObject : DynamicObject
    {
        private readonly IDictionary<string, object> _dictionary;

        public DynamicJsonObject(IDictionary<string, object> dictionary)
        {
            if (dictionary == null)
                throw new ArgumentNullException("dictionary");
            _dictionary = dictionary;
        }

        public override string ToString()
        {
            var sb = new StringBuilder("{");
            ToString(sb);
            return sb.ToString();
        }

        private void ToString(StringBuilder sb)
        {
            var firstInDictionary = true;
            foreach (var pair in _dictionary)
            {
                if (!firstInDictionary)
                    sb.Append(",");
                firstInDictionary = false;
                var value = pair.Value;
                var name = pair.Key;
                if (value is string)
                {
                    sb.AppendFormat("{0}:\"{1}\"", name, value);
                }
                else if (value is IDictionary<string, object>)
                {
                    new DynamicJsonObject((IDictionary<string, object>)value).ToString(sb);
                }
                else if (value is ArrayList)
                {
                    sb.Append(name + ":[");
                    var firstInArray = true;
                    foreach (var arrayValue in (ArrayList)value)
                    {
                        if (!firstInArray)
                            sb.Append(",");
                        firstInArray = false;
                        if (arrayValue is IDictionary<string, object>)
                            new DynamicJsonObject((IDictionary<string, object>)arrayValue).ToString(sb);
                        else if (arrayValue is string)
                            sb.AppendFormat("\"{0}\"", arrayValue);
                        else
                            sb.AppendFormat("{0}", arrayValue);

                    }
                    sb.Append("]");
                }
                else
                {
                    sb.AppendFormat("{0}:{1}", name, value);
                }
            }
            sb.Append("}");
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (!_dictionary.TryGetValue(binder.Name, out result))
            {
                // return null to avoid exception.  caller can check for null this way...  
                result = null;
                return true;
            }

            var dictionary = result as IDictionary<string, object>;
            if (dictionary != null)
            {
                result = new DynamicJsonObject(dictionary);
                return true;
            }

            var arrayList = result as ArrayList;
            if (arrayList != null && arrayList.Count > 0)
            {
                if (arrayList[0] is IDictionary<string, object>)
                    result = new List<object>(arrayList.Cast<IDictionary<string, object>>().Select(x => new DynamicJsonObject(x)));
                else
                    result = new List<object>(arrayList.Cast<object>());
            }

            return true;
        }
    }

    #endregion
}
View Code

接下來是GetContent方法,此方法的目的很簡單,就是要根據客戶傳遞的模板變量參數鍵值對和短信模板內容,拼裝成最后的短信發送內容,之前此方法里面是硬編碼的,現在我們需要變成動態獲取。

短信模板的內容示例:

【一應生活】您有一件單號為expressNumbers company,已到communityName收發室,請打開一應生活APP“收發室”獲取取件碼進行取件。點擊下載http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life

我發現這樣的模板內容有問題,模板中的變量參數是直接用的英文單詞表示的,而我們的短信內容中可能有時候也會存在英文單詞,那么我就給所有的變量參數加上{}。修改后如下:

【一應生活】您有一件單號為{expressNumbers} {company},已到{communityName}收發室,請打開一應生活APP“收發室”獲取取件碼進行取件。點擊下載http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life

我們需要根據客戶傳遞過來的對象,將短信模板中的變量參數,替換成變量參數對應的值。那么我們首先就要解析這個對象中的鍵值對信息。

        /// <summary>
        /// 把object對象的屬性反射獲取到字典列表中
        /// </summary>
        /// <param name="data">object對象</param>
        /// <returns>返回Dictionary(屬性名,屬性值)列表</returns>
         static Dictionary<string, string> GetProperties(object data)
        {
            Dictionary<string, string> dict = new Dictionary<string, string>();

            Type type = data.GetType();
            string[] propertyNames = type.GetProperties().Select(p => p.Name).ToArray();
            foreach (var prop in propertyNames)
            {
                object propValue = type.GetProperty(prop).GetValue(data, null);
                string value = (propValue != null) ? propValue.ToString() : "";
                if (!dict.ContainsKey(prop))
                {
                    dict.Add(prop, value);
                }
            }
            return dict;
        }

接下來是通過正則表達式來匹配短信模板內容。

        /// <summary>
        /// 多個匹配內容
        /// </summary>
        /// <param name="sInput">輸入內容</param>
        /// <param name="sRegex">表達式字符串</param>
        /// <param name="sGroupName">分組名, ""代表不分組</param>
        static List<string> GetList(string sInput, string sRegex, string sGroupName)
        {
            List<string> list = new List<string>();
            Regex re = new Regex(sRegex, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline);
            MatchCollection mcs = re.Matches(sInput);
            foreach (Match mc in mcs)
            {
                if (sGroupName != "")
                {
                    list.Add(mc.Groups[sGroupName].Value);
                }
                else
                {
                    list.Add(mc.Value);
                }
            }
            return list;
        }
        public static string ReplaceTemplate(string template, object data)
        {
            var regex = @"\{(?<name>.*?)\}";
            List<string> itemList = GetList(template, regex, "name"); //獲取模板變量對象

            Dictionary<string, string> dict = GetProperties(data);
            foreach (string item in itemList)
            {
                //如果屬性存在,則替換模板,並修改模板值
                if (dict.ContainsKey(item))
                {
                    template = template.Replace("{"+item+"}", dict.First(x => x.Key == item).Value);
                }
            }

            return template;
        }

這樣就講客戶傳遞的對象和我們的解析代碼進行了解耦,客戶傳遞的對象不再依賴於我們的代碼實現,而是依賴於我們數據表中模板內容的配置。

這幾個方法我是寫好了,順便弄個單元測試來驗證一下是不是我要的效果,可憐的是,這個項目中根本就沒用到單元測試,沒辦法,我自己創建一個單元測試

    [TestClass]
    public class MatchHelperTest
    {
        [TestMethod]
        public void ReplaceTemplate()
        {
            //模板文本
            var template = "【一應生活】您有一件單號為{expressNumbers} {company},已到{communityName}收發室,請打開一應生活APP“收發室”獲取取件碼進行取件。點擊下載http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life";
            //數據對象
            var data = new { expressNumbers = "2016", company = "長城", communityName = "長怡花園"};
            string str = "【一應生活】您有一件單號為2016 長城,已到長怡花園收發室,請打開一應生活APP“收發室”獲取取件碼進行取件。點擊下載http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life";
            string str1=MatchHelper.ReplaceTemplate(template, data);

            Assert.AreEqual(str1,str);

            //重復標簽的測試
            template = "【一應生活】您有一件單號為{expressNumbers} {company},已到{communityName}收發室,單號:{expressNumbers}";
            str = "【一應生活】您有一件單號為2016 長城,已到長怡花園收發室,單號:2016";
            str1=MatchHelper.ReplaceTemplate(template, data);
            Assert.AreEqual(str1, str);
        }
    }

說到單元測試,我相信在許多公司都沒有用起來,理由太多。我也覺得如果業務簡單的話,根本沒必要寫單元測試,國內太多創業型公司項目進度都非常趕,如果說寫單元測試不費時間,那絕對是騙人的,至於說寫單元測試能提高開發效率,減少返工率,個人感覺這個還真難說,因為即便不寫單元測試也還是可以通過許多其它手段來彌補的,個人觀點,勿噴。

接下來修改GetContent方法如下:

        public string GetContent(dynamic messageContext)
        {
            string strMsg = "";
            string TypeCode = string.IsNullOrEmpty(messageContext.serviceCode) ? "001" : messageContext.serviceCode;
            string channel = messageContext.channel;
            try
            {
                var Module = unitOfWork.MessageModule.Get(c => c.Type == channel && c.TypeNo == TypeCode).FirstOrDefault();
                if (!string.IsNullOrEmpty(Module.Content))
                {
                    var content = Module.Content;
                    strMsg = MatchHelper.ReplaceTemplate(content, messageContext);
                }

                return strMsg;
            }
            catch (Exception ex)
            {
                strMsg = ex.Message;
            }
            return strMsg;
        } 

(話外:先吐槽一下之前這個變量命名,MessageContext messageContext 和string messageContent,長得太像了,一開始我重構的時候害我弄錯了,建議不要在同一個方法中使用相似的變量名稱,以免弄混淆。媽蛋,老司機的我又被坑了,憤怒,無可忍受,果斷重命名。)

原來控制器調用業務邏輯代碼是直接這樣的

MessageModuleBusiness messageModuleBusiness = new MessageModuleBusiness()

依賴於具體類的實現,而我們知道,具體是不穩定的,抽象才是穩定的,我們應該面向接口編程。今天是發送短信,明天可能就是發郵件,又或者要加日志記錄等等等。

    public interface IMessageModuleBusiness
    {
        /// <summary>
        /// 組裝消息內容
        /// </summary>
        /// <param name="messageContext">動態參數對象</param>
        /// <returns>組裝后的消息內容</returns>
        string GetContent(dynamic messageContext);
    }

然后調用的代碼修改為:

   private IMessageModuleBusiness messageModuleBusiness = new MessageModuleBusiness();

最終的externalMerchantSendMessage代碼為:

        /// <summary>
        /// 外部商戶發送信息
        /// </summary>
        /// <returns></returns>
        public ActionResult externalMerchantSendMessage()
        {
            try
            {
                dynamic param = null;
                string json = Request.QueryString.ToString();

                if (Request.QueryString.Count != 0) //ajax get請求
                {
                    //兼容舊的客戶調用寫法,暫時硬編了
                    if (json.Contains("param."))
                    {
                        json = json.Replace("param.", "");
                    }
                    json = "{" + json.Replace("=", ":'").Replace("&", "',") + "'}";
                }
                else  //ajax Post請求
                {
                    Request.InputStream.Position = 0;//切記這里必須設置流的起始位置為0,否則無法讀取到數據
                    json = new StreamReader(Request.InputStream).ReadToEnd();
                }
                var serializer = new JavaScriptSerializer();
                serializer.RegisterConverters(new[] { new DynamicJsonConverter() });
                param = serializer.Deserialize(json, typeof(object));

                logger.Info("[externalMerchantSendMessage]param:" + param);
                bool isAuth = authModelBusiness.isAuth(param.channel, param.phone, param.sign);
                if (!isAuth)
                {
                    return Json(new Result<string>()
                    {
                        resultCode = ((int)ResultCode.NoPermission).ToString(),
                        resultMsg = "簽名或無權限訪問"
                    }, JsonRequestBehavior.AllowGet);
                }

                var meaage = messageModuleBusiness.GetContent(param);

                if (string.IsNullOrEmpty(meaage))
                {
                    return Json(new Result<string>()
                    {
                        resultCode = ((int)ResultCode.failure).ToString(),
                        resultMsg = "發送失敗"
                    }, JsonRequestBehavior.AllowGet);
                }

                SMSHelper helper = new SMSHelper();
                helper.SendSMS(meaage, param.phone); //發送短信
                return Json(new Result<string>()
                {
                    resultCode = ((int)ResultCode.success).ToString(),
                    resultMsg = "發送成功"
                }, JsonRequestBehavior.AllowGet);
            }
            catch (Exception ex)
            {
                return Json(new Result<string>()
                {
                    resultCode = ((int)ResultCode.failure).ToString(),
                    resultMsg = "發送失敗"+ex.Message
                }, JsonRequestBehavior.AllowGet);
            }
        }

這樣的話,即便日后通過反射或者IOC來再次解耦也方便。

好了,通過這樣一步一步的重構,在不修改原有表結構和不影響客戶調用的情況下,我已經將變化點進行了封裝,當客戶的模板參數變量變化的時候,再也不需要變更代碼,只需要修改表中的模板內容就可以了。

 

重構時,畫類圖是一個非常好的習慣,代碼結構一目了然,這里我附上類圖。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM