设计原则与思想

设计原则与思想: 面向对象

我们在讨论面向对象的时候,我们到底在谈论什么?

什么是面向对象编程和面向对象编程语言?

  • 面向对象编程是一种编程范式或编程风格, 它以类或对象作为组织代码的基本单元, 并将封装, 抽象, 继承, 多态四个特性,作为代码设计和实现的基石
  • 面向对象编程语言是支持类或对象的语法机制, 并由现成的语法机制,能方便地实现面向对象编程四大特性(封装, 抽象, 继承, 多态)的编程语言

面向对象语言和面向对象编程语言之间地关系

  • 面向过程语言也可以实现面向对象编程

  • 面向对象语言也会写出面向过程编程

如何判定一个编程语言是否面向对象编程语言

并没有严格地定义,面向对象语言要求宽泛,并不一定要求具有所有的四大特性, 只要某种编程语言支持类, 对象语法机制,那几本上就可以说这种编程语言是面向对象编程语言了

什么是面向对象分析和面向对象设计?

OOA, OOD, OOP 软件开发流程

面向对象分析就是搞清楚做什么, 面向对象设计就是搞清楚怎么做,. 两个阶段地产出就是类的设计,包括程序被拆解为哪些类, 每个类有哪些属性方法,类与类之间如何交互等等

封装, 抽象, 继承,多态可以解决哪些编程问题?]

封装特性

封装也叫信息隐藏或者数据访问保护. 类通过暴露有限的访问接口,授权外部仅能通过类提供的范式来访问内部信息或者数据.

