为什么extends是有害的(一)

本文探讨了Java编程中应避免实现继承的原因。实现继承会失去灵活性,代码修改困难;会导致耦合问题,影响程序稳定性;还存在脆弱的基类问题,基类修改可能使派生类功能紊乱。建议使用接口继承和封装数据结构代替实现继承。
部署运行你感兴趣的模型镜像

概述
大多数好的设计者象躲避瘟疫一样来避免使用实现继承(extends 关系)。%80的代码应该完全用interfaces写,不用具体的基类。事实上,四人帮的设计模式的书大量的关于怎样用interface继承代替实现继承。这个文章描述设计者为什么有这样的怪癖的想法。

Extends是有害的;也许对于Charles Manson这个级别的不是,但是足够糟糕的它应该在任何的可能的时候被避开。四人帮的设计模式花了很大的部分讨论用interface继承代替实现继承。

好的设计者在他的代码中,大部分用interface,而不是具体的基类。这个文章讨论,为什么设计者会有这样怪癖的习惯,并且也介绍一些基于interface的编程基础。

Interface和Class
一次,我参加一个Java用户组的会议。在会议中,Jams Gosling(Java之父)做发起人讲话。在那令人难忘的Q&A部分,有人问他:“如果你重新构造Java,你想改变什么?”。“我想抛弃classes”他回答。在笑声平息后,它解释说,真正的问题不是由于class本身,而是实现继承(extends 关系)。接口继承(implements关系)是更好的。你应该尽可能的避免实现继承。

失去了灵活性
为什么你应该避免实现继承呢?第一个问题是明确的使用具体类名将你固定到特定的实现,给底层的改变增加了不必要的困难。

在当前的敏捷编程方法中,核心是并行的设计和开发的概念。在你详细设计程序前,你开始编程。这个技术不同于传统方法的形式----传统的方式是设计应该在编码开始前完成----但是许多成功的项目已经证明你能够更快速的开发高质量代码,相对于传统的按部就班的方法。但是在并行开发的核心是主张灵活性。你不得不以某一种方式写你的代码以至于最新发现的需求能够尽可能没有痛苦的合并到已有的代码中。

胜于实现你也许需要的特征,你只需实现你明确需要的特征,而且适度的对变化的包容。如果你没有这种灵活,并行的开发,那简直不可能。

对于Inteface的编程是灵活结构的核心。为了说明为什么,让我们看一下当使用它们的时候,会发生什么。考虑下面的代码:
[/代码]
f()
{   LinkedList list = new LinkedList();
    //...
    g( list );
}

g( LinkedList list )
{
    list.add( ... );
    g2( list )
}
[/代码]
现在,假设一个对于快速查询的需求被提出,以至于这个LinkedList不能够解决。你需要用HashSet来代替它。在已有代码中,变化不能够局部化,因为你不仅仅需要修改f()也需要修改g()(它带有LinkedList参数),并且还有g()把列表传递给的任何代码。
象下面这样重写代码:
[/代码]
f()
{   Collection list = new LinkedList();
    //...
    g( list );
}

g( Collection list )
{
    list.add( ... );
    g2( list )
}
[/代码]
这样修改Linked list成hash,可能只是简单的用new HashSet()代替new LinkedList()。就这样。没有其他的需要修改的地方。
作为另一个例子,比较下面两段代码:
[/代码]
f()
{   Collection c = new HashSet();
    //...
    g( c );
}

g( Collection c )
{
    for( Iterator i = c.iterator(); i.hasNext() )
        do_something_with( i.next() );
}
[/代码]
和 
[/代码]
f2()
{   Collection c = new HashSet();
    //...
    g2( c.iterator() );
}

g2( Iterator i )
{   while( i.hasNext() )
        do_something_with( i.next() );
}
[/代码]
g2()方法现在能够遍历Collection的派生,就像你能够从Map中得到的键值对。事实上,你能够写iterator,它产生数据,代替遍历一个Collection。你能够写iterator,它从测试的框架或者文件中得到信息。这会有巨大的灵活性。
耦合
对于实现继承,一个更加关键的问题是耦合---令人烦躁的依赖,就是那种程序的一部分对于另一部分的依赖。全局变量提供经典的例子,证明为什么强耦合会引起麻烦。例如,如果你改变全局变量的类型,那么所有用到这个变量的函数也许都被影响,所以所有这些代码都要被检查,变更和重新测试。而且,所有用到这个变量的函数通过这个变量相互耦合。也就是,如果一个变量值在难以使用的时候被改变,一个函数也许就不正确的影响了另一个函数的行为。这个问题显著的隐藏于多线程的程序。

