【重构学习】07 数据的重构

这里的数据指的大概就是字段(貌似这章有些东西也是过时了,因为现在的.NET已经发展了很久了,包括Java也是)

1、自封装字段(其实就是属性啦,过时了)

修改点:直接访问字段,但是与字段间的耦合关系逐渐变得笨拙

做法:为这个字段建立一个取值/设值函数,并且只以这些函数来访问数据

好吧,现在明白属性是怎么来的了吗,就是因为这个原因,所以有的人会干脆和你说,你不要写公共字段,直接写公共属性。

因为公共字段能做的公共属性都能做,不能做的公共属性也能做。

public string 我叫公共属性 { get; set; }

多写个这东西好用多了,以后万一有个什么反射的需要,也简单多了,举手之劳,所以忘记还有公共字段这回事吧。

2、以对象取代字段

修改点:你有一个字段,需要与其它数据和行为一起使用才有意义

做法:用对象取代字段

简单地说,你玩过DateTime这个类吗,就是系统的类,其实就是对数据的一个封装啊。

如果没有这个东西,那么你是不是要用年,月,日(就算没有时分秒吧),三个字段来表示。搞的多了自然就像自己封装一个Date类,哪天又多了个按格式输出的要求,是不是要再写个格式化函数

所以那么就把这些好基友都放在一起好了,于是DateTime就成了基友之家。(然而我们需要更多的DateTime样的基友之家)

3、将值对象改为引用对象

修改点:你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象

做法:将这个值对象变为引用对象

简单地说,这里所谓的值对象是指DateTime这种对象,即可以建立很多次,纯数据的传递,而所谓的引用对象是指缓存的东西,比如淘宝界面的分类数据,因为所有人看到的分类都是一样的,那么我们不需要每次都从数据库取值

缓存一下就好,不需要每次New一个分类对象,然后调用查询函数,而是去找缓存去看是否有了缓存,有了缓存,那么就调用,没有缓存那么就再从数据库里面取值。只要缓存一更改,大家看到的分类就都更改了。

好了,就是这么个意思

其中为了适应也许引用对象会根据不同的类型创建一个不同的对象,(就像有个墙类,上节课Troy教小朋友们如何搬砖砌墙,所以新建正常的墙的对象,这节课话题换了,Troy教小朋友们码长城,于是就新建一个长城对象)

当涉及到切换子类的时候,可以用这种方法使得客户端调用的时候不需要知道还有长城这个类,只需要用墙类里的工厂函数就好了,当然也许你下次还得搞个扛水泥去搞个水泥墙的类,那么在 工厂函数里再加就好了。

所以提到的一个重构方法:

将构造函数改为调用工厂函数,即

 public class Wall{
        public Wall(string length){
            //啦啦啦
        }
   }

改为

public class Wall{
        public static Wall Create(string type){
            if (type=="Great"){
                return new GreatWall("一万里");
            }
            else{
                return new Wall("一米");
            }
        }
        protected Wall(string length)
        {
            //啦啦啦
        }
    }
    public class GreatWall:Wall
    {
        public GreatWall(string length):base(length)
        {
            //啦啦啦
        }
    }

这种方法的用处在于客户调用不需要去知道子类,只需要知道type就行了,当然具体情况具体对待,你也可以不用这么写

然后这个所谓的引用变量的稳妥一点的做法还是要判断一下这个对象存不存在,不存在就去取值,存在就直接调用。

