Architecture 重构代码,使子例程不知道被调用方

Architecture 重构代码,使子例程不知道被调用方,architecture,refactoring,Architecture,Refactoring,我遇到了一些关于应用程序代码结构(可维护性)的问题 我已经对代码进行了重构,以使通用子例程能够处理常见任务,从而优化可重用性。 在这些子例程中,有时必须根据被调用方传递给例程的参数执行附加/其他操作 然而,我一直认为软件设计的一个基本概念是例程不应该“意识到”被调用方;例程不应该知道调用它的是哪个例程。我越来越清楚,我需要在我的应用程序中更高程度地实现这一点 如何整合这两个看似对立的概念,使代码更加透明 考虑下一个例子: public void processA(){ //specif

我遇到了一些关于应用程序代码结构(可维护性)的问题

我已经对代码进行了重构,以使通用子例程能够处理常见任务,从而优化可重用性。 在这些子例程中,有时必须根据被调用方传递给例程的参数执行附加/其他操作

然而,我一直认为软件设计的一个基本概念是例程不应该“意识到”被调用方;例程不应该知道调用它的是哪个例程。我越来越清楚,我需要在我的应用程序中更高程度地实现这一点

如何整合这两个看似对立的概念,使代码更加透明

考虑下一个例子:

public void processA(){

    //specific code

    genericStuff(true);

    //specific code

}

public void processB(){

    //specific code

    genericStuff(false);

    //specific code

}

public void genericStuff (boolean doExtra){

    initializeStuff();
    doStuff();

    if(doExtra){
        doExtraStuff();
    }

    doEvenMoreStuff();
    finalizeStuff();    
}
如果我要分解通用例程的功能,结果是:

public void processA(){

    //specific code

    initializeStuff();
    doStuff();
    doExtraStuff();
    doEvenMoreStuff();
    finalizeStuff();

    //specific code

}

public void processB(){

    //specific code

    initializeStuff();
    doStuff();
    doEvenMoreStuff();
    finalizeStuff();

    //specific code

}
你最终会重复代码。显然,这不是重构的目的,重构的重点是集中功能,从而最小化代码库?如果例程
doEvenMoreStuff()
不再需要怎么办?在那种情况下,我必须检查所有的通话程序

我如何整合这两个看似对立的概念,使我的 代码变得更透明

假性精神分裂症.:-)这有助于将自己与呼叫者和被呼叫者分开,把它想象成两个完全不同的鞋子来填补

在这些子例程中,有时必须执行附加/其他操作 根据调用通用例程的人执行

这可能就是问题所在。设计反映你的心态。如果您认为一个函数必须根据调用它的人/对象拥有不同的代码分支,那么很难进入这样的思维模式,即允许您独立于调用方来设计函数。您可能让太多的代码工作方式完全影响您的设计,而不是轻微影响您的设计

你通常想退一步,从更高层次的角度来看待这个问题。“这个函数应该做什么?它的参数是什么?”忘记调用者,尤其是在应用泛型/可重用思维时,因为代码担心它将被调用的确切位置以及由谁来调用很少重用

要记住的另一件事是,您不希望单个函数提供太多不同的功能分支。这里有一个平衡的动作,但是想象一下一个功能,可以治愈怪物,治愈它的疾病,伤害它,或者让它从死亡中复活,所有这些都是一次性的。这真的需要4个独立的函数,所以您通常只需要将其设为4个独立的函数。您可能对调用函数的各种场景考虑得太多的原因之一是,如果您的函数做得太多,并且承担的责任太多。在这种情况下,重构通常归结为将其分解为具有更多单一职责的更小的函数

根据进一步的详细信息进行编辑

public void genericStuff(boolean doExtra){
    initializeStuff();
    doStuff();
    if(doExtra){
        doExtraStuff();
    }
    doEvenMoreStuff();
    finalizeStuff();    
}
给定这样一个例程,消除这个
doExtra
参数将导致一些冗余逻辑(
initializeStuff
doEvenMoreStuff
,和
finalizeStuff

但是,分解它不会导致将该逻辑添加到
processA
processB
——这与其说是分解,还不如说是消除函数并将其实现细节泄漏到调用方。如果为了更高的内聚性而将此函数拆分为两个,例如,结果将是
genericA
genericB

public void genericA(){
    initializeStuff();
    doStuff();
    doEvenMoreStuff();
    finalizeStuff();
}

public void genericB(){
    initializeStuff();
    doStuff();
    doExtraStuff();
    doEvenMoreStuff();
    finalizeStuff();
}
时间耦合

值得注意的是,
初始化
完成
的需求存在时间耦合。依赖于其函数调用顺序的接口(尤其是没有检查错误的机制的接口)可能是人为错误和维护问题的根源,因此,您通常不希望向
processA
processB
公开处理此初始化和完成工作的需要。在设计接口时处理辅助接口中的时间耦合的一种策略如下:

void some_function(function whatToDo){
    initializeStuff();
    whatToDo();
    finalizeStuff();    
}
这就隐藏了对外部世界进行初始化和完成、开始和结束、开始和提交、启动和关闭等操作的需要,将处理易出错逻辑的需要保持在少数集中化功能上。客户机可以在
initializeStuff
finalizeStuff
之间以包含他们想要执行的内容的函数的形式传递他们想要执行的操作。支持闭包的语言可以使这非常容易做到

不伦不类

但是,
doExtra
不像一个分支控制变量。它不是一个导致完全不同的事情发生的参数,只是一个额外的事情发生。只要这个可选的额外行为在逻辑上与函数所做的事情相关联,它就不一定是混乱或维护问题的根源

这里的困难之一是,我们用一种难以描述的方式来描述函数。要真正确定一个函数是否在做逻辑上和功能上相关的事情,很大程度上取决于人为因素,甚至取决于您命名函数的方式,而不仅仅是耦合和修改的外部内存量等指标,因此,为了进一步展开讨论,我们必须使用一些描述性的例子,以便更好地确定工作的逻辑单位,以及它们是否有意义,或者仅仅是造成混淆的原因

副作用

public void genericStuff(boolean doExtra){
    initializeStuff();
    doStuff();
    if(doExtra){
        doExtraStuff();
    }
    doEvenMoreStuff();
    finalizeStuff();    
}
尽管如此,一些让你担心的因素可能是副作用。一个函数通常会产生副作用(它会改变什么)
 public void processA_helper() {
    doExtraStuff();
 }

 public void processA(){
   //specific code
   genericStuff(processA_helper);
   //specific code
 }

 public void processB_helper() {
    // nothing to do
 }

 public void processB(){
    //specific code
    genericStuff(processB_helper);
    //specific code
 }

 public void genericStuff (procedure caller_specific_action){
    initializeStuff();
    doStuff();
    caller_specific_action();
    doEvenMoreStuff();
    finalizeStuff();    
}