面向可复用性和可维护性的编程

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、软件复用的分类

软件复用可以从不同的层次进行划分,每个层次都有其独特的特性和应用场景。主要分为以下几类:

  1. 源代码级复用:直接复用代码片段。
  2. 模块级复用:通过类或接口进行复用。
  3. 库级复用:通过API或包进行复用。
  4. 系统级复用:通过框架进行复用。

源代码级复用

源代码级复用是最基本的复用形式,即将代码片段复制到新项目中。这种方法简单直接,但也有一些明显的缺点,如维护困难、代码冗余和潜在的错误风险。为了高效地进行源代码级复用,可以利用互联网中的代码搜索工具,如GitHub和Searchcode,快速找到需要的代码片段。

模块级复用

模块级复用通过复用类和接口来实现。主要方法包括继承、重载、组合和委派。继承允许新类从现有类中继承属性和方法,重载允许同一方法有不同的实现,组合和委派则是通过将现有对象作为新对象的成员来实现复用。

示例代码:

// 模块级复用示例:通过继承和组合实现
class BaseClass {
    public void display() {
        System.out.println("BaseClass Display");
    }
}

class DerivedClass extends BaseClass {
    @Override
    public void display() {
        System.out.println("DerivedClass Display");
    }
}

class CompositeClass {
    private BaseClass base;

    public CompositeClass(BaseClass base) {
        this.base = base;
    }

    public void display() {
        base.display();
    }
}

库级复用

库级复用通过使用已有的API和包来实现。开发者可以直接调用库中的功能,而无需了解其内部实现。常见的库包括Java的标准库、第三方库如Apache Commons等。

示例代码:

// 使用库级复用示例:Apache Commons Lang库
import org.apache.commons.lang3.StringUtils;

public class LibraryReuseExample {
    public static void main(String[] args) {
        String str = "Hello World!";
        System.out.println(StringUtils.reverse(str));
    }
}

系统级复用

系统级复用通过使用框架来实现。框架提供了一整套解决方案,开发者只需在框架的基础上进行开发即可。常见的框架有Spring、Hibernate等。

示例代码:

// 使用Spring框架进行系统级复用
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
class HelloController {
    @GetMapping("/hello")
    public String sayHello() {
        return "Hello, World!";
    }
}

比较不同复用方法

不同层级的复用方法各有优缺点:

  1. 源代码级复用:简单直接,但维护困难。
  2. 模块级复用:灵活性高,但需要良好的设计。
  3. 库级复用:开发效率高,但依赖库的稳定性和文档。
  4. 系统级复用:提供完整解决方案,但学习成本高。

二、里氏替换原则 (LSP)

里氏替换原则由Barbara Liskov在1987年提出,它是SOLID五大面向对象设计原则中的第二个。LSP的核心思想是:如果S是T的子类型,那么类型为T的对象可以被类型为S的对象替换,而不会改变程序的正确性。换句话说,子类必须能够替换其父类,并且不会导致程序行为的不一致。

LSP的关键点

  1. 行为兼容性
  • 子类应实现父类的所有方法,但不应改变这些方法的预期行为;并且子类的方法不能引入新的异常类型或者破坏父类的方法约定。
  1. 接口一致性
  • 子类应保持父类的接口不变,包括方法的签名、输入参数和返回值类型。
  1. 语义一致性
  • 子类的方法实现应该与父类的方法具有相同的语义,即子类不应违背父类的业务逻辑。

在这里插入图片描述

遵循LSP的好处

  • 增强代码的可维护性:由于子类可以无缝替换父类,代码的修改和扩展变得更加容易和安全。
  • 提高代码的可复用性:设计良好的父类和子类可以在不同的项目和场景中重复使用。
  • 简化测试过程:通过确保子类与父类的行为一致,可以减少测试的复杂性和工作量。

给出一个遵循LSP的例子

interface Shape {
    int getArea();
}

class Rectangle implements Shape {
    private int width;
    private int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    public int getArea() {
        return width * height;
    }
}

