目录
1.概述
2.编程范式
2.1.结构化编程
2.2.面向对象编程
2.3.函数式编程
3.设计原则
3.1.单一职责原则
3.2.开闭原则
3.3.里氏替换原则
3.4.接口隔离原则
3.5.依赖反转原则
4.小结
1.概述
软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。
工程师们忽视了一个自然规律:无论是从短期还是长期来看,胡乱编写代码的工作速度其实比循规蹈矩更慢。研发团队最好的选择是清晰地认识并避开工程师们过度自信的特点,开始认真地对待自己的代码架构,对其质量负责。要想跑得快,先要跑得稳。
软件software。“ware”的意思是“产品”,而“soft”的意思,不言而喻,是指软件的灵活性。
艾森豪威尔矩阵中,软件的系统架构——那些重要的事情——占据了该列表的前两位,而系统行为——那些紧急的事情——只占据了第一和第三位。重视软件系统架构。
2.编程范式
编程范式指的是程序的编写模式,与具体的编程语言关系相对较小。三个编程范式分别限制了goto语句、函数指针和赋值语句的使用。
2.1.结构化编程
Dijkstra发现了:goto语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元;Bohm和Jocopini证明人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。结构化编程主张用我们现在熟知的if/then/else语句和do/while/until语句来代替跳转语句的。
本质是:结构化编程对程序控制权的直接转移进行了限制和规范。在架构设计领域,功能性降解拆分仍然是最佳实践之一
2.2.面向对象编程
通常认为,面向对象的三大特点是封装、继承、多态。
封装:把一组相关联的数据和函数圈起来,使圈外面的代码只能看见部分函数,数据则完全不可见。虽然C语言是非面向对象的编程语言,但却是完美封装。
继承:让我们可以在某个作用域内对外部定义的某一组变量与函数进行覆盖。虽然C语言支持继承,但需要显式转换,不够完美。
多态:ML1和接口I在源代码上的依赖关系(或者叫继承关系),该关系的方向和控制流正好是相反的,我们称之为依赖反转。(实线为源代码依赖,虚线为控制流)。换句话说,无论我们面对怎样的源代码级别的依赖关系,都可以将其反转。因此,我们让用户界面和数据库都成为业务逻辑的插件。也就是说,业务逻辑模块的源代码不需要引入用户界面和数据库这两个模块,可以做到独立部署和开发。
因此,作者认为:面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力,这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。
本质是:面向对象编程对程序控制权的间接转移进行了限制和规范。
2.3.函数式编程
大部分函数式编程语言只允许在非常严格的限制条件下,才可以更改某个变量的值。函数式编程核心思想是将计算视作一系列函数的组合。它强调函数的纯粹性、不可变性和无副作用。以Java程序为例:本质是:函数式编程对程序中的赋值进行了限制和规范。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
//问题:假设我们有一个整数列表,我们要找到这个列表中的偶数,并将它们乘以2,然后将结果打印出来。
//解释:我们使用了Java 8的流API和Lambda表达式。
//首先,我们通过调用stream()方法将列表转换为流。然后,我们使用filter()方法过滤出偶数。在filter()方法中,我们传递了一个Lambda表达式,该表达式接受一个整数参数n,并检查n是否是偶数。接下来,我们使用map()方法将所有偶数乘以2。最后,我们使用collect()方法将结果收集到一个新的列表中,并使用forEach()方法打印每个元素。
public class FunctionalProgrammingExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenNumbersDoubled = numbers.stream()
.filter(n -> n % 2 == 0) // 过滤偶数
.map(n -> n * 2) // 将偶数乘以2
.collect(Collectors.toList()); // 收集结果到一个新的列表
evenNumbersDoubled.forEach(System.out::println); // 打印结果
}
}
3.设计原则
软件构建中层结构的主要目标为:可容忍被改动、更容易被理解、可在多个软件系统中复用的组件。
设计原则简述为:SOLID。分别为:SRP(单一职责原则)、OCP(开闭原则)、LSP(里氏替换原则)、ISP(接口隔离原则)和DIP(依赖反转原则)。
3.1.单一职责原则
单一职责原则是指一个类只负责一项职责。另外,我们可以将不同职责的代码分开,并将每个职责放在独立的方法中。
//在这个示例中,我们定义了一个UserService类和一个UserRepository类。UserService类负责处理用户相关的业务逻辑,如获取用户、添加用户、更新用户和删除用户等。UserRepository类负责与数据库交互,包括获取用户、添加用户、更新用户和删除用户等。这样,我们将业务逻辑和数据访问逻辑分开,并将它们分配到不同的类中,从而实现了单一职责原则。
//使用了以下单一职责原则的实现方法:
//1.一个类只负责一项职责:UserService类只负责处理用户相关的业务逻辑,UserRepository类只负责与数据库交互。这样可以使得每个类只负责一项职责,避免了类的职责模糊和不必要的复杂性。
//2.将业务逻辑和数据访问逻辑分开:UserService类和UserRepository类分别处理业务逻辑和数据访问逻辑。这样可以使得每个类的职责更加明确和专一,提高了代码的可读性、可维护性和可扩展性。
//在实现中,我们可以通过将业务逻辑和数据访问逻辑分开来实现单一职责原则;同时,我们还需要考虑类的职责和类的依赖关系,以便更好地组织和管理我们的代码。再者,我们可以将不同职责的代码分开,并将每个职责放在独立的方法中,这样可以使得代码更加简单、易读和易维护。
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(int id) {
return userRepository.getUserById(id);
}
public void addUser(User user) {
userRepository.addUser(user);
}
public void updateUser(User user) {
userRepository.updateUser(user);
}
public void deleteUser(int id) {
userRepository.deleteUser(id);
}
}
public class UserRepository {
private Connection connection;
public UserRepository(Connection connection) {
this.connection = connection;
}
public User getUserById(int id) {
// 调用数据库API获取用户信息
return null;
}
public void addUser(User user) {
// 调用数据库API添加用户信息
}
public void updateUser(User user) {
// 调用数据库API更新用户信息
}
public void deleteUser(int id) {
// 调用数据库API删除用户信息
}
}
3.2.开闭原则
开闭原则是指软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
如何实现呢?可以先将满足不同需求的代码分组(即SRP),然后再来调整这些分组之间的依赖关系(即DIP)(单一职责+依赖反转+单向依赖)。另外,如果A组件不想被B组件上发生的修改所影响,那么就应该让B组件依赖于A组件。
什么是高阶/低阶组件?高阶组件具有更高的复用性,因为它们可以用于多个组件,并且可以在不同的应用中使用。低阶组件则是实现具体功能的组件,它们通常只能在一个特定的应用中使用。
//我们定义了一个Shape接口,它有一个area()方法来计算形状的面积。然后,我们定义了一个Rectangle类和一个Circle类,它们都实现了Shape接口,并分别计算矩形和圆形的面积。最后,我们定义了一个AreaCalculator类,它接受一个Shape数组作为参数,并计算数组中所有形状的总面积。
//使用了以下开闭原则的实现方法:
//1.对扩展开放:我们通过定义Shape接口和Rectangle类和Circle类来实现对扩展开放。如果我们需要添加其他形状,我们只需要实现Shape接口,并创建一个新的类来计算该形状的面积即可。
//2.对修改关闭:我们通过AreaCalculator类来实现对修改关闭。我们不需要修改AreaCalculator类就可以添加新的形状,因为我们已经定义了Shape接口来表示所有形状,并且每个形状都实现了area()方法来计算面积。
//在实现中,我们可以通过定义接口、抽象类和使用基类等方式来实现对扩展开放;同时,我们可以使用策略模式、工厂模式和依赖注入等方式来实现对修改关闭。
public interface Shape {
double area();
}
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double area() {
return width * height;
}
}
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double area() {
return Math.PI * radius * radius;
}
}
public class AreaCalculator {
public double calculateTotalArea(Shape[] shapes) {
double totalArea = 0;
for (Shape shape : shapes) {
totalArea += shape.area();
}
return totalArea;
}
}
3.3.里氏替换原则
里氏替换原则是指子类能够替换其父类并且不会影响程序的正确性。
在实现中,我们可以通过继承、实现接口和使用抽象类等方式来实现里式替换原则;同时,我们还需要确保子类满足父类的所有行为和属性,并且不会增加或修改父类的行为。
//我们定义了一个Rectangle类,它有一个width和一个height属性,以及一个area()方法来计算矩形的面积。然后,我们定义了一个Square类,它继承自Rectangle类,并覆盖了setWidth()和setHeight()方法来确保正方形的宽度和高度相等。这样,我们可以使用Square类来代替Rectangle类,并且不会影响程序的正确性。
//使用了以下里式替换原则的实现方法:
//1.子类可以替换父类:Square类继承自Rectangle类,它可以替换Rectangle类的实例,因为它继承了Rectangle类的所有属性和方法,并且具有与Rectangle类相同的行为。
//2.不会影响程序的正确性:Square类覆盖了setWidth()和setHeight()方法来确保正方形的宽度和高度相等,并且保持了与Rectangle类相同的行为,因此在使用Square类替换Rectangle类时,不会影响程序的正确性。
//在实现中,我们可以通过继承、实现接口和使用抽象类等方式来实现里式替换原则;同时,我们还需要确保子类满足父类的所有行为和属性,并且不会增加或修改父类的行为。
public class Rectangle {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public double area() {
return width * height;
}
}
public class Square extends Rectangle {
public Square(double sideLength) {
super(sideLength, sideLength);
}
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width);
}
public void setHeight(double height) {
super.setWidth(height);
super.setHeight(height);
}
}
3.4.接口隔离原则
接口隔离原则要求接口设计应该精简单一,不应该包含多余的方法。
//我们定义了两个接口Worker和Eater,分别表示工作者和食客。然后,我们定义了一个Programmer类和一个Waiter类,它们都实现了Worker接口,并分别表示程序员和服务员。Waiter类还实现了Eater接口,表示服务员还是一个食客。最后,我们定义了一个Robot类,它也实现了Worker接口,表示机器人也是一个工作者。
//使用了以下接口隔离原则的实现方法:
//1.接口设计精简单一:Worker接口只包含work()方法,Eater接口只包含eat()方法。这样可以使接口设计更加精简,不会包含多余的方法,避免了接口冗余和不必要的复杂性。
//2.类只实现必要的接口:Programmer类只实现Worker接口,Waiter类实现了Worker和Eater接口,Robot类也实现了Worker接口。这样可以使得每个类只实现必要的接口,避免了不必要的依赖和复杂性。
//在实现中,我们可以通过定义精简的接口和让类只实现必要的接口来实现接口隔离原则;同时,我们还需要考虑接口的依赖关系和接口的易用性,以便更好地组织和管理我们的代码。
public interface Worker {
void work();
}
public interface Eater {
void eat();
}
public class Programmer implements Worker {
public void work() {
System.out.println("Programmer works.");
}
}
public class Waiter implements Worker, Eater {
public void work() {
System.out.println("Waiter works.");
}
public void eat() {
System.out.println("Waiter eats.");
}
}
public class Robot implements Worker {
public void work() {
System.out.println("Robot works.");
}
}
3.5.依赖反转原则
依赖反转原则是指依赖关系的建立应该多依赖于抽象而不是具体实现,通过构造函数注入依赖。源代码依赖方向永远是控制流方向的反转,这就是DIP被称为依赖反转原则的原因。
//在这个示例中,我们定义了一个Animal接口和两个实现类Dog和Cat,它们分别表示狗和猫。然后,我们定义了一个AnimalFeeder类,它依赖于Animal接口,而不是具体的实现类。AnimalFeeder类包含一个构造函数,它接受一个Animal类型的参数,并将它保存在类内部。AnimalFeeder类还有一个feed()方法,它调用了Animal接口的eat()方法来喂养动物。
//使用了以下依赖反转原则的实现方法:
//1.依赖关系建立于抽象而不是具体实现:AnimalFeeder类依赖于Animal接口,而不是具体的实现类。这样可以使得AnimalFeeder类与具体的实现类解耦,从而提高了代码的灵活性、可扩展性和可维护性。
//2.通过构造函数注入依赖:AnimalFeeder类通过构造函数接受一个Animal类型的参数,并将它保存在类内部。这样可以使得依赖关系的建立显式化,同时也可以方便进行依赖注入。
//在实现中,我们可以通过依赖注入和面向接口编程来实现依赖反转原则;同时,我们还需要考虑接口的设计和类的依赖关系,以便更好地组织和管理我们的代码。
public interface Animal {
void eat();
}
public class Dog implements Animal {
public void eat() {
System.out.println("Dog eats bones.");
}
}
public class Cat implements Animal {
public void eat() {
System.out.println("Cat eats fish.");
}
}
public class AnimalFeeder {
private Animal animal;
public AnimalFeeder(Animal animal) {
this.animal = animal;
}
public void feed() {
animal.eat();
}
}
依赖注入是实现依赖反转原则的一种技术手段,它可以将依赖关系的建立从类内部转移到外部。方式有:1.构造函数注入;2.Setter方法注入;3.接口注入。
//1.构造函数注入: 它通过类的构造函数接受依赖对象,并将其保存在类的成员变量中。
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// ...
}
//2.Setter方法注入:它通过类的Setter方法接受依赖对象,并将其保存在类的成员变量中。使得依赖注入更加灵活,但也容易导致类的职责模糊和Setter方法的滥用。
public class UserService {
private UserRepository userRepository;
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
// ...
}
//3.接口注入:它通过一个接口来定义依赖注入的方法,并由类实现该接口。
public interface UserRepositoryAware {
void setUserRepository(UserRepository userRepository);
}
public class UserService implements UserRepositoryAware {
private UserRepository userRepository;
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
// ...
}
4.小结
软件架构的目标是用最小的人力成本来满足构建和维护该系统的需求。要想跑得快,先要跑得稳。
编程范式有三种:结构化编程、面向对象编程和函数式编程。构化编程主张用我们现在熟知的if/then/else语句和do/while/until语句来代替goto等跳转语句。面向对象编程是以多态为手段来对源代码中的依赖关系进行控制的能力,这种能力让高层策略性组件与底层实现性组件相分离。函数式编程强调函数的纯粹性、不可变性和无副作用。
设计原则为SOLID原则:单一职责原则是指一个类只负责一项职责;开闭原则是指软件实体(类、模块、函数等)应该对扩展开放,对修改关闭;里氏替换原则是指子类能够替换其父类并且不会影响程序的正确性;接口隔离原则要求接口设计应该精简单一,不应该包含多余的方法;依赖反转原则是指依赖关系的建立应该多依赖于抽象而不是具体实现,通过构造函数注入依赖。