public class Wallet {
private String id;
private long createTime;
private BigDecimal balance;
private long balanceLastModifiedTime;
// ...省略其他属性...

public Wallet() {
this.id = IdGenerator.getInstance().generate();
this.createTime = System.currentTimeMillis();
this.balance = BigDecimal.ZERO;
this.balanceLastModifiedTime = System.currentTimeMillis();
}

// 注意:下面对get方法做了代码折叠,是为了减少代码所占文章的篇幅
public String getId() { return this.id; }
public long getCreateTime() { return this.createTime; }
public BigDecimal getBalance() { return this.balance; }
public long getBalanceLastModifiedTime() { return this.balanceLastModifiedTime; }

public void increaseBalance(BigDecimal increasedAmount) {
if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
this.balance.add(increasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}

public void decreaseBalance(BigDecimal decreasedAmount) {
if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
if (decreasedAmount.compareTo(this.balance) > 0) {
throw new InsufficientAmountException("...");
}
this.balance.subtract(decreasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}

它需要编程语言提供权限访问控制语法来支持, 例如Java中的public protected, public 关键字

封装:

  • 保护数据不被随意修改 ,提高代码的可维护性
  • 仅暴露有限的必要接口,提高类的易用性(不需要调用者了解过多的细节) 冰箱的按钮多少,出错的概率

抽象特性

抽象讲解如何隐藏方法的具体实现, 让使用者只需要关心方法提供了哪些功能, 不需要知道这些功能如何实现.

抽象通过接口类或者抽象类来实现, 但也不需要特殊的语法机制来支持,命名时也要有抽象思维,getAliyunPicture() ---- > getPicture()

抽象存在的意义:

  • 提高代码的可扩展性,维护性,修改实现不需要改变定义,减少代码改动的范围
  • 处理复杂系统的有效手段,能有效过滤不必关注的信息

继承特性

继承用来表示类之间的is-a关系,分为两种模式

  • 单继承
  • 多继承

为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持

继承存在的意义:

  • 继承主要用来解决代码复用的问题(也可以用组合来实现),特定情况下用继承的is-关系非常符合人类的思维方式,从设计来说也有一种设计美感

​ 过度使用继承时错误的,例如在阅读代码时过多层次会增加代码阅读负担, 同时继承的复用会导致子类和父类耦合过高,继承特性很受争议

多态特性

三个语法机制实现多态

  • 语法要支持父类引用可以引用子类对象
  • 语法要支持继承
  • 语法要支持子类重写父类方法

除了"继承加方法重写"还有接口类语法 , duck-typing语法(动态语言支持)

接口类语法

public interface Iterator {
boolean hasNext();
String next();
String remove();
}

public class Array implements Iterator {
private String[] data;

public boolean hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}

public class LinkedList implements Iterator {
private LinkedListNode head;

public boolean hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}

public class Demo {
private static void print(Iterator iterator) {
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}

public static void main(String[] args) {
Iterator arrayIterator = new Array();
print(arrayIterator);

Iterator linkedListIterator = new LinkedList();
print(linkedListIterator);
}
}

duck-typing

class Logger:
def record(self):
print(“I write a log into file.”)

class DB:
def record(self):
print(“I insert data into db. ”)

def test(recorder):
recorder.record()

def demo():
logger = Logger()
db = DB()
test(logger)
test(db)

多态的意义:

  • 提高代码扩展性和复用性,很多设计模式, 设计原则, 编程技巧的代码实现基础

面向对象相比面向过程有哪些优势? 面向过程真的过时了吗?

总有人拿面向对象语言写面向过程风格的代码

什么是面向过程编程与面向过程编程语言?

面向对象定义

  • 面向对象过程编程是一种编程范式, 是以类和对象作为基本单元, 以封装, 抽象, 继承,多态为基石来设计和实现代码
  • 面向对象编程语言是支持类和对象的语法机制,并有现成的语法机制, 能方便地实现面向对象编程地四大特性地编程语言

面向过程定义

  • 面向过程编程也是一种编程范式,它以过程(方法, 函数, 操作)作为组织代码的基本单元, 以数据(成员变量, 属性)与方法相分离为最主要的特点.面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能
  • 面向过程编程语言,最大特点是不支持类和对象两个语法概念,不支持丰富的面向对象的编程特性,仅支持面向过程编程

假设我们有一个记录了用户信息的文本文件users.txt,每行文本的格式是name&age&gender(比如,小王&28&男)。我们希望写一个程序,从users…txt文件中逐行读取用户信息,然后格式化成
name\tage\tgender(其中,\t是分隔符)这种文本格式,并且按照age从小到大排序之后,
重新写入到另一个文本文件formatted_.users…txt中,

用面向过程和面向对象两种编程风格,编写出来的代码有什么不同。

面向过程

struct User {
char name[64];
int age;
char gender[16];
};

struct User parse_to_user(char* text) {
// 将text(“小王&28&男”)解析成结构体struct User
}

char* format_to_text(struct User user) {
// 将结构体struct User格式化成文本("小王\t28\t男")
}

void sort_users_by_age(struct User users[]) {
// 按照年龄从小到大排序users
}

void format_user_file(char* origin_file_path, char* new_file_path) {
// open files...
struct User users[1024]; // 假设最大1024个用户
int count = 0;
while(1) { // read until the file is empty
struct User user = parse_to_user(line);
users[count++] = user;
}

sort_users_by_age(users);

for (int i = 0; i < count; ++i) {
char* formatted_user_text = format_to_text(users[i]);
// write to new file...
}
// close files...
}

int main(char** args, int argv) {
format_user_file("/home/zheng/user.txt", "/home/zheng/formatted_users.txt");
}

面向对象

 public class User {
private String name;
private int age;
private String gender;

public User(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}

public static User praseFrom(String userInfoText) {
// 将text(“小王&28&男”)解析成类User
}

public String formatToText() {
// 将类User格式化成文本("小王\t28\t男")
}
}

public class UserFileFormatter {
public void format(String userFile, String formattedUserFile) {
// Open files...
List users = new ArrayList<>();
while (1) { // read until file is empty
// read from file into userText...
User user = User.parseFrom(userText);
users.add(user);
}
// sort users by age...
for (int i = 0; i < users.size(); ++i) {
String formattedUserText = user.formatToText();
// write to new file...
}
// close files...
}
}

public class MainApplication {
public static void main(String[] args) {
UserFileFormatter userFileFormatter = new UserFileFormatter();
userFileFormatter.format("/home/zheng/users.txt", "/home/zheng/formatted_users.txt");
}
}

面向对象编程相比面向过程编程有哪些优势

  • 对于大规模程序的开发,程序的处理流程并非单一主线, 而是错综复杂的玩转结构.面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发

  • 面向对象编程丰富的特性.利用这些特性编写出来的代码,更易扩展,易维护,易复用

  • 编程语言和机器打交道的方式的演进规律中,我们可以总结出: 面向对象编程语言比面向过程编程语言,更加人性化,更加高级,更加智能

哪些代码设计看似是面向对象,实际是面向过程的?

滥用getter,setter方法

​ 违反封装特性,将面向对象编程风格退化为面向过程编程风格

在设计类时尽量不要给属性定义setter方法, 除此之外,尽管getter方法相对于setter方法要安全,但是如果返回的是集合容器,也要防范集合内部数据被修改的危险

滥用全局变量(Constants)和全局方法(Utils)

将数据和操作分离, 不是面向对象,但也有一定好处

Utils改进策略

​ 判定表标准: 在定义Utils类之前, 你要问一下自己,:

  • **你真的需要单独定义这样一个Utils类吗? **

  • 是否可以把Utils类中地某些方法定义到其它类中呢?

​ 回答完这些问题,你还是觉得确实有必要去定义这样一个Utils类, 那么大胆地去定义它吧(不要为了面向对象, 随意抽象一个父类) ,因为在面向对象编程中, 我们也并不是完全排斥面向过程风格的代码. 只要能为我们写好代码,贡献力量,我们就可以适度地去使用

Constants改进策略

​ 当Constants类中包含很多常量定义的时候,依赖这个类的代码就会很多, 每次修改Constants类,都会导致依赖它的类文件重新编译,因此会浪费很多不必要的编译时间, 对于一个非常大的工程项目,编译一次救药花费几分钟,甚至几十分钟, 每次进行单元测试都会出发一次编译的过程,编译过程会影响我们的开发效率

改进Constants类 两种思路:

  • Constants拆分为功能更加单一的多个类
  • 不单独设计Constants常量类,而是哪个类用到了某个常量,将常量定义到这个类中(让我想到了注解)

Constants类, Utils类的设计问题, 对于这两种类的设计, 我们尽量能做到职责单一,定义一些细化到的类, 比如有RedisConstatns, FileUtils, 而不是定义一个大而全的Constants类, Utils类. 除此之外,如果能将这些类中的属性和方法, 划分归并到其他业务中, 那是最好不过的了, 能极大地提高类的内举行和代码地可复用性

定义数据和方法分离的类

	传统`MVC`结构分为`Model`层, `Controller`层, `View`层这三层

​ 逻辑上分为 Controller层, Service层, Repository

​ 数据上定义VO(View Object) BO(Business Object) Entity

这是典型的面向过程的编程风格

​ 这种开发模式叫做基于贫血模型的开发模型, 是我们现在非常常用的一种Web项目的开发模式.(日后会讲)

​ 面向对象编程比面向过程编程难一些,不经意就写成面向过程了

​ 我们在写一些微小程序时,或者一个数据处理相关的代码, 以算法为主, 数据为辅, 脚本是的面向过程编码风格就更加适合一些,而且面向过程本身就是面向对象的基础, 面向对象编程离不开基础的面向过程编程.

​ 不管使用面向对象还是面向过程哪种风格来写代码, 我们最终的目的还是写出易维护, 易读, 易复用, 易扩展的高质量代码. 只要能控制面向过程编程风格的一些弊端, 控制好它的副作用, 在掌握范围内为我们所用, 我们大可不避讳在面向对象编程中写面向过程风格的代码

优秀评论(笑死看不懂)

1.用she实现自动化脚本做的服务编排,一般都是面向过程,一步一步的。而k8s的编排却是
面向对象的,因为它为这个顺序流抽象出了很多角色,将原本一步一步的顺序操作转变成了多
个角色间的轮转和交互。
2.从接触ddd才走出javaer举面向对象旗,干面向过程勾当的局面。所谓为什么“充血模型”不流
行,我认为不外呼两个。一,规范的领域模型对于底层基础架构来说并不友好(缺少stge
t),所以会导致规范的领域模型与现有基础架构不贴合,切很难开发出完全贴合的基础架
构,进而引深出,合理的业务封装却阻碍关于复用通用抽象的矛盾。二,合理的业务封装,需
要在战略上对业务先做合理的归类分割和抽象。而这个前置条件很少也不好达成。进而缺少前
置设计封装出来的“充血模型”会有种四不像的味道,反而加剧了业务的复杂性,还不如“贫血
模型”来得实用。事实上快节奏下,前置战略设计往往都是不足的,所以想构建优秀的“充血模
型”架构,除了要对业务领域和领域设计有足够的认知,在重构手法和重构意愿上还要有一定
讲究和追求,这样才能让项目以“充血模型”持续且良性的迭代。
3.“充血模型”相对于“贫血模型”有什么好处?从我的经验来看,可读性其实可能“贫血模型”还好
一点,这也可能有思维惯性的原因在里面。但从灵活和扩展性来说“充血模型”会优秀很多,因
为好的“充血模型"”往往意味着边界清晰(耦合低),功能内敛(高内聚)。

接口vs抽象类的区别? 如何用普通的类模拟抽象类和接口?

C++不支持抽象类, python不支持抽象类也不支持接口

什么是抽象类和接口? 区别在哪里?

典型的抽象类的使用场景(模板设计模式)

Logger是一个记录日志的抽象类,FileLoggerMessageQueueLogger继承Logger, 分别实现两种不同的日志记录方式: 记录日志到文件中和记录日志到消息队列中.

FileLoggerMessageQueueLogger两个子类服用了父类Logger中的name, enabled, minPermittedLevel属性和log()方法, 但因为这两个子类写日志的方式不同, 它们又各自重写了父类中的doLog()方法

抽象类:

// 抽象类
public abstract class Logger {
private String name;
private boolean enabled;
private Level minPermittedLevel;

public Logger(String name, boolean enabled, Level minPermittedLevel) {
this.name = name;
this.enabled = enabled;
this.minPermittedLevel = minPermittedLevel;
}

public void log(Level level, String message) {
boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
if (!loggable) return;
doLog(level, message);
}

protected abstract void doLog(Level level, String message);
}
// 抽象类的子类:输出日志到文件
public class FileLogger extends Logger {
private Writer fileWriter;

public FileLogger(String name, boolean enabled,
Level minPermittedLevel, String filepath) {
super(name, enabled, minPermittedLevel);
this.fileWriter = new FileWriter(filepath);
}

@Override
public void doLog(Level level, String mesage) {
// 格式化level和message,输出到日志文件
fileWriter.write(...);
}
}
// 抽象类的子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {
private MessageQueueClient msgQueueClient;

public MessageQueueLogger(String name, boolean enabled,
Level minPermittedLevel, MessageQueueClient msgQueueClient) {
super(name, enabled, minPermittedLevel);
this.msgQueueClient = msgQueueClient;
}

@Override
protected void doLog(Level level, String mesage) {
// 格式化level和message,输出到消息中间件
msgQueueClient.send(...);
}
}

定义接口:

// 接口
public interface Filter {
void doFilter(RpcRequest req) throws RpcException;
}
// 接口实现类:鉴权过滤器
public class AuthencationFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...鉴权逻辑..
}
}
// 接口实现类:限流过滤器
public class RateLimitFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...限流逻辑...
}
}
// 过滤器使用Demo
public class Application {
// filters.add(new AuthencationFilter());
// filters.add(new RateLimitFilter());
private List<Filter> filters = new ArrayList<>();

public void handleRpcRequest(RpcRequest req) {
try {
for (Filter filter : filters) {
filter.doFilter(req);
}
} catch(RpcException e) {
// ...处理过滤结果...
}
// ...省略其他处理逻辑...
}
}

