Spring 常犯的十大错误,打死都不要犯!

原文链接:https://www.cnblogs.com/liululee/p/11235999.html

作者:Toni Kukurin,译者:万想

原文:

https://www.toptal.com/spring/top-10-most-common-spring-framework-mistakes

译文:www.cnblogs.com/liululee/p/11235999.html

       

1. 错误一:太过关注底层

我们正在解决这个常见错误,是因为 “非我所创” 综合症在软件开发领域很是常见。症状包括经常重写一些常见的代码,很多开发人员都有这种症状。

虽然理解特定库的内部结构及其实现,在很大程度上是好的并且很有必要的(也可以是一个很好的学习过程),但作为软件工程师,不断地处理相同的底层实现细节对个人的开发生涯是有害的。

像 Spring 这种抽象框架的存在是有原因的,它将你从重复地手工劳作中解放出来,并允许你专注于更高层次的细节 —— 领域对象和业务逻辑。

因此,接受抽象。下次面对特定问题时,首先进行快速搜索,确定解决该问题的库是否已被集成到 Spring 中;现在,你可能找到一个合适的现成解决方案。

比如,一个很有用的库,在本文的其他部分,我将在示例中使用 Project Lombok 注解。Lombok 被用作样板代码生成器,希望懒惰的开发人员在熟悉这个库时不会遇到问题。举个例子,看看使用 Lombok 的 “标准 Java Bean” 是什么样子的:

如你所想,上述代码被编译为:

但是,请注意,如果你打算在 IDE 中使用 Lombok,很可能需要安装一个插件,可在 此处 找到 Intellij IDEA 版本的插件。

2. 错误二:内部结构 “泄露”

公开你的内部结构,从来都不是一个好主意,因为它在服务设计中造成了不灵活性,从而促进了不好的编码实践。“泄露” 的内部机制表现为使数据库结构可以从某些 API 端点访问。例如,下面的 POJO(“Plain Old Java Object”)类表示数据库中的一个表:

@Entity
@NoArgsConstructor
@Getter
public class TopTalentEntity {

    @Id
    @GeneratedValue
    private Integer id;

    @Column
    private String name;

    public TopTalentEntity(String name) {
        this.name = name;
    }

}

假设,存在一个端点,他需要访问 TopTalentEntity 数据。返回 TopTalentEntity 实例可能很诱人,但更灵活的解决方案是创建一个新的类来表示 API 端点上的 TopTalentEntity 数据。

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class TopTalentData {
    private String name;
}

这样,对数据库后端进行更改将不需要在服务层进行任何额外的更改。考虑下,在 TopTalentEntity 中添加一个 “password” 字段来存储数据库中用户密码的 Hash 值 —— 如果没有 TopTalentData 之类的连接器,忘记更改服务前端,将会意外地暴露一些不必要的秘密信息。

3. 错误三:缺乏关注点分离

随着程序规模的增长,逐渐地,代码组织成为一个越来越重要的问题。讽刺的是,大多数好的软件工程原则开始在规模上崩溃 —— 特别是在没有太多考虑程序体系结构设计的情况下。开发人员最常犯的一个错误就是混淆代码关注点,这很容易做到!

通常,打破 关注点分离 的是将新功能简单地 “倒” 在现有类中。当然,这是一个很好的短期解决方案(对于初学者来说,它需要更少的输入),但它也不可避免地会在将来成为一个问题,无论是在测试期间、维护期间还是介于两者之间。考虑下下面的控制器,它将从数据库返回 TopTalentData

@RestControllerpublic class TopTalentController {    private final TopTalentRepository topTalentRepository;    @RequestMapping("/toptal/get")    public List<TopTalentData> getTopTalent() {        return topTalentRepository.findAll()                .stream()                .map(this::entityToData)                .collect(Collectors.toList());    }    private TopTalentData entityToData(TopTalentEntity topTalentEntity) {        return new TopTalentData(topTalentEntity.getName());    }}
public class TopTalentController {

    private final TopTalentRepository topTalentRepository;

