一個控制器一個Action - Janos Pasztor

banq發表於2019-01-09

你在控制器中放了多少個動作Action?5-6?20?如果我告訴你我的限制只能是一種Action方法,你會怎麼說?
可以肯定地說,大多數Web應用程式在其控制器中都有太多的Action操作方法,但它很快就會失去控制,違背單一責任原則違規行為。我一直在和朋友談論這個問題,他們建議在一個控制器類中只放一個動作Action 的方法可能就是解決這個問題的方法。聽起來很荒謬,讓我們按照這條路走一會兒。

一個控制器......
構建控制器的一種非常流行的方法是沿著CRUD(Create-Read-Update-Delete)的分離。如果我們要編寫一個非常簡單的API來處理BlogPost遵循這種方法的實體,我們會得到這樣的結果:

class BlogPostController {
    @Route(method="POST", endpoint="/blogposts")
    public BlogPostCreateResponse create(String title /*...*/) {
        //...
    }

    @Route(method="GET", endpoint="/blogposts")
    public BlogPostListResponse list() {
        //...
    }

    @Route(method="GET", endpoint="/blogposts/:id")
    public BlogPostGetResponse get(String id) {
        //...
    }
    
    @Route(method="PATCH", endpoint="/blogposts/:id")
    public BlogPostUpdateResponse update(String id, String title /*...*/) {
        //...
    }

    @Route(method="DELETE", endpoint="/blogposts/:id")
    public BlogPostDeleteResponse delete(String id) {
        //...
    }
}

從表面上看,這看起來很好,因為與BlogPost實體相關的所有功能都組合在一起。但是,我們留下了一部分:建構函式。如果我們使用依賴注入(我們真的應該),我們的建構函式必須宣告所有依賴項,如下所示:

class BlogPostController {
    private UserAuthorizer userAuthorizer;
    private BlogPostBusinessLogic blogPostBusinessLogic;
    
    public BlogPostController(
        UserAuthorizerInterface userAuthorizer,
        BlogPostBusinessLogicInterface blogPostBusinessLogic
    ) {
        this.userAuthorizer        = userAuthorizer;
        this.blogPostBusinessLogic = blogPostBusinessLogic;
    }
    
    /* ... */
}


現在,讓我們寫一個測試。你測試你的應用程式,對吧?首先,我們測試get方法:

class BlogPostControllerTest {
    @Test
    public void testGetNonExistentShouldThrowException() {
        BlogPostController controller = new BlogPostController(
            //get does not need an authorizer
            null,
            new FakeBlogPostBusinessLogic()
        );
        
        //Do the test
    }
}

等等......你看到了嗎?建構函式的第一個引數是null。你可能在想,那又怎樣?但這非常重要:null表示您的控制器的get() 方法不需要的依賴項。

如果是這種情況,您將違反單一責任原則,因為您可以刪除該依賴項而不影響該get()方法的功能。

確實,單一責任是在業務意義上定義的,而不是在編碼意義上定義,但如果您遵循CRUD設定,那麼您在商業意義上也可能違反SRP。

單一責任原則:一個class應該只有一個改變的理由。

一個Action動作

當我開始用這種實現來檢視我的程式碼時,我不得不承認:在查詢SRP違規時,CRUD風格更應該進行仔細檢查。
所以,我提出了一個激進的解決方案:一個控制器,一個動作。重構後,我們的程式碼如下所示:

class BlogPostGetController {
    private BlogPostBusinessLogicInterface blogPostBusinessLogic;
    
    public BlogPostGetController(
        BlogPostBusinessLogicInterface blogPostBusinessLogic
    ) {
        this.blogPostBusinessLogic = blogPostBusinessLogic;
    }
    
    @Route(method="GET", endpoint="/blogposts/:id")
    public BlogPostGetResponse get(String id) {
        //...
    }
}


簡單,包裝精美,最重要的是:責任不再是單一的。但等等,還有更多!看看BlogPostBusinessLogicInterface。從API來看,還必須有一些公平的方法。有一個叫做介面隔離原理的東西。

介面隔離原則:不應強制客戶端(呼叫者)依賴它不使用的方法。

如果我們想要堅持這個原則,我們需要將該介面分為BlogPostGetBusinessLogicInterface 幾個。然後,實現可能如下所示:

class BlogPostBusinessLogicImpl
    implements
        BlogPostGetBusinessLogicInterface,
        BlogPostCreateBusinessLogicInterface,
        /* ... */ {
        
    /* ... */
}