​ 抽象类实际就是类, 只不过是一种特殊的类,这种类不能被实例化对象, 只能被子类继承. 继承关系是一种is-a关系来说, 接口表示一种 has-a关系, 表示具有某些功能, 对于接口, 有一种更加抽象的叫法,那就是协议(contract).

抽象类和接口能解决什么编程问题?

抽象类是为了代码复用而生的

但普通类也可以实现代码复用, 那么抽象类有什么独特作用吗?

抽象类是 is-a

接口时 has-a

我们还是拿之前那个打印日志的例子来讲解。我们先对上面的代码做下改造。在改造之后的代
码中,Logger不再是抽象类,只是一个普通的父类,删除了Loggerlog()doLog()方法,
新增了isLoggablet()方法。FileLoggerMessageQueueLogger还是继承Logger父类,以达
到代码复用的目的。具体的代码如下:

// 父类:非抽象类,就是普通的类. 删除了log(),doLog(),新增了isLoggable().
public class Logger {
private String name;
private boolean enabled;
private Level minPermittedLevel;

public Logger(String name, boolean enabled, Level minPermittedLevel) {
//...构造函数不变,代码省略...
}

protected boolean isLoggable() {
boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
return loggable;
}
}
// 子类:输出日志到文件
public class FileLogger extends Logger {
private Writer fileWriter;

public FileLogger(String name, boolean enabled,
Level minPermittedLevel, String filepath) {
//...构造函数不变,代码省略...
}

public void log(Level level, String mesage) {
if (!isLoggable()) return;
// 格式化level和message,输出到日志文件
fileWriter.write(...);
}
}
// 子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {
private MessageQueueClient msgQueueClient;

