提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
一、软件复用的分类
软件复用可以从不同的层次进行划分,每个层次都有其独特的特性和应用场景。主要分为以下几类:
- 源代码级复用:直接复用代码片段。
- 模块级复用:通过类或接口进行复用。
- 库级复用:通过API或包进行复用。
- 系统级复用:通过框架进行复用。
源代码级复用
源代码级复用是最基本的复用形式,即将代码片段复制到新项目中。这种方法简单直接,但也有一些明显的缺点,如维护困难、代码冗余和潜在的错误风险。为了高效地进行源代码级复用,可以利用互联网中的代码搜索工具,如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!";
}
}
比较不同复用方法
不同层级的复用方法各有优缺点:
- 源代码级复用:简单直接,但维护困难。
- 模块级复用:灵活性高,但需要良好的设计。
- 库级复用:开发效率高,但依赖库的稳定性和文档。
- 系统级复用:提供完整解决方案,但学习成本高。
二、里氏替换原则 (LSP)
里氏替换原则由Barbara Liskov在1987年提出,它是SOLID五大面向对象设计原则中的第二个。LSP的核心思想是:如果S是T的子类型,那么类型为T的对象可以被类型为S的对象替换,而不会改变程序的正确性。换句话说,子类必须能够替换其父类,并且不会导致程序行为的不一致。
LSP的关键点
- 行为兼容性:
- 子类应实现父类的所有方法,但不应改变这些方法的预期行为;并且子类的方法不能引入新的异常类型或者破坏父类的方法约定。
- 接口一致性:
- 子类应保持父类的接口不变,包括方法的签名、输入参数和返回值类型。
- 语义一致性:
- 子类的方法实现应该与父类的方法具有相同的语义,即子类不应违背父类的业务逻辑。
遵循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>
的子类型。协变通常用于返回类型。
示例:协变
假设我们有一个动物类层次结构,其中 Cat
是 Animal
的子类:
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>
。
总结
- 协变:允许子类型替换父类型,通常用于返回类型。
- 逆变:允许父类型替换子类型,通常用于参数类型。