作为一个设计者,你应该努力最小化耦合关系。你不能一并消除耦合,因为从一个类的对象到另一个类的对象的方法调用是一个松耦合的形式。你不可能有一个程序,它没有任何的耦合。然而,你能够通过遵守OO规则,最小化一定的耦合(最重要的是,一个对象的实现应该完全隐藏于使用他的对象)。例如,一个对象的实例变量(不是常量的成员域),应该总是private。我意思是某段时期的,无例外的,不断的。(你能够偶尔有效地使用protected方法,但是protected实例变量是可憎的事)同样的原因你应该不用get/set函数---他们对于是一个域公用只是使人感到过于复杂的方式(尽管返回修饰的对象而不是基本类型值的访问函数是在某些情况下是由原因的,那种情况下,返回的对象类是一个在设计时的关键抽象)。

这里,我不是书生气。在我自己的工作中,我发现一个直接的相互关系在我OO方法的严格之间,快速代码开发和容易的代码实现。无论什么时候我违反中心的OO原则,如实现隐藏,我结果重写那个代码(一般因为代码是不可调试的)。我没有时间重写代码,所以我遵循那些规则。我关心的完全实用—我对干净的原因没有兴趣。

脆弱的基类问题
现在,让我们应用耦合的概念到继承。在一个用extends的继承实现系统中,派生类是非常紧密的和基类耦合,当且这种紧密的连接是不期望的。设计者已经应用了绰号“脆弱的基类问题”去描述这个行为。基础类被认为是脆弱的是,因为你在看起来安全的情况下修改基类,但是当从派生类继承时,新的行为也许引起派生类出现功能紊乱。你不能通过简单的在隔离下检查基类的方法来分辨基类的变化是安全的;而是你也必须看(和测试)所有派生类。而且,你必须检查所有的代码,它们也用在基类和派生类对象中,因为这个代码也许被新的行为所打破。一个对于基础类的简单变化可能导致整个程序不可操作。

让我们一起检查脆弱的基类和基类耦合的问题。下面的类extends了Java的ArrayList类去使它像一个stack来运转:
[/代码]
class Stack extends ArrayList
{   private int stack_pointer = 0;

    public void push( Object article )
    {   add( stack_pointer++, article );
    }

    public Object pop()
    {   return remove( --stack_pointer );
    }

    public void push_many( Object[] articles )
    {   for( int i = 0; i < articles.length; ++i )
            push( articles );
    }
}
[/代码]

甚至一个象这样简单的类也有问题。思考当一个用户平衡继承和用ArrayList的clear()方法去弹出堆栈时:
[/代码]
Stack a_stack = new Stack();
a_stack.push("1");
a_stack.push("2");
a_stack.clear();
[/代码]

这个代码成功编译,但是因为基类不知道关于stack指针堆栈的情况,这个stack对象当前在一个未定义的状态。下一个对于push()调用把新的项放入索引2的位置。(stack_pointer的当前值),所以stack有效地有三个元素-下边两个是垃圾。(Java的stack类正是有这个问题,不要用它).

对这个令人讨厌的继承的方法问题的解决办法是为Stack覆盖所有的ArrayList方法,那能够修改数组的状态,所以覆盖正确的操作Stack指针或者抛出一个例外。(removeRange()方法对于抛出一个例外一个好的候选方法)。

这个方法有两个缺点。第一,如果你覆盖了所有的东西,这个基类应该真正的是一个interface,而不是一个class。如果你不用任何继承方法,在实现继承中就没有这一点。第二,更重要的是,你不能够让一个stack支持所有的ArrayList方法。例如,令人烦恼的removeRange()没有什么作用。唯一实现无用方法的合理的途径是使它抛出一个例外,因为它应该永远不被调用。这个方法有效的把编译错误成为运行错误。不好的方法是,如果方法只是不被定义,编译器会输出一个方法未找到的错误。如果方法存在,但是抛出一个例外,你只有在程序真正的运行时,你才能够发现调用错误。

对于这个基类问题的一个更好的解决办法是封装数据结构代替用继承。这是新的和改进的Stack的版本:
[/代码]
class Stack
{
    private int stack_pointer = 0;
    private ArrayList the_data = new ArrayList();
    
    public void push( Object article )
{
    the_data.add( stack_poniter++, article );
}

public Object pop()
{
    return the_data.remove( --stack_pointer );
}

public void push_many( Object[] articles )
{
    for( int i = 0; i < o.length; ++i )
        push( articles );
}
}
[/代码]

到现在为止,一直都不错,但是考虑脆弱的基类问题,我们说你想要在stack创建一个变量, 用它在一段周期内跟踪最大的堆栈尺寸。一个可能的实现也许象下面这样:
[/代码]
class Monitorable_stack extends Stack
{
    private int high_water_mark = 0;
    private int current_size;

    public void push( Object article )
    {
        if( ++current_size > high_water_mark )
            high_water_mark = current_size;
        super.push( article );
    }

    publish Object pop()
    {
        --current_size;
        return super.pop();
    }

    public int maximum_size_so_far()
    {
        return high_water_mark;
    }
}
[/代码]