    @RequestMapping("/toptal/get")
    public List<TopTalentData> getTopTalent() {
        return topTalentRepository.findAll()
                .stream()
                .map(this::entityToData)
                .collect(Collectors.toList());
    }

    private TopTalentData entityToData(TopTalentEntity topTalentEntity) {
        return new TopTalentData(topTalentEntity.getName());
    }

}

起初,这段代码似乎没什么特别的问题;它提供了一个从 TopTalentEntity 实例检索出来的 TopTalentData 的 List。

然而,仔细观察下,我们可以看到 TopTalentController 实际上在此做了些事情;也就是说,它将请求映射到特定端点,从数据库检索数据,并将从 TopTalentRepository 接收的实体转换为另一种格式。一个“更干净” 的解决方案是将这些关注点分离到他们自己的类中。看起来可能是这个样子的:

@RestController@RequestMapping("/toptal")@AllArgsConstructorpublic class TopTalentController {    private final TopTalentService topTalentService;    @RequestMapping("/get")    public List<TopTalentData> getTopTalent() {        return topTalentService.getTopTalent();    }}@AllArgsConstructor@Servicepublic class TopTalentService {    private final TopTalentRepository topTalentRepository;    private final TopTalentEntityConverter topTalentEntityConverter;    public List<TopTalentData> getTopTalent() {        return topTalentRepository.findAll()                .stream()                .map(topTalentEntityConverter::toResponse)                .collect(Collectors.toList());    }}@Componentpublic class TopTalentEntityConverter {    public TopTalentData toResponse(TopTalentEntity topTalentEntity) {        return new TopTalentData(topTalentEntity.getName());    }}
@RequestMapping("/toptal")
@AllArgsConstructor
public class TopTalentController {

    private final TopTalentService topTalentService;

    @RequestMapping("/get")
    public List<TopTalentData> getTopTalent() {
        return topTalentService.getTopTalent();
    }
}

@AllArgsConstructor
@Service
public class TopTalentService {

    private final TopTalentRepository topTalentRepository;
    private final TopTalentEntityConverter topTalentEntityConverter;

    public List<TopTalentData> getTopTalent() {
        return topTalentRepository.findAll()
                .stream()
                .map(topTalentEntityConverter::toResponse)
                .collect(Collectors.toList());
    }
}

@Component
public class TopTalentEntityConverter {
    public TopTalentData toResponse(TopTalentEntity topTalentEntity) {
        return new TopTalentData(topTalentEntity.getName());
    }
}

这种层次结构的另一个优点是,它允许我们通过检查类名来确定将功能驻留在何处。此外,在测试期间,如果需要,我们可以很容易地用模拟实现来替换任何类。

4. 错误四:缺乏异常处理或处理不当

一致性的主题并非是 Spring(或 Java)所独有的,但仍然是处理 Spring 项目时需要考虑的一个重要方面。虽然编码风格可能存在争议(通常团队或整个公司内部已达成一致),但拥有一个共同的标准最终会极大地提高生产力。对多人团队尤为如此;一致性允许交流发生,而不需要花费很多资源在手把手交接上,也不需要就不同类的职责提供冗长的解释。

考虑一个包含各种配置文件、服务和控制器的 Spring 项目。在命名时保持语义上的一致性,可以创建一个易于搜索的结构,任何新的开发人员都可以按照自己的方式管理代码;例如,将 Config 后缀添加到配置类,服务层以 Service 结尾,以及控制器用 Controller 结尾。

与一致性主题密切相关,服务器端的错误处理值得特别强调。如果你曾经不得不处理编写很差的 API 的异常响应,那你可能知道原因 —— 正确解析异常会是一件痛苦的事情,而确定这些异常最初发生的原因则更为痛苦。

作为一名 API 开发者,理想情况下你希望覆盖所有面向用户的端点,并将他们转换为常见的错误格式。这通常意味着有一个通用的错误代码和描述,而不是逃避解决问题:a) 返回一个 “500 Internal Server Error”信息。b) 直接返回异常的堆栈信息给用户。(实际上,这些都应该不惜一切代价地去避免,因为除了客户端难以处理以外,它还暴露了你的内部信息)。