4、将引用对象改为值对象

  是的,你没有看错,当你千辛万苦改成了值对象之后,有个需求第二天可能又会让你改回来。。

  动机:因为你的引用对象很小且不可变,而且不易管理。

  不可变是什么鬼?我看了《重构》这个看了半天,最后才弄明白。

  还是用分类缓存来说,淘宝的缓存就是可变的,因为淘宝的缓存假如作为商家的你可以自己去去增加分类,然后刷新它,那么所有的客户得到的分类都变了。

  那么不可变是就是:

  

  我在本地把百度首页改成这样,但是你们这些人并不知道我已经在用谷歌了╮(╯▽╰)╭

  换句话说,有天,马老板说咱淘宝分类就这么搞了,无论怎样就这么多分类了,不许商家去增加分类,那么,这个缓存就是不可变的了,某天他说不要那么多分类了,就假货和真货两个类的时候,那么引用对象足够小了

  那么这个引用对象也就不需要了,改成值对象就好了,比如像百度一下直接作为文本,写死在页面上了。

  好吧,我就是这么理解的。

  正经话:引用对象就是客户公用的,当我去改动后,其他客户看到的也就变了,而值对象不是,当我改动后只有我自己这边变了。(懂了吗,我觉得自己已经讲得很清楚了)

5、以对象取代数组

  修改点:你有一个数组,其中的元素代表不同的东西

  做法:以对象替换数组,对于数组中的每个东西,以一个字段来表示。

  换句话说,有个混蛋把DateTime的年月日写在一个int date[3];这样的东西里面,其中date[0]代表年,date[1]代表月,你是否想要打死他?

 6、复制"被监视数据"(大白话:别把数据放在表现层)

  修改点:你又一些领域数据置身于GUI控件中,而领域函数需要访问数据。

  做法:将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。

  一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。

  动机:(1)你可能需要使用不同的用户界面来表现相同的业务逻辑,如果同时承担两种责任,用户界面会变得过分复杂;(手机端微信和PC端微信)

     (2)与GUI隔离之后,领域对象的维护和演化都会很容易,你甚至可以让不同的开发者负责不同部分的开发。

  作为WEB开发者,MVC都知道吧,所以这里讲得就是业务逻辑别写在表现层,当然也包括数据。

  做法:

  话说这小节的做法真的超长,一大堆java代码,我最终还是没敢看。

  由于我在客户端程序这方面基本上就只写过一些小程序,总是业务逻辑和表现层混在一起,所以我就写个自己经常写的垃圾代码改一下好了。

  

  功能:

  每个按钮点一次讲一句话

  好了,下面贴上这段典型的混合果汁代码:

  

private void btnWangyi_Click(object sender, EventArgs e)
        {
            txtYongBoy.Text += getNewLineString("网易:")+ getNewLineString("我是一个有态度的媒体,我每天引诱一堆人互喷赚流量");
        }

        private void BtnWumao_Click(object sender, EventArgs e)
        {
            txtYongBoy.Text += getNewLineString("五毛(一群整天阴谋论的人):") + getNewLineString("美狗SB,你们这些美分就是成天造谣,臭傻叉,汪汪汪");
        }

        private void btnMeifen_Click(object sender, EventArgs e)
        {
            txtYongBoy.Text +=  getNewLineString("美分(一群整天抱怨国家社会父母,自己又没能力的low逼):")+  getNewLineString("五毛SB,只有我清醒着,你们这群被洗脑的人,我最清醒,我为什么没被洗脑,因为汪汪汪");
        }

        private void btnZhengfu_Click(object sender, EventArgs e)
        {
            txtYongBoy.Text +=  getNewLineString("政府(话说真的想看,网络对它不存在匿名,程序员一句代码的事,太可怕了):") + getNewLineString("我就看看,不说话");
        }

        private void btnSendFuck_Click(object sender, EventArgs e)
        {
            txtYongBoy.Text += getNewLineString("Troy:")+ getNewLineString("好好工作,好好赚钱,有能力不喜欢这里分分钟移民,没能力出去也是low,要不你去个唐人街去旅游看看,lol");
        }
        private string getNewLineString(string str){
            return str + Environment.NewLine + Environment.NewLine;
        }

好了,按照作者的话来讲,应该是改成接下来这种样子:

