代码交叉拷贝悖论

一直以来写代码会把自己逼到某种非常难受的死角,明明是非常类似的代码,却不能方便的剪切粘贴,也无法用重构代码的方式合理的解决问题。就是他恰好类似于数据库交叉表那种情况,好像冥冥中指定了他的复杂度就是n平方。。。

从自己写的一个scroll bar的类里面找了一段代码出来。

m_thumbCap->SetVOffset(m_Height);
m_thumb->SetVOffset(m_Height);
m_thumbBottom->SetVOffset(m_Height);

m_thumbCap->SetRotation(270);
m_thumb->SetRotation(270);
m_thumbBottom->SetRotation(270);

滚动条的thumb由三块Image组件构成,cap,body和bottom。滚动条的图片默认是纵向的,横向滚动条是将图片横过来用的。这一段是将滚动条设置为横向的时候,分别将三块图片的垂直位置设置为属性里设置的高度,并将他们的图片旋转270度来横向使用。

我想说的是,这块代码的两组语句之间有着高度对称性,却难以利用这个对称性高效的开发出来,反而费时费力。。很纠结。不知大家遇到这种情况没有。

m_thumbCap->SetVOffset(m_Height);
m_thumbCap->SetRotation(270);
m_thumb->SetVOffset(m_Height);
m_thumb->SetRotation(270);
m_thumbBottom->SetVOffset(m_Height);
m_thumbBottom->SetRotation(270);

这个例子比较幸运,恰好修改一下分组方式修改起来会比较方便。甚至可以写个函数将这两句话包装起来。但有时遇到的情况就没这么方便了。


int coordinaryValue;
if(m_Orientation == ScrollBar::vertical){
coordinaryValue = value / (double)(m_Max-m_Min+m_PageSize) * m_Height;
m_thumbCap->SetVOffset(coordinaryValue);
}else if(m_Orientation == ScrollBar::horizontal){
coordinaryValue = value / (double)(m_Max-m_Min+m_PageSize) * m_Width;
m_thumbCap->SetHOffset(coordinaryValue);
}
setImagePos();

这段代码根据滚动条是纵向的还是横向的重新计算thumb块的位置并更新图形显示。横向和纵向的计算方式雷同却不相同,复制之后仍需修改几个地方,假如更复杂些的话就会更麻烦。并且这里甚至找不到很好的包装函数的方式。且不说由于整个项目只有这里用到了这一小块代码,封装函数可能对于开发效率也没啥提高。

啊啊貌似举了两个例子都不是很要命的那种,一时找不到非常典型的例子了。不过假如以前遇到同类问题的话,应该会有印象吧。也有可能所有类似问题都有解决办法的只是我有时困西西的编程没有仔细想吧。欢迎砸我~