例如,常见错误响应格式可能长这样:

@Valuepublic class ErrorResponse {    private Integer errorCode;    private String errorMessage;}
public class ErrorResponse {

    private Integer errorCode;
    private String errorMessage;

}

与此类似的事情在大多数流行的 API 中也经常遇到,由于可以容易且系统地记录,效果往往很不错。将异常转换为这种格式可以通过向方法提供 @ExceptionHandler 注解来完成(注解案例可见于第六章)。

5. 错误五:多线程处理不当

不管是桌面应用还是 Web 应用,无论是 Spring 还是 No Spring,多线程都是很难破解的。由并行执行程序所引起的问题是令人毛骨悚然且难以捉摸的,而且常常难以调试 —— 实际上,由于问题的本质,一旦你意识到你正在处理一个并行执行问题,你可能就不得不完全放弃调试器了,并 “手动” 检查代码,直到找到根本上的错误原因。

不幸的是,这类问题并没有千篇一律的解决方案;根据具体场景来评估情况,然后从你认为最好的角度来解决问题。

当然,理想情况下,你也希望完全避免多线程错误。同样,不存在那种一刀切的方法,但这有一些调试和防止多线程错误的实际考虑因素:

5.1. 避免全局状态

首先,牢记 “全局状态” 问题。如果你正创建一个多线程应用,那么应该密切关注任何可能全局修改的内容,如果可能的话,将他们全部删掉。如果某个全局变量有必须保持可修改的原因,请仔细使用 synchronization,并对程序性能进行跟踪,以确定没有因为新引入的等待时间而导致系统性能降低。

5.2. 避免可变性

这点直接来自于 函数式编程,并且适用于 OOP,声明应该避免类和状态的改变。简而言之,这意味着放弃 setter 方法,并在所有模型类上拥有私有的 final 字段。它们的值唯一发生变化的时间是在构造期间。这样,你可以确定不会出现争用问题,且访问对象属性将始终提供正确的值。

5.3. 记录关键数据

评估你的程序可能会在何处发生异常,并预先记录所有关键数据。如果发生错误,你将很高兴可以得到信息说明收到了哪些请求,并可更好地了解你的应用程序为什么会出现错误。需要再次注意的是,日志记录引入了额外的文件 I/O,可能会严重影响应用的性能,因此请不要滥用日志。

5.4. 复用现存实现

每当你需要创建自己的线程时(例如:向不同的服务发出异步请求),复用现有的安全实现来代替创建自己的解决方案。这在很大程度上意味着要使用 ExecutorServices 和 Java 8 简洁的函数式 CompletableFutures 来创建线程。Spring 还允许通过 DeferredResult 类来进行异步请求处理。

6. 错误六:不使用基于注解的验证

假设我们之前的 TopTalent 服务需要一个端点来添加新的 TopTalent。此外,假设基于某些原因,每个新名词都需要为 10 个字符长度。执行此操作的一种方法可能如下:

@RequestMapping("/put")
public void addTopTalent(@RequestBody TopTalentData topTalentData) {
    boolean nameNonExistentOrHasInvalidLength =
            Optional.ofNullable(topTalentData)
         .map(TopTalentData::getName)
   .map(name -> name.length() == 10)
   .orElse(true);

    if (nameNonExistentOrInvalidLength) {
        // throw some exception
    }

    topTalentService.addTopTalent(topTalentData);
}

然而,上面的方法(除了构造很差以外)并不是一个真正 “干净” 的解决办法。我们正检查不止一种类型的有效性(即 TopTalentData 不得为空,TopTalentData.name 不得为空,且 TopTalentData.name 为 10 个字符长度),以及在数据无效时抛出异常。

通过在 Spring 中集成 Hibernate validator,数据校验可以更干净地进行。让我们首先重构 addTopTalent 方法来支持验证:

