专栏首页java一日一条有经验的Java开发者和架构师容易犯的10个错误(上)

有经验的Java开发者和架构师容易犯的10个错误(上)

首先允许我们问一个严肃的问题?为什么Java初学者能够方便的从网上找到相对应的开发建议呢?每当我去网上搜索想要的建议的时候,我总是能发现一 大堆是关于基本入门的教程、书籍以及资源。同样也发现网上到处充斥着从宽泛的角度描述一个大型的企业级项目:如何扩展你的架构,使用消息总线,如何与数据 库互联,UML图表使用以及其它高层次的信息。

这时问题就来了:我们这些有经验的(专业的)Java开发者如何找到合适的开发建议呢?现在,这就是所谓的灰色区域,当然同样的也很难找到哪些是针 对于资深开发者、团队领导者以及初级架构师的开发建议。你会发现网上那些纷杂的信息往往只关注于开发世界的一角,要么是极致(甚至可以说变态级别)地关心 开发代码的细节,要么是泛泛而谈架构理念。这种拙劣的模仿需要有一个终结。

说了半天,大家可能明白我希望提供的是那些好的经验、有思考的代码、和一些可以帮助从中级到资深开发者的建议。本文记录了在我职业生涯里发现的那些有经验的开发者最常犯的10个问题。发生这些问题大多是对于信息的理解错误和没有特别注意,而且避免这些问题是很容易的。

让我们开始逐个讨论这些你可能不是很容易注意的问题。我之所以会用倒序是因为第一个问题给我带来了最大的困扰。但所有这10个问题(考虑一些额外的因素)对于你而言来说都有可能给你造成困扰(信不信由你);-)。

文章分上篇和下篇,本文是上篇。

10、错误地使用或者误解了依赖式注入

对于一个企业级项目来说,依赖式注入通常被认为是好的概念。存在一种误解——如果使用依赖注入就不会出现问题。但是这是真的吗?

依赖式注入的一个基本思想是不通过对象本身去查看依赖关系,而是通过开发者以在创建对象之前预先定义并且初始化依赖关系(需要一个依赖式注入框架, 譬如Spring)。当对象真正被创建时,仅仅需要在构造函数中传入预先配置好的对象(构造函数注入)或者使用方法(方法注入)。

然而总的思想是指仅仅传递对象需要的依赖关系。但是我依然可以在一些新的项目里发现如下的代码:

public class CustomerBill {

        //Injected by the DI framework
        private ServerContext serverContext;

        public CustomerBill(ServerContext serverContext)
        {
                    this.serverContext = serverContext;
        }

        public void chargeCustomer(Customer customer)
        {
                    CreditCardProcessor creditCardProcessor = serverContext.getServiceLocator().getCreditCardProcessor();
                    Discount discount  = serverContext.getServiceLocator().getActiveDiscounts().findDiscountFor(customer);

                    creditCardProcessor.bill(customer,discount);
        }
}

当然,这不是真正的依赖注入。因为这个对象始终需要由它自己进行初始化。在上面的代码中 “serverContext.getServiceLocator().getCreditCardProcessor()”这一行代码更加体现了该问题。

译注:作者指的是 creditCardProcessor、discount 这两个变量的初始化。

当然,最好的方式应该是只注入那些真正需要的变量(最好是标记为final):

public class CustomerBillCorrected {

        //Injected by the DI framework
        private ActiveDiscounts activeDiscounts;

        //Injected by the DI framework
        private CreditCardProcessor creditCardProcessor;

        public CustomerBillCorrected(ActiveDiscounts activeDiscounts,CreditCardProcessor creditCardProcessor)
        {
                    this.activeDiscounts = activeDiscounts;
                    this.creditCardProcessor = creditCardProcessor;
        }

        public void chargeCustomer(Customer customer)
        {
                    Discount discount  = activeDiscounts.findDiscountFor(customer);

                    creditCardProcessor.bill(customer,discount);
        }
}

译注:请注意两段代码的区别在于对于代码中需要的资源的范围。从使用依赖注入的角度来看,前一段代码中注入的范围很大,那就意味着有了更多的变化空 间,但是容易造成代码的功能不单一,同时增加了代码测试的复杂度。后一段代码中注入的范围就很精确,代码简单易懂测试起来也比较容易。

9、像使用perl一样来使用Java

(跟其它编程语言比较)Java提供了一个好的属性,就是它的类型安全性。可能在一些小型项目中开发者只有你自己,你可以使用任何喜欢的编程风格。 但如果是一个代码量很大以及复杂系统的Java项目中, 在错误发生时你需要早一些得到警示。大多数的错误应该在编译阶段而不是在到运行期就被发现(如果 你对Java不甚了解,请阅读Java的相关资料)。

Java提供了许多特性去辅助产生这些编译器的警告。但是如果你写出下面的代码编译器还是没有办法捕获到对应的警告:

public class AnimalFactory {