但是,這個類可能會遇到與我們的控制器相同的問題:它是單一責任原則違規的體現。獲取部落格文章和建立部落格文章的業務邏輯根本不同。

為了解決這個問題,我們可以採用與控制器相同的方法:將(大概數千行)BlogPostBusinessLogicImpl拆分成整齊打包的單方法類。

然後我們繼續進入資料儲存層,並在那裡發現同樣的事情。所以我們分割介面以及實現本身。
如果我們遵循這個邏輯,你最終得到的應用程式被切割成只有一個動作的類。但是,雖然我們正在努力,但我們可以進一步推動事情。

這是......函式性的嗎?!
如果你稍微眯一眼就會看到一個奇怪的模式出現:我們的建構函式的唯一目的是在例項變數中儲存傳入的依賴項,在我們的例子中是blogPostBusinessLogic物件。blogPostBusinessLogic本身也是一個具有單個函式的類例項,它將在執行期間由操作使用。

正如我們將在本節中看到的,只有一個建構函式和一個方法的類與函數語言程式設計中使用的兩個概念的組合非常相似:高階函式和currying。

高階函式是一個採用了一種不同的函式作為引數。JavaScript中的一個簡單示例如下所示:

//foo gets bar (a function) as a parameter for execution
function foo (bar) {
    //The function stored in the variable bar is executed and the result returned
    return bar();
}


Currying 就是我們將一個帶有兩個引數的函式拆分成一個帶有一個引數的函式,該引數再次返回第二個函式,這第二個函式也還是一個引數。

無Curring

function add (a, b) {
    return a + b;
}
//yields 42
add(32, 10);


變成Curring:

function add (a) {
    return function(b) {
        return a + b;
    }
}
//yields 42
intermediate = add(32);
final = intermediate(10);



Currying允許更多的關注點分離,因為第一次呼叫可以完全獨立於第二次呼叫。

加入更高階函式和currying,我們以前的Java程式碼可以用函式式Javascript重寫,如下所示:

/**
 * This is the constructor, which is receiving the dependencies.
 * 
 * @param {function} blogPostGetBusinessLogicInterface
 */
function BlogPostGetController(blogPostGetBusinessLogicInterface) {
    /**
     * This is the actual action method. 
     * 
     * @param {string} id
     */
    return function(id) {
        //Call the business logic passed in the outer function.
        //(analogous to the getById method)
        return blogPostGetBusinessLogicInterface(id)
    }
}

如果仔細觀察,函式風格的Javascript和OOP Java實現具有相同的功能,即從業務邏輯中獲取部落格文章並將其返回。
所以從本質上講,每個控制器只有一個動作使我們更接近編寫函式程式碼,因為單方法類幾乎完全符合高階函式的行為。您仍然可以繼續編寫OOP程式碼並使用函數語言程式設計的一些有益方面。
但我們可以更進一步,我們實際上可以使我們的Java程式碼純淨。(純函式中沒有可變狀態。)為了實現這一點,我們宣告所有變數,final以便在設定後不能修改它們:

class BlogPostGetController {
    private final BlogPostGetBusinessLogicInterface blogPostGetBusinessLogic;
    
    public BlogPostGetController(
        final BlogPostGetBusinessLogicInterface blogPostGetBusinessLogic
    ) {
        this.blogPostGetBusinessLogic = blogPostGetBusinessLogic;
    }
    
    @Route(method="GET", endpoint="/blogposts/:id")
    public BlogPostGetResponse get(final String id) {
        //All variables here should be final
        return new BlogPostGetResponse(
            blogPostGetBusinessLogic.getById(id) 
        );
    }
}


正在用Java進行函數語言程式設計!嗯,無論如何,或多或少。函式風格的程式設計不會讓您的程式碼神奇地變得更好。你仍然可以編寫長達數千行的方法,但這隻會有點困難。

這不是OOP與FP
網際網路上的許多討論似乎都是OOP是函數語言程式設計的致命敵人,FP既是程式設計的未來,也是時髦的時尚,取決於你傾聽哪一方。
然而,事實是OOP和FP相處得很好。物件導向為您提供了結構,而函數語言程式設計為您提供了不變性並且更容易測試程式碼。
一個控制器一個動作範例,當與不變性結合時,在我看來導致OOP和FP的有益混合。
 

相關文章