@RequestMapping("/put")public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) {    topTalentService.addTopTalent(topTalentData);}@ExceptionHandler@ResponseStatus(HttpStatus.BAD_REQUEST)public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) {    // handle validation exception}// 此外,我们还必须指出我们想要在 TopTalentData 类中验证什么属性:public class TopTalentData {    @Length(min = 10, max = 10)    @NotNull    private String name;}
public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) {
    topTalentService.addTopTalent(topTalentData);
}

@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) {
    // handle validation exception
}
// 此外,我们还必须指出我们想要在 TopTalentData 类中验证什么属性:
public class TopTalentData {
    @Length(min = 10, max = 10)
    @NotNull
    private String name;
}

现在,Spring 将在调用方法之前拦截其请求并对参数进行验证 —— 无需使用额外的手工测试。

另一种实现相同功能的方法是创建我们自己的注解。虽然你通常只在需要超出 Hibernate的内置约束集 时才使用自定义注解,本例中,我们假设 @Length 不存在。你可以创建两个额外的类来验证字符串长度,一个用于验证,一个用于对属性进行注解:

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Documented@Constraint(validatedBy = { MyAnnotationValidator.class })public @interface MyAnnotation {    String message() default "String length does not match expected";    Class<?>[] groups() default {};    Class<? extends Payload>[] payload() default {};    int value();}@Componentpublic class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> {    private int expectedLength;    @Override    public void initialize(MyAnnotation myAnnotation) {        this.expectedLength = myAnnotation.value();    }    @Override    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {        return s == null || s.length() == this.expectedLength;    }}
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { MyAnnotationValidator.class })
public @interface MyAnnotation {

    String message() default "String length does not match expected";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    int value();

}

@Component
public class MyAnnotationValidator implements ConstraintValidator<MyAnnotationString{

    private int expectedLength;

    @Override
    public void initialize(MyAnnotation myAnnotation) {
        this.expectedLength = myAnnotation.value();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        return s == null || s.length() == this.expectedLength;
    }
}

请注意,这些情况下,关注点分离的最佳实践要求在属性为 null 时,将其标记为有效(isValid 方法中的 s == null),如果这是属性的附加要求,则使用 @NotNull 注解。

public class TopTalentData {
    @MyAnnotation(value = 10)
    @NotNull
    private String name;
}


7. 错误七:(依旧)使用基于xml的配置

虽然之前版本的 Spring 需要 XML,但如今大部分配置均可通过 Java 代码或注解来完成;XML 配置只是作为附加的不必要的样板代码。

本文(及其附带的 GitHub 仓库)均使用注解来配置 Spring,Spring 知道应该连接哪些 Bean,因为待扫描的顶级包目录已在 @SpringBootApplication 复合注解中做了声明,如下所示:

@SpringBootApplicationpublic class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }}
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

复合注解(可通过 Spring 文档 了解更多信息)只是向 Spring 提示应该扫描哪些包来检索 Bean。在我们的案例中,这意味着这个顶级包 (co.kukurin)将用于检索:

  • @Component (TopTalentConverter, MyAnnotationValidator)
  • @RestController (TopTalentController)
  • @Repository (TopTalentRepository)
  • @Service (TopTalentService) 类

如果我们有任何额外的 @Configuration 注解类,它们也会检查基于 Java 的配置。

8. 错误八:忽略 profile

在服务端开发中,经常遇到的一个问题是区分不同的配置类型,通常是生产配置和开发配置。在每次从测试切换到部署应用程序时,不要手动替换各种配置项,更有效的方法是使用 profile。

考虑这么一种情况:你正在使用内存数据库进行本地开发,而在生产环境中使用 MySQL 数据库。本质上,这意味着你需要使用不同的 URL 和 (希望如此) 不同的凭证来访问这两者。让我们看看可以如何做到这两个不同的配置文件:

8.1. APPLICATION.YAML 文件


# set default profile to 'dev'spring.profiles.active: dev# production database detailsspring.datasource.url: 'jdbc:mysql://localhost:3306/toptal'spring.datasource.username: rootspring.datasource.password:8.2. APPLICATION-DEV.YAML 文件spring.datasource.url: 'jdbc:h2:mem:'spring.datasource.platform: h2
spring.profiles.active: dev