public MessageQueueLogger(String name, boolean enabled,
Level minPermittedLevel, MessageQueueClient msgQueueClient) {
//...构造函数不变,代码省略...
}

public void log(Level level, String mesage) {
if (!isLoggable()) return;
// 格式化level和message,输出到消息中间件
msgQueueClient.send(...);
}
}

这个设计思路虽然达到了代码复用的目的,但是无法使用多态特性了。像下面这样编写代码,
就会出现编译错误,因为Logger中并没有定义log()方法。

Logger logger = new FileLogger("access-log", true, Level.WARN, "/users/wangzheng/access.log");
logger.log(Level.ERROR, "This is a test log message.");

你可能会说,这个问题解决起来很简单啊。我们在Logger父类中,定义一个空的Iog()方法,
让子类重写父类的1og ()方法,实现自己的记录日志的逻辑,不就可以了吗?

public class Logger {
// ...省略部分代码...
public void log(Level level, String mesage) { // do nothing... }
}
public class FileLogger extends Logger {
// ...省略部分代码...
@Override
public void log(Level level, String mesage) {
if (!isLoggable()) return;
// 格式化level和message,输出到日志文件
fileWriter.write(...);
}
}
public class MessageQueueLogger extends Logger {
// ...省略部分代码...
@Override
public void log(Level level, String mesage) {
if (!isLoggable()) return;
// 格式化level和message,输出到消息中间件
msgQueueClient.send(...);
}
}

但它显然没有之前抽象类的实现思路优雅

  • Logger中定义一个空的方法,可读性差
  • 当创建一个新的子类继承Logger父类的时候, 我们又可能会忘记重新实现log()方法,之前的抽象类会强制子类重写log()方法
  • Logger可以被实例化,增加类被误用的风险

其他语言即使不支持接口特性, 也可以模拟接口

为什么基于接口而非实现编程? 有必要为每个类都定义接口吗?