19 comments on “代码交叉拷贝悖论

  • 现实世界肯定会有这样琐碎的时候了,复制过来再修改吧。Vim 就很擅长做这种小修改。

  • 是啊,我也常常为这种事情烦恼。不过采取的办法是:首先极力剥离为公共函数,然后相似部分作为参数;如果这个还不行的话,就可以考虑宏定义。。。不知道还有没有更好的办法啊。。。

    • 有想过包函数的问题。。这里恰好只有 水平-垂直 两种情况。。假如他有20种情况肯定就高高兴兴做成函数了。。只有两种情况的话。。写了别扭的函数然后只用了一次。。感觉很奇怪。。
      另一个典型的例子就是height-width还有 x-y。往往会用到类似的处理方法。。但是他只有两组,写函数又不甘心。。。

      • 其实我个人对于函数的理解并不是局限于为了在很多地方得到重用,然则函数是为了让功能得到独立使得程序不是耦合,并且让后面的看你代码的人能够一目了然,还不用写一些不必要的注释。当然这一切都是在函数不会太多的影响程序的执行效率的情况下。
        虽然过多的预测未来的需求是一种极端做法,但是假如后面有一些需求会用到跟多的类似的方法的时候,到时候这些元函数能够得到利用也会使得开发效率更高啊。

  • OwnWaterloo says:

    很有追求嘛。 大部分人在完成功能之后是不会继续考虑这些问题的, 你懂的。

    “由于整个项目只有这里用到了这一小块代码,封装函数可能对于开发效率也没啥提高。”
    —— 这个很赞同。

    虽然很多人(甚至一些大师)说函数不能超过xx行什么的。
    但是, “为了将少函数长度, 将一些snippet放入到另一个函数中, 并且也仅有这一个调用点”
    —— 这样的代码我感觉读起来更头痛。

    不知道是不是想要这样的效果? 伪代码大致是这样:
    for image in [ thumb, caption, buttom /* 这里还可以更多 */ ] :
    image.offset(height)
    image.rotation(270)

    • R: “但是, “为了将少函数长度, 将一些snippet放入到另一个函数中, 并且也仅有这一个调用点”
      —— 这样的代码我感觉读起来更头痛”。

      我觉得这也未必吧,到时候看代码的时候就像浏览一个一个的功能,阅读代码更像是在了解流程一样,在了解了这些流程之后再去看有问题的地方我觉得效率会更高。如果让代码散落一地,然后用各种注释来导航更让人痛苦啊。。。

      • 说实话我受不了看代码的时候切换文件或者上下找函数太频繁。。感觉很痛苦。。
        有时函数多了三个屏幕都不够用,毕竟屏幕还是太小吧。受不了的时候不得不去把代码打印出来放纸上看,还找起来方便点。。

        另外一点就是,函数多了就不知道怎么起名字了。就会起很长的名字,然后发现很多小函数名字差不多,功能却差得挺远。自己用这还好,别人一下子就会被这么多名字类似的函数搞懵了。。

        • OwnWaterloo says:

          你提到的问题: 函数长, 屏幕放不下; 或者抽象过多, 跳来跳去; 都有方法。
          比如前者, 编辑器有折叠功能; 后者有source insight。
          都不算太大的问题。
          你提到了一个重点: 命名。
          这么多爆炸式增长的函数, 如何给予一个恰当的命名?
          以我的经验, 很多时候取不出一个很nice的命名, 就是一个警告: 这确实不是一个很好的过程的抽象。

      • OwnWaterloo says:

        举个例子:
        f(p) {

        step1

        step2

        step3

        }

        可能会被重构为:
        step1(p1) { … }
        step2(p2) { … }
        step3(p3) { … }
        f(p) {
        step1(…);
        step2(…);
        step3(…);
        }

        有个前提: f本身是一个较完整的算法, stepX没有”独立的抽象意义”, 仅仅是为了缩短f的行数。
        这种情况下, 我觉得前者可读性还好一些, 不需要跳来跳去。
        实现起来也比较容易, 不需要设计那3个函数的参数。

        例子2: 函数确实具有抽象意义
        这是code complete还是clear code中的例子

        date d = …
        if (d.month >=4 && d.month =4 && d.month <7; }
        date d;
        if (not_summer(d) ) { … }

        咋看很合理, 但仔细想想: 有not_summer, 是否应该有is_summer? 2个。
        是否应该有is|not_spring|autumn|winter? 8个。
        是否应该有is|not_daylight_saving_time? 10个。
        还可以继续。。。

        这样下去, 一个简单的date, 可以被一些人搞出近3位数的操作方法。

        如果拿到一个项目, 都是按这种喜好 —— date, string, vector, … 都被弄出上百个方法 —— 弄的, 虽然每一个都不难, 但那爆炸性增长的函数数量, 只让人头疼。

      • OwnWaterloo says:

        总结一下:
        1. 仅仅为了减少行数而把一些snippet剥离到一个函数中,函数没有什么抽象意义, 可重用性几乎没有
        要将snippet弄成函数, 还需要一些local variable设计函数参数 —— 这不是很稳定的, 这种函数的签名经常会被修改。
        这种情况我更倾向于就保持snippet不变, 把代码排版得利于编辑器折叠就行了。
        2. 确实具有一个意义
        这其实就是”最小接口” vs “人本接口”
        我觉得两者都可接受, 只是最好能有一个方法(比如命名规则, 或者C++中成员函数表示最小, 非成员表示人本), 将最小接口和人本接口区分开。
        这样, 理解一个项目的时候可以有”轻重缓急”, 而不会在大量的人本接口上浪费心力。

  • OwnWaterloo says:

    comment没有notify?

    其实那代码是一个暗示……
    做GUI这种效率不吃紧的东西, 能不用C++, 就别用C++……

    如果我早点学python, 并且发现OpenCV其实有python的绑定……
    不知道可以节省多少时间…… 陪gf玩玩什么的……

    • 那时候因为是在winCE上做widget,没有.NET,所以只能用C++了。。
      现在做界面都尽可能用网页或者.NET了。。。

      我是觉得回复comment总发email是不是太spam了。。

  • OwnWaterloo says:

    .net也行, 它至少有元数据, 可以”按名字调用”。
    C++在运行前完成”名字到地址的转换”, 运行时就没函数名了。

    其实按今天的内存和磁盘容量来说, 元数据并不大。
    但需要它的时候, 真离不开它……
    貌似C++0x也不打算加入……

    spam这个嘛…… 应该可以做成有选项的吧?
    如果需要, 就订阅评论。
    你想啊, 习惯greader后, 还会挨个去各个网站浏览信息么……

    • 我空间有评论订阅的~~请看右边~~
      是的,javascript那种字符串直接当代码调用的功能强大得一塌糊涂!!

      • OwnWaterloo says:

        这种不一样, 这是per site的rss, 粒度太粗了。
        至少要per post吧? 有没有per user的忘记了……
        我太挑剔了……

Comments are closed.