# production database details
spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal'
spring.datasource.username: root
spring.datasource.password:
8.2. APPLICATION-DEV.YAML 文件
spring.datasource.url: 'jdbc:h2:mem:'
spring.datasource.platform: h2

假设你不希望在修改代码时意外地对生产数据库进行任何操作,因此将默认配置文件设为 dev 是很有意义的。

然后,在服务器上,你可以通过提供 -Dspring.profiles.active=prod 参数给 JVM 来手动覆盖配置文件。另外,还可将操作系统的环境变量设置为所需的默认 profile。

9. 错误九:无法接受依赖项注入

正确使用 Spring 的依赖注入意味着允许其通过扫描所有必须的配置类来将所有对象连接在一起;这对于解耦关系非常有用,也使测试变得更为容易,而不是通过类之间的紧耦合来做这样的事情:

public class TopTalentController {

    private final TopTalentService topTalentService;

    public TopTalentController() {
        this.topTalentService = new TopTalentService();
    }
}

我们让 Spring 为我们做连接:

public class TopTalentController {    private final TopTalentService topTalentService;    public TopTalentController(TopTalentService topTalentService) {        this.topTalentService = topTalentService;    }}class TopTalentController {

    private final TopTalentService topTalentService;

    public TopTalentController(TopTalentService topTalentService) {
        this.topTalentService = topTalentService;
    }
}