        public static Animal createAnimal(String type)
        {
                    switch(type)
                    {
                    case "cat":
                                return new Cat();
                    case "cow":
                                return new Cow();
                    case "dog":
                                return new Dog();
                    default:
                                return new Cat();
                    }
        }

}

译注:请注意这段代码只能工作在jdk 1.7 下。JDK 1.7以下的版本编译不能通过。

这段代码是非常危险,而且编译器不会产生任何的警告帮到你。一个开发者也许会调用工厂方法以一个错误拼写“dig”创建一个Cat对象。但实际上, 他需要的是一个Dog对象。这段代码不但会编译通过,而且错误往往只能在运行期被发现。更严重的是,这个错误的产生依赖于应用程序本身的特性,因而有可能 在程序上线几个月以后才能发现它。

你是否希望Java编译器可以通过某种机制帮你提前捕获到这样错误呢?这里提供一个更正确的方式来确保代码只有被正确的使用的情况下才能编译通过(当然还有其他的解决方案)。

public class AnimalFactoryCorrected {
        public enum AnimalType { DOG, CAT,COW,ANY};

        public static Animal createAnimal(AnimalType type)
        {
                    switch(type)
                    {
                    case CAT:
                                return new Cat();
                    case COW:
                                return new Cow();
                    case DOG:
                                return new Dog();
                    case ANY:
                    default:
                                return new Cat();
                    }
        }

}

译注: 1. 这里使用了java enum类型。 2. 由于对Perl语言不慎了解,猜测作者隐含的意思是perl语言如果按照第一种写法,被错误调用的时候是否在编译器就会报错。 如果知道的人可以帮忙解释一下。

8、像C语言一样使用Java (即不理解面向对象编程的理念)

回到C语言编程的时代,C语言建议用过程化的形式来书写代码。开发者使用结构体存储数据,通过函数来描述那些发生在数据上的操作。这时数据是愚笨的,方法反而是聪明的。

译注:作者估计是想说,数据和函数是分离的没有直接的上下文来描述之间的关系。

然而Java正好是反其道而行。由于Java是一门面向对象的语言,在创建类的时候数据和函数被聪明地绑定在一起。

然而大多数的Java开发者要么不理解上述两门语言之间的区别,要么就是他们讨厌编写面向对象代码。虽然他们知道过程化开发方式与Java有些格格不入。

一个在Java应用程序中,最显而易见的过程化编程就是使用instanceof,并在随后的代码中判断向上转换或向下转换。instanceof有它合适使用的情况,但在企业级的代码中通常它是一个严重的反模式示例。

下面的示例代码描述了这种情况:

public void bill(Customer customer, Amount amount) {

                    Discount discount = null;
                    if(customer instanceof VipCustomer)
                    {
                                VipCustomer vip = (VipCustomer)customer;
                                discount = vip.getVipDiscount();
                    }
                    else if(customer instanceof BonusCustomer)
                    {
                                BonusCustomer vip = (BonusCustomer)customer;
                                discount = vip.getBonusDiscount();
                    }
                    else if(customer instanceof LoyalCustomer)
                    {
                                LoyalCustomer vip = (LoyalCustomer)customer;
                                discount = vip.getLoyalDiscount();
                    }

                    paymentGateway.charge(customer, amount);

}

使用面向对象的来重构以后的代码如下:

public void bill(Customer customer, Amount amount) {

                   Discount discount = customer.getAppropriateDiscount();

                   paymentGateway.charge(customer, amount);

}

译注:这里可以认为使用设计模式当中的Factory method模式。

每个继承Customer(或者实现Customer接口)定义了一个返回折扣的方法。这样做的好处在于你可以假如新的类型的Customer而不 需要关系customer的管理逻辑。而使用instanceof的判断每次添加一个新的类型的Customer意味着你需要修改customer打印代 码、财务代码、联系代码等等,当然同时还需要添加一个If判断。

你可能也需要查看一下关于充血模型vs贫血模型的讨论。

7、滥用延迟初始化 (即不能真正的理解对象的生命周期)

我经常能发现如下的代码:

public class CreditCardProcessor {
        private PaymentGateway paymentGateway = null;

        public void bill(Customer customer, Amount amount) {

            //Billing a customer always needs a payment gateway anyway
            getPaymentGateway().charge(customer.getCreditCart(),amount);

        }

        private PaymentGateway getPaymentGateway()
        {
                    if(paymentGateway == null)
                    {
                                paymentGateway = new PaymentGateway();
                                paymentGateway.init(); //Network side Effects  here
                    }
                    return paymentGateway;
        }

}

延迟初始化初衷是好的,即如果你有个非常昂贵的对象(譬如对象需要网络连接或者连接Web API等等),当然应该只在需要的时候创建它。然而,在你的项目中使用这项技术的时候最好确认以下两点:

  • 这个对象真的很“昂贵”(你是如何给出这样的结论或者定义?)
  • 存在这个对象不被使用的情况 (确实不需要创建这个对象)

