首先,對于這個糟糕的標題,我感到很抱歉,但我發現用一句簡短的話來概括我的問題有些困難......
在我們的軟體中,有一些代碼讓我很不滿意。它是這樣的:
@FunctionalInterface
public interface OneArgCall<T, U, A> {
T execute(U u, A arg)。
}
@FunctionalInterface; }
public interface TwoArgCall<T, U, A, B> {
T execute(U u, A arg, B arg2)。
}
public <T, U, A, B> T execCall(String x, Class<U> c, OneArgCall< T, U, A> call, A arg) {
U u = doSomething(x, c)。
try {
return call.execute(u, arg);
} catch (SomeException se) {
handleSe(se)。
} catch (SomeOtherException soe) {
handleSoe(oe);
}
public <T, U, A, B> T execCall(String x, Class<P> c, TwoArgCall<T, U, A, B> call, A arg, B arg2){
U u = doSomething(x, c)。
try {
return call.execute(u, arg, arg2);
} catch (SomeException se) {
handleSe(se)。
} catch (SomeOtherException soe) {
handleSoe(oe);
也就是說,除了作為第三個引數傳遞的功能介面(當然還有該介面的引數串列),execCall方法是相同的。現在,我仍然可以接受這一點,但是這些方法的數量更多(想象一下ThreeArgCall、FourArgCall......)--這就是它變得有點令人無法忍受的地方。
那么,以所有DRY的名義:你將如何去清理這段代碼?我想象的是T execCall(String x, Class<U> c, SOMETHING, SOMETHING_ELSE),其中SOMETHING可以是OneArgCall、TwoArgCall...的任何介面,SOMETHING_ELSE代表引數串列(?
這到底能不能做到?或者有什么其他的方法可以讓我重構這段代碼以減少重復性?
uj5u.com熱心網友回復:
你實際上不需要所有這些介面。 你不需要接收任何額外的方法引數。所有這些都可以由呼叫者使用 lambda 語法來處理。
這是你唯一需要的方法:
public <T, U> T execCall(String x, Class<U> c, Function<U, T> call) {
U u = doSomething(x, c)。
try {
return call.apply(u);
} catch (SomeException se) {
handleSe(se)。
} catch (SomeOtherException soe) {
handleSoe(oe);
現在,假設你有一個需要多個引數的方法,你想使用execCall(),那該如何操作?
public Foo someMethodCall(bar bar, Arg1 a1, Arg2 a2) { .... }
Arg1 a1 = ...。
Arg2 a2 = ...。
String x = ...。
Bar b = execCall(x, Bar.class, (u) -> someMethodCall(u, a1, a2))。
使用lambda語法,你將你的3引數方法 "改編 "為1引數的Function介面。 這就是使用函式式編程中的 "部分應用 "概念。
uj5u.com熱心網友回復:
我可以想到一些受GOF設計模式啟發的方法來減少execCall中的重復,但代價是增加了復雜性,同時也給execCall的呼叫者增加了一些負擔。
1. 使用配接器或命令模式:
您可以洗掉重復的內容。
您可以通過讓 execCall 只接受一個介面并向 execCall 發送配接器來消除 execCall 的重復,配接器將包裹原始介面和引數并委托給它們。這意味著execCalls中的代碼會減少,但其他地方的代碼會增加。配接器將有一個統一的介面,這意味著引數必須被包裝在一個統一的基類中,而具體的配接器將不得不下移到他們知道的實際目標介面所需的引數型別。
與其將此解決方案視為配接器,你還可以將其視為實作命令模式。 與其讓每個客戶端只發送 execCall 的 2 或 3 個引數,不如讓他們發送一個完整的命令物件。這將是一些實作介面的類,包括一個抽象的執行函式。2個引數的客戶端將向execCall發送一個實作命令介面的物件,包括arg、arg2和call.execute(u, arg)一行。原始的execCall將執行commandObj.setU(u),然后呼叫commandObj.execute()。這樣做的代價是讓呼叫者對call.execute的知識產生負擔
。interface CallCommand<T,U>
{
T execute() 。
void setU(U u)。
}
class OneArgCallCommand<T,U,A> implements CallCommand<T,U> {
A arg;
U u;
OneArgCall<T, U, A> 呼叫。
public void setU( U u ) { this.u = u; }
@Override
public T execute() {
return call.execute(u, arg)。
}
}
class TwoArgCallCommand<T,U,A,B> implements CallCommand<T,U> {
A arg;
B arg2;
U u;
TwoArgCall<T, U, A,B> 呼叫。
public void setU( U u ) { this.u = u; }
@Override
public T execute() {
return call.execute(u, arg,arg2)。
}
}
class Logic
{
public <T, U, A, B> T execCall(String x, Class<U> c, CallCommand<T, U> callCommand) {
U u = doSomething(x, c)。
callCommand.setU(u)。
try {
return callCommand.execute()。
} catch (SomeException se) {
handleSe(se)。
} catch (SomeOtherException soe) {
handleSoe(oe);
}
return null;
}
2.使用模板方法模式
將execCall放在一個抽象的基類中。將execCall的大部分內容放在抽象類中,包括doSomething和tryCatch。把call.Execute這一行變成一個抽象方法,由每個派生類重寫。為2個引數的呼叫和3個引數的呼叫創建了execCall的派生版本,等等。同樣,不同的引數需要被打包在一個具體的類中,并有一個共同的抽象引數持有者父類,每個具體的execCall都需要下移到它知道自己需要的引數型別。這值得嗎?
3.
3.完全擺脫 execCall? 最后,重新思考整個設計可能是值得的。execCall 函式的好處是什么?是否值得增加它的復雜性?完全避免它也許是可能的,也許是不可能的。 不確定這個是否可行,這取決于代碼的結構方式。
uj5u.com熱心網友回復: 如果不采用代碼生成技巧,或者重構API本身,就不可能。這兩種方法當然都可以選擇,但我認為我更喜歡第二種方法。
try {
///最初呼叫execCallWithOneArgument的地方。
call.execute(doSomething(x, c), arg)。
//原來呼叫execCallWithTwoArguments的地方catch (SomeException se) {
handleSe(se)。
} catch (SomeOtherException soe){
handleSoe(oe);
代碼生成
它最有可能采取這樣的形式:
- 你寫一個注釋。
- 你寫一個注釋處理器。這有點名不副實;AP是作為編譯程序的一部分運行的。它們通常由注解 "觸發",并從注解中獲得大部分輸入引數,但這并不是一個要求。你應該把它們看作是 "編譯器插件"。
- 這個注解處理器將處理一個 "模板 "類。這個模板類就像你現在的20個
execCall方法一樣,只不過它被命名為ExecCallContainer0或ExecCallContainerTemplate,并且是打包私有的。當然,也有注釋。它只包含一個execCall方法,而不是全部20個。注解的作用是 "觸發 "處理器(以便它運行;你也可以設計它來觸發任何東西,并檢測以Template結尾的類或其他什么)。 - 注釋處理器創建實際的
ExecCallContainer類,為你生成所有的20個變體。這些方法大概只是處理引數(例如,將它們收集到一個串列中,或者創建一個封裝呼叫的閉包,例如: 。
/**由AP生成的。請勿編輯。*/
public <T, U, A, B> T execCall(String x, Class<P> c, TwoArgCall<T, U, A, B> call, A arg, B arg2) {
Supplier<T> s = () -> call.execute(u, arg, arg2);
return ExecCallContainerTemplate.exec(x, c, s)。
- 該生成的源檔案是你的公共類的源檔案,你在該專案中的所有其他代碼都應使用該源檔案。
你確實遇到了AP的常見缺點。它們會使構建程序變得緩慢一些,而且如果您正在處理 AP 代碼本身,您的代碼往往會變得一團糟(因為您的所有代碼現在都在呼叫不存在的方法,直到它們被生成,而這還沒有發生,目前也不能發生,因為您的 AP 正在被處理)--至少,在您運行實際構建之前。Eclipse在這方面做得很好,既簡單又快速(只需保存檔案,eclipse就會在需要的地方運行AP),其他大多數IDE將這項作業交給構建,而構建的速度往往沒有那么快,但是,一旦你完成了AP的作業,你通常不會對它進行太多的處理,所以這并不是什么大問題。
這里最大的問題是,注釋處理器的API并不完全是微不足道的,所以團隊中的某個人可能應該對該API和該代碼生成器相當熟悉,否則如果其中出現問題,整個團隊將進入火燒眉毛的模式,這可不是什么好事。這是任何使用復雜庫的人都會遇到的問題。
這段代碼中存在一些異味。可能它們只是較小的邪惡,但這表明 API 本身可以簡單地進行重構,從而使其更容易使用,并減少維護 - 贏了。
例如,四處傳遞 換句話說,不好: 好的: 你可以用哪里來呼叫它: 而不是: 也許 嘗試: 這第二個片段比第一個片段多不了多少代碼,而且消除了有20個
標籤: 上一篇:怎樣才能防止sed插入空白?
對 API 本身進行重構
。
java.lang.Class 實體是不好的,而讓 j.l.Class 型別上的泛型變得更糟(通常這應該是某種工廠介面,而不是;你使用 Class 實體是為了什么?如果是為了構造它的實體,你應該有一個工廠來代替。如果你把它作為一個鍵使用,一個專門的鍵類可能是更好的選擇。如果你將其用于反射目的,例如 "出于某種原因,以編程方式從這里獲取所有欄位",那么工廠的想法通常也是更好的選擇,除非你不想這樣稱呼它("工廠 "只是將類本身的構造器和元方面拿出來,使其可被抽象)。
public <T> T make(Class< T> type, String param) {
構造器<T> c = type.getConstructor(String.class)。
return c.newInstance(param)。
public <T> make(Function<String, T> factory, String param) {
return factory.apply(param);
}
Function<String, QuitMessage> quitMessageFactory = param -> new QuitMessage(param);
make(quitMessageFactory, " Going to sleep for the night")。
make(QuitMessage.class, " Going to sleep for the night")。
call.execute可以通過外部化傳遞xArgsCall引數和所有引數的作業來抽象化。因此,不要再有:public class Calculator {
public TwoArgCall<Double, Double, Double> addButton = (a, b) -> a b;
....
public void foo(){
double lhs = 5.5;
double rhs = 3.3;
calculatorTape.execCall(addButton, lhs, rhs)。
}
public class Calculator {
public TwoArgCall<Double, Double, Double> addButton = (a, b) -> a b;
....
public void foo(){
double lhs = 5.5;
double rhs = 3.3;
calculatorTape.execCall(() -> addButton.exec(lhs, rhs))。
}
}
execCall方法、20個XArgsCall功能介面等的必要性。我認為這很值得。