public partial class MainForm : Form
    {
        private Speak mySpeak;
        public MainForm()
        {
            InitializeComponent();
            mySpeak = new Speak(this);
        }

        private void btnWangyi_Click(object sender, EventArgs e)
        {
            mySpeak.SendMySound("网易:", "我是一个有态度的媒体,我每天引诱一堆人互喷赚流量");
        }

        private void BtnWumao_Click(object sender, EventArgs e)
        {
            mySpeak.SendMySound("五毛(一群整天阴谋论的人):", "美狗SB,你们这些美分就是成天造谣,臭傻叉,汪汪汪");
        }

        private void btnMeifen_Click(object sender, EventArgs e)
        {
            mySpeak.SendMySound("美分(一群整天抱怨国家社会父母,自己又没能力的low逼):", "五毛SB,只有我清醒着,你们这群被洗脑的人,我最清醒,我为什么没被洗脑,因为汪汪汪");
        }

        private void btnZhengfu_Click(object sender, EventArgs e)
        {
            mySpeak.SendMySound("政府(话说真的想看,网络对它不存在匿名,程序员一句代码的事,太可怕了):", "我就看看,不说话");
        }

        private void btnSendFuck_Click(object sender, EventArgs e)
        {
            mySpeak.SendMySound("Troy:", "好好工作,好好赚钱,有能力不喜欢这里分分钟移民,没能力出去也是low,要不你去个唐人街去旅游看看,lol");
        }
        public void UpdateSound(string text){
            txtYongBoy.Text=text;
        }
    }

    public class Speak {
        private string Alltext;

        private MainForm speakForm;

        public Speak(MainForm form) {
            speakForm = form;
        }

        private void updateSounds(){
            speakForm.UpdateSound(Alltext);
        }

        public void SendMySound(string speaker,string text){
            Alltext += getNewLineString(speaker) + getNewLineString(text);
            this.updateSounds();
        }

        private string getNewLineString(string str)
        {
            return str + Environment.NewLine + Environment.NewLine;
        }
    }

好吧,虽然上面代码很渣,但是应该足够清晰向你表示什么叫分离数据到业务层了。

如果你又兴趣可以改写我上面的渣代码,比如给MainForm继承某个接口,然后Speak类里的speakForm是这个接口而不是MainForm你觉得是不是好看多了,如果你继续改下去,那么你就会玩观察者模式了

7、单向关联改为双向关联

修改点:两个类都需要使用对方特性,但其间只有一条单向连接。

做法:添加一个反向指针,并使修改函数能够同时更新两条连接。

可以参考我上面的例子,MainForm和Speak就是双向关联的两个类,你可能不知道指针是什么,你就理解为引用就行了。(或者自己去看一下C语言)

8、双向关联改为单向关联

修改点:两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。

做法:去除不必要的关联

这句话是我自己的见解,或许是错误的,你自己决定是否这么玩:我不太赞成双向关联,单向关联就好了,我在A类要用B类,我自己在A类建个函数,接收B类的对象,到时候调用B类的对象的方法就好。

大家各玩各的,分开好一点,不要搅到一起。所以如果是我写正式的代码,我会尽量把所有的双向关系全换成单向的,除非我自己偷懒(我经常偷懒⊙﹏⊙‖∣,通常这意味着我或者将来的某人要为此付出代价(^o^)/),好吧,仁者见仁,智者见智,也许我是错的,仅仅我自己觉得双向关系对我来说很不清晰,会造成耦合。

在这里Martin也说了,双向关联很有用,但是你必须为它付出代价,那就是维护双向连接、确保对象被正确创建和删除而增加的复杂度。

9、以字面常量来取代魔法数字

修改点:你有一个字面数值,带有特别含义

做法:创造一个常量,根据其意义为它命名,并将上述的字面数值替换成这个常量。

举个简单例子:

    static void Main(string[] args)
        {
            Console.Write("请问一天有多少个小时:");
            if (24 != Console.Read())
            {
                Console.WriteLine("你说什么就是什么咯!");
            }
            else {
                Console.WriteLine("答对了");
            }
        }  

一般大家