这个新类运行的很好,至少是一段时间。不幸的是,这个代码发掘了一个事实,push_many()通过调用push()来运行。首先,这个细节看起来不是一个坏的选择。它简化了代码,并且你能够得到push()的派生类版本,甚至当Monitorable_stack通过Stack的参考来访问的时候,以至于high_water_mark能够正确的更新。

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

<think>好的,用户想用IntelliJ IDEA开发个垃圾分类系统。首先,我需要明确用户的需求是什么。垃圾分类系统通常包括分类规则、数据库管理、用户界面、识别功能等。用户可能希望实现个能够根据输入物品返回分类结果的系统,可能需要机器学习模型或者规则引擎的支持。 接下来,我要考虑如何在IntelliJ IDEA中搭建这样的项目。首先,项目创建步骤:使用IDEA新建Java项目,选择Maven或Gradle作为构建工具,这有助于管理依赖。然后需要配置JDK,确保版本兼容性。用户可能对Java环境不太熟悉,所以需要简要说明JDK的安装和配置,比如引用[3]提到的环境变量设置。 然后是模块设计。垃圾分类系统可能需要几个模块,比如核心分类逻辑、数据管理、用户界面。核心逻辑可以用Java实现,比如定义垃圾类别和匹配规则。数据管理可能需要数据库,比如MySQL,或者使用文件存储。这部分需要引入JDBC或者Hibernate等框架,用户可能需要指导如何添加相关依赖到pom.xml或build.gradle。 用户界面部分,如果用户想要图形界面,可以考虑JavaFX或Swing。如果是Web应用,可能需要Spring Boot框架,这样可以用Thymeleaf或前端技术。需要根据用户的技术栈选择合适的方案,但用户的问题中没有明确说明,可能需要假设是桌面应用或简单的控制台应用。 集成智能识别功能的话,可能需要调用外部API,比如图像识别服务,或者训练个简单的分类模型。这部分需要引入HTTP客户端库,比如OkHttp,或者机器学习库如Weka。不过,考虑到复杂度,可能先实现基于规则的分类,再逐步扩展。 测试和调试部分,需要指导用户如何使用IDEA的调试工具,比如断点、变量查看等。引用[1]提到IDEA提升代码质量,所以可以建议用户编写单元测试,使用JUnit,并利用IDEA的测试运行功能。 部署方面,如果是桌面应用,打包成JAR;如果是Web应用,用Maven或Gradle构建WAR包。需要说明如何使用IDEA的Build功能生成可执行文件。 可能遇到的问题包括依赖管理、数据库连接、异常处理等。需要提示用户查阅日志,利用IDEA的代码分析工具优化代码结构。 最后,生成相关问题时要覆盖开发流程的各个方面,比如环境配置、模块设计、技术选型、测试部署等,帮助用户深入思考每个步骤可能遇到的挑战和解决方案。</think>### IntelliJ IDEA开发垃圾分类系统实现步骤 #### 、环境准备 1. 安装**JDK 17**并配置环境变量(参考引用[3]的PATH配置) 2. 在IntelliJ IDEA中创建Maven项目: ```java New Project → Maven → 选择JDK版本 ``` 3. 添加必要依赖到`pom.xml`: ```xml <dependencies> <!-- 数据库连接 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- 界面框架 --> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>17.0.2</version> </dependency> </dependencies> ``` #### 二、系统架构设计 $$ \begin{cases} \text{数据层} & \text{(MySQL 8.0 垃圾分类数据库)} \\ \text{服务层} & \text{(分类算法服务、数据查询服务)} \\ \text{表现层} & \text{(JavaFX图形界面)} \end{cases} $$ #### 三、核心功能实现 1. **分类规则引擎**: ```java public class GarbageClassifier { private static final Map<String, String> RULES = Map.of( "电池", "有害垃圾", "菜叶", "厨余垃圾", "玻璃", "可回收物" ); public String classify(String item) { return RULES.getOrDefault(item, "其他垃圾"); } } ``` 2. **数据库设计**: ```sql CREATE TABLE garbage_types ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) UNIQUE, category ENUM('可回收物','有害垃圾','厨余垃圾','其他垃圾') ); ``` #### 四、界面开发(JavaFX) ```java public class MainApp extends Application { @Override public void start(Stage stage) { TextField input = new TextField(); Button btn = new Button("识别"); Label result = new Label(); btn.setOnAction(e -> { String classification = new GarbageClassifier().classify(input.getText()); result.setText("分类结果:" + classification); }); stage.setScene(new Scene(new VBox(10, input, btn, result), 300, 200)); stage.show(); } } ``` #### 五、调试与优化 - 使用`Shift+F9`启动调试模式 - 通过`Alt+F8`评估表达式验证分类逻辑 - 使用Database工具连接MySQL(参考引用[2]的目录配置) #### 六、部署打包 1. 通过`Build → Build Artifacts`生成可执行JAR 2. 使用`jpackage`创建原生安装包
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值