一个控制器一个Action - Janos Pasztor

栏目: 后端 · 前端 · 发布时间: 5年前

内容简介:你在控制器中放了多少个动作Action?5-6?20?如果我告诉你我的限制只能是一种Action方法,你会怎么说?可以肯定地说,大多数Web应用程序在其控制器中都有太多的Action操作方法,但它很快就会失去控制,违背单一责任原则违规行为。我一直在和朋友谈论这个问题,他们建议在一个控制器类中只放一个动作Action 的方法可能就是解决这个问题的方法。听起来很荒谬,让我们按照这条路走一会儿。

你在控制器中放了多少个动作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 /*...*/<font>) {
        </font><font><i>//...</i></font><font>
    }

    @Route(method=</font><font>"GET"</font><font>, endpoint=</font><font>"/blogposts"</font><font>)
    <b>public</b> BlogPostListResponse list() {
        </font><font><i>//...</i></font><font>
    }

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

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

从表面上看,这看起来很好,因为与BlogPost实体相关的所有功能都组合在一起。但是,我们留下了一部分:构造函数。如果我们使用依赖注入(我们真的应该),我们的构造函数必须声明所有依赖项,如下所示:

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

现在,让我们写一个测试。你测试你的应用程序,对吧?首先,我们测试get方法:

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

等等......你看到了吗?构造函数的第一个参数是null。你可能在想,那又怎样?但这非常重要:null表示您的控制器的get() 方法不需要的依赖项。

如果是这种情况,您将违反单一责任原则,因为您可以删除该依赖项而不影响该get()方法的功能。

确实,单一责任是在业务意义上定义的,而不是在编码意义上定义,但如果您遵循CRUD设置,那么您在商业意义上也可能违反SRP。

单一责任原则:一个class应该只有一个改变的理由。

一个Action动作

当我开始用这种实现来查看我的代码时,我不得不承认:在查找SRP违规时,CRUD风格更应该进行仔细检查。

所以,我提出了一个激进的解决方案:一个控制器,一个动作。重构后,我们的代码如下所示:

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

简单,包装精美,最重要的是:责任不再是单一的。但等等,还有更多!看看BlogPostBusinessLogicInterface。从API来看,还必须有一些公平的方法。有一个叫做接口隔离原理的东西。

接口隔离原则:不应强制客户端(调用者)依赖它不使用的方法。

如果我们想要坚持这个原则,我们需要将该接口分为BlogPostGetBusinessLogicInterface 几个。然后,实现可能如下所示:

<b>class</b> BlogPostBusinessLogicImpl
    implements
        BlogPostGetBusinessLogicInterface,
        BlogPostCreateBusinessLogicInterface,
        <font><i>/* ... */</i></font><font> {
        
    </font><font><i>/* ... */</i></font><font>
}
</font>

但是,这个类可能会遇到与我们的控制器相同的问题:它是单一责任原则违规的体现。获取博客文章和创建博客文章的业务逻辑根本不同。

为了解决这个问题,我们可以采用与控制器相同的方法:将(大概数千行)BlogPostBusinessLogicImpl拆分成整齐打包的单方法类。

然后我们继续进入数据存储层,并在那里发现同样的事情。所以我们分割接口以及实现本身。

如果我们遵循这个逻辑,你最终得到的应用程序被切割成只有一个动作的类。但是,虽然我们正在努力,但我们可以进一步推动事情。

这是......函数性的吗?!

如果你稍微眯一眼就会看到一个奇怪的模式出现:我们的构造函数的唯一目的是在实例变量中存储传入的依赖项,在我们的例子中是blogPostBusinessLogic对象。blogPostBusinessLogic本身也是一个具有单个函数的类实例,它将在执行期间由操作使用。

正如我们将在本节中看到的,只有一个构造函数和一个方法的类与函数式编程中使用的两个概念的组合非常相似:高阶函数和currying。

高阶函数是一个采用了一种不同的函数作为参数。JavaScript中的一个简单示例如下所示:

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

Currying 就是我们将一个带有两个参数的函数拆分成一个带有一个参数的函数,该参数再次返回第二个函数,这第二个函数也还是一个参数。

无Curring

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

变成Curring:

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

Currying允许更多的关注点分离,因为第一次调用可以完全独立于第二次调用。

加入更高阶函数和currying,我们以前的 Java 代码可以用函数式Javascript重写,如下所示:

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

如果仔细观察,函数风格的Javascript和OOP Java实现具有相同的功能,即从业务逻辑中获取博客文章并将其返回。

所以从本质上讲,每个控制器只有一个动作使我们更接近编写函数代码,因为单方法类几乎完全符合高阶函数的行为。您仍然可以继续编写OOP代码并使用函数式编程的一些有益方面。

但我们可以更进一步,我们实际上可以使我们的Java代码纯净。(纯函数中没有可变状态。)为了实现这一点,我们声明所有变量,final以便在设置后不能修改它们:

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

正在用Java进行函数式编程!嗯,无论如何,或多或少。函数风格的编程不会让您的代码神奇地变得更好。你仍然可以编写长达数千行的方法,但这只会有点困难。

这不是OOP与FP

互联网上的许多讨论似乎都是OOP是函数式编程的致命敌人,FP既是编程的未来,也是时髦的时尚,取决于你倾听哪一方。

然而,事实是 OOP和FP相处得很好 。面向对象为您提供了结构,而函数式编程为您提供了不变性并且更容易测试代码。

一个控制器一个动作范例,当与不变性结合时,在我看来导致OOP和FP的有益混合。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java5.0Tiger程序高手秘笈

Java5.0Tiger程序高手秘笈

BrettMclaughlin / 东南大学出版社 / 2005-10 / 28.00元

代号为 “Tiger”的下一个 Java 版本,不只是个小改动版。在语言核心中有超过 100 项以上的变动,同时有大量的对 library 与 API 所做的加强,让开发者取得许多新的功能、工具与技术。但在如此多的变化下,应该从何处开始着手?也许可以从既长又无趣的语言规范说明书开始看起;或等待最少 500 页的概念与理论巨著出版;甚至还可以直接把玩新的 JDK 看看能够有什么发现;或者借由《Jav......一起来看看 《Java5.0Tiger程序高手秘笈》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具