const int HourNumOfOneDay = 24;

            Console.Write("请问一天有多少个小时:");
            if (HourNumOfOneDay != Console.Read())
            {
                Console.WriteLine("你说什么就是什么咯!");
            }
            else {
                Console.WriteLine("答对了");
            }

好吧,这里怎么写都一样,其实很好理解,那么你自己想象一下现在你是在一个业务很复杂的系统中,突然出现了一个21.23这样诡异的数字,然后与某个变量做比较, 你可能就会觉得这段代码理解起来有点吃力了,如果出现一大堆,也许你就要花很长时间去研究业务了。

10、封装字段好吧,已经过时了,就是属性

修改点:你的类中存在一个public字段

做法:将它声明为private,并提供相应的访问函数

11、封装集合

修改点:有个函数返回一个集合

做法:将这个函数返回该集合的一个只读副本,并在这个类中提供添加.移除集合元素的函数

因为现在给你的集合是个引用,所以你可以修改这个集合,而这个集合的拥有者一无所知。

好吧,在我的理解力,他的作用大概就是和封装字段一样的性质,为了数据隐藏,并且让数据和行为保持一致性

并且保持数据严谨性和安全性(好吧,反正我感觉自己应该不会用到这项重构,这个重构的作用应该是为了防止一些非正常的编码行为)。

那么我多句嘴吧:别用数组,用list和dictionary这种集合就好了(我也不知道多久没用过数组了 )。

12、以数据类取代记录

修改点:你需要面对传统编程环境中的记录结构

做法:为该记录创建一个“哑”数据对象

好吧,这个东西你可以理解为,如何把一堆数组转化为类对象,举个例子:沟通数据库的ORM

13、以类取代类型码

修改点:类中有一个数值类型码,但它并不影响类的行为

做法:以一个类替换该数值类型码

好吧,在.NET里你用enum枚举就好了,非常简洁易用。

14、以子类取代类型码

修改点:你有一个不可变的类型码,它会影响类的行为

做法:以子类去取代这个类型码

动机:它把对不同行为的了解,从类用户那儿转移到了类本身。如果加入新的行为变化,只需要添加一个子类就好了,如果不用多态机制来处理,那么你就要找到每一个条件表达式,然后逐一修改。

这种做法就是为了消除switch和if这样的分支语句,所以用子类继承父类的多态来处理,如果没有出现条件表达式那么就用第13个重构行为就好了。

好吧,你也许仔细想一下switch还是没有消除。哈哈,当初我纠结了好久。其实还是很简单,switch当然不会消除,但是它在客户类调用的时候已经看不到了啊,客户不需要知道内部逻辑,也就是说你写的代码,上一级调用起来会更简单。

更重要的是,你可能通过这种方式就只需要一次switch语句了,因为你只需要知道创建哪个类的对象就好了

说了这么多,如果你还是不懂那么就来看下面这个简单例子吧。

class Program
    {
        static void Main(string[] args)
        {
            Console.Write("请输入您要释放技能的神奇宝贝:");
            string name=Console.ReadLine();
            new 神奇宝贝().释放技能(name);
            new 神奇宝贝().叫一声(name);
        }
    }
    public class 神奇宝贝 {
        public 神奇宝贝() { 
            
        }
        public void 释放技能(string name){
            switch(name){
                case "皮卡丘":
                    Console.WriteLine("您的精灵释放十万伏特");
                    break;
                case "杰尼龟":
                    Console.WriteLine("您的精灵释放水枪");
                    break;
                default:
                    Console.WriteLine("小火龙释放火球");
                    break;
            }
        }
        public void 叫一声(string name)
        {
            switch (name)
            {
                case "皮卡丘":
                    Console.WriteLine("皮卡");
                    break;
                case "杰尼龟":
                    Console.WriteLine("杰尼");
                    break;
                default:
                    Console.WriteLine("呵呵哒");
                    break;
            }
        }
    }
}

修改后:

