设计原则与思想: 面向对象 我们在讨论面向对象的时候,我们到底在谈论什么? 什么是面向对象编程和面向对象编程语言?
面向对象编程是一种编程范式或编程风格, 它以类或对象作为组织代码的基本单元, 并将封装, 抽象, 继承, 多态四个特性,作为代码设计和实现的基石
面向对象编程语言是支持类或对象的语法机制, 并由现成的语法机制,能方便地实现面向对象编程四大特性(封装, 抽象, 继承, 多态)的编程语言
面向对象语言和面向对象编程语言之间地关系
面向过程语言也可以实现面向对象编程
面向对象语言也会写出面向过程编程
如何判定一个编程语言是否面向对象编程语言 并没有严格地定义,面向对象语言要求宽泛,并不一定要求具有所有的四大特性, 只要某种编程语言支持类, 对象语法机制,那几本上就可以说这种编程语言是面向对象编程语言了
什么是面向对象分析和面向对象设计? 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(); } 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) { } char * format_to_text (struct User user) { } void sort_users_by_age (struct User users[]) { } void format_user_file (char * origin_file_path, char * new_file_path) { struct User users [1024]; int count = 0 ; while (1 ) { 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]); } } 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) { } public String formatToText () { } } public class UserFileFormatter { public void format (String userFile, String formattedUserFile) { List users = new ArrayList <>(); while (1 ) { User user = User.parseFrom(userText); users.add(user); } for (int i = 0 ; i < users.size(); ++i) { String formattedUserText = user.formatToText(); } } } 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
类, 那么大胆地去定义它吧(不要为了面向对象, 随意抽象一个父类) ,因为在面向对象编程中, 我们也并不是完全排斥面向过程风格的代码. 只要能为我们写好代码,贡献力量,我们就可以适度地去使用
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
是一个记录日志的抽象类,FileLogger
和MessageQueueLogger
继承Logger
, 分别实现两种不同的日志记录方式: 记录日志到文件中和记录日志到消息队列中.
FileLogger
和MessageQueueLogger
两个子类服用了父类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) { fileWriter.write(...); } } 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) { 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 { } } public class Application { 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
不再是抽象类,只是一个普通的父类,删除了Logger
中log()
、doLog()
方法, 新增了isLoggablet()
方法。FileLogger
和MessageQueueLogger
还是继承Logger
父类,以达 到代码复用的目的。具体的代码如下:
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 ; fileWriter.write(...); } } 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 ; 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) { } public class FileLogger extends Logger { @Override public void log (Level level, String mesage) { if (!isLoggable()) return ; fileWriter.write(...); } } public class MessageQueueLogger extends Logger { @Override public void log (Level level, String mesage) { if (!isLoggable()) return ; msgQueueClient.send(...); } }
但它显然没有之前抽象类的实现思路优雅
Logger中定义一个空的方法,可读性差
当创建一个新的子类继承Logger父类的时候, 我们又可能会忘记重新实现log()方法,之前的抽象类会强制子类重写log()
方法
Logger可以被实例化,增加类被误用的风险
其他语言即使不支持接口特性, 也可以模拟接口
为什么基于接口而非实现编程? 有必要为每个类都定义接口吗?