class Square implements Shape {
    private int side;

    public void setSide(int side) {
        this.side = side;
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

public class TestLSP {
    public static void main(String[] args) {
        Shape rect = new Rectangle();
        ((Rectangle) rect).setWidth(5);
        ((Rectangle) rect).setHeight(10);
        System.out.println("Rectangle area: " + rect.getArea()); // 输出50

        Shape square = new Square();
        ((Square) square).setSide(5);
        System.out.println("Square area: " + square.getArea()); // 输出25
    }
}

在这个例子中,Rectangle和Square都实现了Shape接口,而不是通过继承关系联系在一起。这样,Rectangle和Square各自独立实现其逻辑,不再相互干扰,遵循了LSP的要求。

三、协变与逆变

协变(covariance)和逆变(contravariance)是面向对象编程中的重要概念,特别是在处理泛型时。它们决定了在使用子类型和父类型的情况下,如何在方法参数和返回值中进行替换。以下是协变和逆变的概念解释和代码示例。

协变(Covariance)

父类型→子类型:越来越具体specific ;返回值类型:不变或变得更具体;异常的类型:也是如此。
协变允许使用子类的对象来替换父类的对象。换句话说,如果类型 B 是类型 A 的子类型,并且 G 是一个泛型类型,那么 G<B> 可以被认为是 G<A> 的子类型。协变通常用于返回类型。

示例:协变

假设我们有一个动物类层次结构,其中 CatAnimal 的子类:

class Animal {
    public void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow");
    }
}

interface AnimalShelter<T> {
    T getAnimal();
}

class CatShelter implements AnimalShelter<Cat> {
    @Override
    public Cat getAnimal() {
        return new Cat();
    }
}

public class CovarianceExample {
    public static void main(String[] args) {
        AnimalShelter<Cat> catShelter = new CatShelter();
        // 由于协变,CatShelter 可以赋值给 AnimalShelter<Animal>
        AnimalShelter<? extends Animal> animalShelter = catShelter;

        Animal animal = animalShelter.getAnimal();
        animal.makeSound(); // 输出:Meow
    }
}

在这个例子中,AnimalShelter<out T> 表示 AnimalShelter 是协变的,也就是说参数类型越来越具体,? extends Animal即Animal的子类型;这样 AnimalShelter<Cat> 可以赋值给 AnimalShelter<Animal>

逆变(Contravariance)

父类型→子类型:越来越具体specific;参数类型:要相反的变化,要不变或越来越抽象。
逆变允许使用父类的对象来替换子类的对象。换句话说,如果类型 B 是类型 A 的子类型,并且 G 是一个泛型类型,那么 G<A> 可以被认为是 G<B> 的子类型。逆变通常用于方法参数。

示例:逆变

还是使用同样的动物类层次结构,但这次我们创建一个逆变的接口:

interface AnimalHandler<T> {
    void handle(T animal);
}

class CatHandler implements AnimalHandler<Cat> {
    @Override
    public void handle(Cat cat) {
        cat.makeSound();
    }
}

public class ContravarianceExample {
    public static void main(String[] args) {
        AnimalHandler<Animal> animalHandler = new AnimalHandlerImpl();
        // 由于逆变,AnimalHandler<Animal> 可以赋值给 AnimalHandler<Cat>
        AnimalHandler<? super Cat> catHandler = animalHandler;

        Cat cat = new Cat();
        catHandler.handle(cat); // 输出:Meow
    }
}

在这个例子中,AnimalHandler<in T> 表示 AnimalHandler 是逆变的,也就是说参数类型越来越泛化抽象,? super Cat即Cat的父类型;这样 AnimalHandler<Cat> 可以赋值给 AnimalHandler<Animal>

总结

  • 协变:允许子类型替换父类型,通常用于返回类型。
  • 逆变:允许父类型替换子类型,通常用于参数类型。

四、黑盒与白盒

黑盒白盒代码的复用

在这里插入图片描述

黑盒框架与白盒框架

在这里插入图片描述

  • 39
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值