Misko Hevery 的 Google talk 深入解释了依赖注入的 “为什么”,所以,让我们看看它在实践中是如何使用的。在关注点分离(常见错误 #3)一节中,我们创建了一个服务和控制器类。

假设我们想在 TopTalentService 行为正确的前提下测试控制器。我们可以通过提供一个单独的配置类来插入一个模拟对象来代替实际的服务实现:

@Configurationpublic class SampleUnitTestConfig {    @Bean    public TopTalentService topTalentService() {        TopTalentService topTalentService = Mockito.mock(TopTalentService.class);        Mockito.when(topTalentService.getTopTalent()).thenReturn(                Stream.of("Mary", "Joel").map(TopTalentData::new).collect(Collectors.toList()));        return topTalentService;    }}
public class SampleUnitTestConfig {
    @Bean
    public TopTalentService topTalentService() {
        TopTalentService topTalentService = Mockito.mock(TopTalentService.class);
        Mockito.when(topTalentService.getTopTalent()).thenReturn(
                Stream.of("Mary""Joel").map(TopTalentData::new).collect(Collectors.toList()));
        return topTalentService;
    }
}

然后,我们可以通过告诉 Spring 使用 SampleUnitTestConfig 作为它的配置类来注入模拟对象:

@ContextConfiguration(classes = { SampleUnitTestConfig.class })

之后,我们就可以使用上下文配置将 Bean 注入到单元测试中。


10. 错误十:缺乏测试,或测试不当

尽管单元测试的概念已经存在很长时间了,但很多开发人员似乎要么 “忘记” 做这件事(特别是如果它不是 “必需” 的时候),要么只是在事后把它添加进来。这显然是不可取的,因为测试不仅应该验证代码的正确性,还应该作为程序在不同场景下应如何表现的文档。

在测试 Web 服务时,很少只进行 “纯” 单元测试,因为通过 HTTP 进行通信通常需要调用 Spring 的 DispatcherServlet,并查看当收到一个实际的 HttpServletRequest 时会发生什么(使它成为一个 “集成” 测试,处理验证、序列化等)。

REST Assured,一个用于简化测试REST服务的 Java DSL,在 MockMVC 之上,已经被证明提供了一个非常优雅的解决方案。考虑以下带有依赖项注入的代码片段:

@RunWith(SpringJUnit4Cla***unner.class)@ContextConfiguration(classes = {        Application.class,        SampleUnitTestConfig.class})public class RestAssuredTestDemonstration {    @Autowired    private TopTalentController topTalentController;    @Test    public void shouldGetMaryAndJoel() throws Exception {        // given        MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given()                .standaloneSetup(topTalentController);        // when        MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get");        // then        response.then().statusCode(200);        response.then().body("name", hasItems("Mary", "Joel"));    }}
@ContextConfiguration(classes = {
        Application.class,
        SampleUnitTestConfig.class
})
public class RestAssuredTestDemonstration {

    @Autowired
    private TopTalentController topTalentController;

    @Test
    public void shouldGetMaryAndJoel() throws Exception {
        // given
        MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given()
                .standaloneSetup(topTalentController);

        // when
        MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get");

        // then
        response.then().statusCode(200);
        response.then().body("name", hasItems("Mary""Joel"));
    }

}

SampleUnitTestConfig 类将 TopTalentService 的模拟实现连接到 TopTalentController 中,而所有的其他类都是通过扫描应用类所在包的下级包目录来推断出的标准配置。RestAssuredMockMvc 只是用来设置一个轻量级环境,并向 /toptal/get 端点发送一个 GET 请求。

推荐阅读:


640喜欢我可以给我设为星标哦640

640?wx_fmt=jpeg640?wx_fmt=jpeg

好文章,我 在看 

640?wx_fmt=jpeg
展开阅读全文

三层中常犯错误

10-09

rnrn相信大家对三层开发都已经耳熟能详,可是我却发现新公司的既有代码中有一些违背分层开发思想的东西,现在与大家分享这些错误,我们共勉之。rnrn如果有人觉得对三层开发拿捏得不是太准,请参照李天平的文章:分层开发思想与小笼包,这篇文章用隐喻说明分层开发,是非常好的一篇文章。rnrn正文:rnrn1.界面层参与非界面逻辑,抢业务逻辑层的饭碗rnrn什么是界面逻辑:rnrn界面层应该有的逻辑就是显示的逻辑,例如根据逻辑结果显示某一个Panel不显示另外一个Panel,或者有一个数据集应该在界面上怎么呈现,这是界面层的逻辑rnrn例子场景:rnrn用户登录时首先验证用户输入的用户名是否有效,如果用户名有效,然后再验证用户输入的密码是否和用户名匹配,如果匹配则表示用户可以登录,增加用户的登录次数,然后将用户的信息写入Session中;否则返回错误。在这个过程中除了将用户信息写入Session这一步属于界面逻辑以外其他的操作都应该放在业务逻辑层。rnrn错误代码示例:rnrnprivate void buttonLogin_Click(object sender, EventArgs ev)rnrn string userName = textBoxUserName.Text;rn string password = textPassword.Text;rnrn if (Business.Account.Exists(userName))rn rn bool success = Business.Account.Login(userName, password);rn if (success)rn rn Business.Account.AddLoginTime(userName);rnrn Session["user"] = new User(userName, password);rn Redirect("/");rn rn elsern rn this.labelMessage.Text = "登录失败。";rn rn rn elsern rn this.lableMessage.Text = "用户名不存在。";rn rnrn rnrnrn分析:在上面的代码中一个UI层的一个事件中调用了三次rules层的方法:rnrnBusiness.Account.Exists(userName)rnrnBusiness.Account.Login(userName, password)rnrnBusiness.Account.AddLoginTime(userName);rnrn还附加有条件判断,这种方法在执行效果上面是没有什么错误的,可是却造成了逻辑前移;本来应该在逻辑层执行的判断放在了界面层,是不合适的。rnrn2.数据访问层参与了大量的业务逻辑rnrn这种现象经常出现在大量使用存储过程的系统中,将一大堆逻辑统统放在一个存储过程中实现了,乍一看可能很有效率,其实造成了系统结构的失调,给维护带来困难,数据访问层甚者数据库要抢逻辑层的饭碗了。rnrn还以用户登录为例:rnrn下面是业务逻辑层的登录方法:rnrn rnrn//业务逻辑层的登录方法rnpublic int Login(string userName, string password) rnrn return DataAccess.UserAccount.Login(userName, password);rn rnrnrn rnrn下面是数据层的登录方法:rnrn rnrn//数据访问层的登录方法rnpublic int Login(string userName, string password)rn rn SqlParameter[] parameters = new SqlParameter[]rn // rn ;rn return SqlHelper.ExecuteProcedure("Login",parameters);rn rnrnrn rnrn下面是登录的存储过程:rnrn rnrnCREATE PROC Loginrn @userName varchar(20),rn @password varchar(20)rnAS rn IF NOT EXISTS(SELECT * FROM UserAccount WHERE UserName = @userName)rn RETURN -1rn IF NOT EXISTS(SELECT * FROM UserAccount WHERE UserName = @userName AND password = @password)rn RETURN 1rnrn UPDATE UserAccountrn SET LoginTimes = LoginTimes + 1rn WHERE UserName = @userNamernrn RETURN 0rn rnrnrn rnrn分析:从上面三段代码中我们可以很显然得看到登录的业务逻辑已经全部被后移到了数据库的存储过程中。这样使用的三层结构就失去了意义,逻辑层名存实亡了;而数据库的压力会越来越大;我们修改业务逻辑的时候不是到逻辑层修改,而是要到数据库中去修改了。rnrn3.将界面层上的数据组件(如SqlDataSource)作为参数传递到业务逻辑层去赋值rnrn这样做的坏处很明显,本来是界面层依赖于业务逻辑层的,现在业务逻辑层反过来去依赖界面层的类,需要逻辑层引用System.Web命名空间,显然是错误的。rnrn4.为了省事儿,不是直接将参数传递到业务逻辑层,而是通过HttpContext直接获得界面层应该传递的参数rnrn例子:在系统设计的初期没有记录用户登录的IP地址,而到了后期发现了这个问题,要求纪录用户IP了,为了不修改业务逻辑层方法的定义,也不用修改界面层的调用方法,于是便写出了下面的代码:rnrnpublic int Login(string userName, string password)rnrn string userIP = System.Web.HttpContext.Current.Request.UserHostAddress;rn //follow is login stepsrn rnrnrn这一点犯的错误和3中的错误相同,导致层之间形成了依赖环。rnrn5.将事务处理放在数据访问层来做rnrn事务处理应该放在业务逻辑层处理,原因是rnrn1)事务的划分是根据业务逻辑来定义的,通常一个事务就代表完成了一个完整的逻辑操作;rnrn2)一个业务逻辑可能有几个数据操作,修改几个表中的数据,而通常数据层的类的划分是根据数据库表来划分的,如果把事务处理放在数据访问层,那么业务层的方法需要调用两个以上的数据层方法时,就会出现执行两个事务的情况,显然这是不合理的。rnrn总结:rnrn1.在三层结构的划分中我们应该使三层各负其责,谁也不能越权,谁也不能懒惰,通常情况下,逻辑层应该在满足各负其责的条件下,尽可能的厚。rnrn2.三层结构中的依赖关系很明确,界面层依赖于逻辑层,逻辑层依赖于数据访问层,不能互相依赖,而形成依赖环。rnrnrnrn相信大家读了上的文章后,如果是做过三层的,那肯定是犯了什么的错误,怎么犯错误先不说,我先说说本人(今年毕业的学生)rn现在在公司开发项目的一点见解:rn我现在在公司开发一个商城网站,因为数据层和业务层都是我做的,所以体会还是满深的,特别是在订单处理那块,业务非常复杂rn基本要操作5个表,所以需要在业务层实现事务(关于这个我已经发了好几个帖子了),而数据层只是单纯的对某个表进行操作rn虽然.net事务能解决上面的问题,但是问题有多,或者服务器配置有问题。。。我现在想把这写操作放在数据层,那觉得有违背了三层的本意,(这样还是三层吗),所以我现在犹豫了到底要怎么做,我想大家在开发中,为了性能的提高而那样(把业务移到数据层做)做却是值得的,因为三层只是一个标准而已,并不是强制要这样做?rnrn 关于这样的错误我还想听听大家的意见?rnrn 论坛

没有更多推荐了,返回首页