在实际开发中,我不断发现延迟初始化被用在对象上。但实际上,这样的对象要么不是真的那么“昂贵”,要么总是在运行期创建。延迟初始化这种对象能得到什么好处呢?

过度使用延迟初始化的主要问题在于它隐藏了组件的生命周期。一个经过良好搭建的应用程序应该对它主要部件的生命周期有清晰的了解。应用程序需要非常清楚对象什么时候应该被创建、使用和销毁。依赖注入可以帮助定义对象的生命周期。

但依赖注入在对象创建时也有副作用。使用以来注入表明应用程序状态依赖于对象被创建的顺序(按照要求的类型顺序)。由于涵盖了过多的用例,对应用程序调试就变成了一件不可能完成的事情。复现生产环境也变成一项巨大的工程,因为你必须十分清楚场景执行的顺序。

相应的我们需要做的就是定义应用程序启动时需要的所有对象。这也带来了一个额外的好处,可以在应用程序发布过程中捕获任何致命的错误。

6、把GOF(俗称四人帮)当作圣经

我十分羡慕设计模式的几位作者。这本书籍以其他书籍所无可比拟的气势影响了整个IT界。如果你没看过《设计模式》,没有记住模式的名字或者准则的话,那么在面试中就可能无法通过。期望这样的错误可以慢慢改善。

不要误解我,这本书本身是没有问题的。问题出在人们如何解释以及使用它。下面是通常场景:

  1. 架构师马克,拿到这本书开始阅读。他觉得这本书牛逼坏了!
  2. 马克趁热打铁开始阅读现在工作的代码。
  3. 马克选择了一种设计模式并应用到了代码当中。
  4. 随后马克把这本书推荐给了那些跟他重复同样步骤的资深开发者。

结果就是一团糟。

如何正确使用这本书实际上已经在导读中做了清晰的说明(提醒那些不看导读的人)——“在过去你有个问题,而且这个问题总是一遍又一遍地困扰着你”。注意到其中的顺序了吗?先有一个问题,然后查看这本书之后找到对应的解决方案。

不要掉进看这本书的陷阱当中——“找到一个方案然后尝试把它应用在自己的的代码中。尤其要注意的是,一些书中描述的模式在现实当中已经不再正确。”(请参见下篇第5条)。

原文链接: zeroturnaround 翻译: ImportNew.com - Andy.Song 译文链接: http://www.importnew.com/6953.html

本文分享自微信公众号 - java一日一条(mjx_java)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2015-11-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java 回顾 ( Revisiting Java )

    最近在看一些工程代码,于是看了看设计模式,看设计模式之前发现Java是先修知识,又重新补了一遍Java,温故知新,获得一些新的体会。 本文不打算作为“Java知...

    哲洛不闹
  • 理解Java Integer的缓存策略

    本文将介绍 Java 中 Integer 缓存的相关知识。这是 Java 5 中引入的一个有助于节省内存、提高性能的特性。首先看一个使用 Integer 的示例...

    哲洛不闹
  • 为什么做java的web开发我们会使用struts2,springMVC和spring这样的框架?

    今年我一直在思考web开发里的前后端分离的问题,到了现在也颇有点心得了,随着这个问题的深入,再加以现在公司很多web项目的控制层的技术框架由struts2迁移到...

    哲洛不闹
  • 全解系列:内存泄漏定位工具LeakCanary!

    在日常开发中,不可避免的会遇到内存泄漏的问题,从而导致App的内存使用紧张,严重的情况还会导致App的卡顿甚至是奔溃,所以需要开发人员解决这些内存泄漏的问题。

    胡飞洋
  • 【计算机本科补全计划】Java学习笔记(二) 基础语法红黄蓝

    正文之前 刚才突然想起来自己CCF报名还没报名成功,就是一阵心绞痛,明晚就截止了,要是没报上,到时候怎么跟老师交差,突然想起来,如果老师问我最近干了啥,可以用准...

    用户1687088
  • Java基础7:关于Java类和包的那些事

    https://h2pl.github.io/2018/04/24/javase7

    黄小斜
  • 【Java基本功】很多人经常忽视的Java基础知识点

    主函数可以被重载,但是JVM只识别main(String[] args),其他都是作为一般函数。这里面的args知识数组变量可以更改,其他都不能更改。

    Java技术江湖
  • 「MoreThanJava」Day 4:面向对象基础

    面向对象程序设计 (Object-Oriented Programming, OOP) 是当今主流的程序设计范型,它取代了 20 世纪 70 年代的 "结构化"...

    我没有三颗心脏
  • 设计模式 接口隔离原则

    接着,要进行更改了。对好看的定义,发生了改变,那么就应该改变PettyGirl中的内容,但是已经在接口中定义了。那么就有问题了。即,接口承担的内容过多导致

    mySoul
  • 设计模式之原型模式

    爱撒谎的男孩

扫码关注云+社区

领取腾讯云代金券