class Program
    {
        static void Main(string[] args)
        {
            Console.Write("请输入您要释放技能的神奇宝贝:");
            string name=Console.ReadLine();
            var 我的神奇宝贝 = 神奇宝贝.Create(name);
            我的神奇宝贝.释放技能();
            我的神奇宝贝.叫一声();
        }
    }
    public abstract class 神奇宝贝 {
        public static 神奇宝贝 Create(string name) {
            switch (name) {
                case "皮卡丘":
                    return new 皮卡丘();
                case "杰尼龟":
                    return new 杰尼龟();
                default:
                    return new 小火龙();
            }
        
        }
        public abstract void 释放技能();
        public abstract void 叫一声();
    }

    public class 皮卡丘 : 神奇宝贝 {
        public 皮卡丘() { }
        public override void 叫一声()
        {
            Console.WriteLine("皮卡");  
        }
        public override void 释放技能()
        {
            Console.WriteLine("您的精灵释放十万伏特");
        }
    }
    public class 杰尼龟 : 神奇宝贝
    {
        public 杰尼龟() { }
        public override void 叫一声()
        {
            Console.WriteLine("杰尼");
        }
        public override void 释放技能()
        {
            Console.WriteLine("您的精灵释放水枪");
        }
    }
    public class 小火龙 : 神奇宝贝
    {
        public 小火龙() { }
        public override void 叫一声()
        {
            Console.WriteLine("呵呵哒");
        }
        public override void 释放技能()
        {
            Console.WriteLine("小火龙释放火球");
        }
    }

修改完毕,首先我们可以看到整个代码结构清晰了很多,其次,对于在Main函数里调用方法的你而言,你觉得你更喜欢哪种调用方法,最后想象一下宠物小精灵可是有五百多只,而且不只是叫一声和释放技能两个行为,你如果加分支,那就真是毁童年系列了

15、用策略模式或者状态模式取代类型码

类型码:为什么你们总是要取代我,我就这么不招人喜欢吗(#‵′)凸

  上面的两个模式都是23种设计模式之一。

  修改点:你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它

  做法:以状态对象取代类型码

  以下是两种你不能通过14来解决的情况:

  1.类型码的值在对象创建后发生了改变(皮卡丘在调用完释放技能后,自动修改类型码成了杰尼龟,然后再叫一声的情况、)

  2.由于某种原因,类型码宿主,已经有了子类(因为你是用子类集成来消灭分支,所以人家有了子类你肯定就不能这么玩了)

  一提起这两个模式就感觉好厉害的样子,根本就看不下去了。

  其实很简单啦,不要管这乱七八糟的东模式西模式的。

  现在的问题在于我没法用子类消灭分支了,所以解决问题。

  因为我不能继承现在的Current类,所以我新建一个type类(具体类名你自己取),然后给有几个类型码我就继承几个子类,看我不就能用继承的方式来消灭分支了吗

  那么能解决分支了,剩下的就是把Current类和type类关联起来咯,你在Current类里加一个type的字段myType,然后写一个设置type字段的函数,函数里面就用类型码来switch区分创建哪一个type类的子类,这样就OK了嘛。

  什么设计模式嘛,根本就不需要记住,是不是?

  临时百度一下就OK,拿这几套剑法套路用来装装B就好了,正式写代码不用想,需要什么就写什么好了,我管你什么套路,一板砖能解决那扔过去就好了。

  作为一个剑客,目的就是砍倒人,你真的这么在意用什么剑法吗?

16、以字段取代子类

  修改点:你的各个子类的唯一差别只在“返回常量数据”的函数上

  做法:修改这些函数,使它们返回父类的某个(新增)字段,然后销毁函数。

  动机:建立子类的目的是为了增加新特性或变化和行为。有一种变化行为被称为常量函数,它们返回一个硬编码的值。

  这个东西有其价值,但是如果子类里面仅仅只有这些常量函数的话,就没有足够的存在价值了,你可以在父类中加一个与常量函数相对应的字段,从而完全去除这样的子类,如此一来就可以避免因继承而带来的额外复杂性。

原文地址:https://www.cnblogs.com/vvjiang/p/5104251.html