Skip to content

Latest commit

 

History

History
 
 

OOD 学习资源

针对面试

  • 面向对象设计基础,部分参考链接
    • 类/对象基础知识
      • 继承、接口、多态、组合、封装、抽象
      • hashCode()、equals() 函数
    • 设计模式 Design Pattern - 可复用面向对象软件的基础
    • 简单的数据结构以及算法
      • 数组、线性表
      • FIFO 用队列、FILO 用栈
      • KV、去重或缓存用哈希表
      • 数据类型选用(比如财务数据用 BigDecimal)
    • 数据关系:一对一、一对多、多对多
    • 并发:静态/非静态的线程安全/线程不安全的对象/变量、锁、CAS、并发相关设计模式
    • 设计原则
      • DRY (Don't Repeat Yourself)
        • 创建可复用的组件:将通用功能封装成可复用的组件,例如库、包、模块、函数和方法。这样,其他部分的代码可以重复使用这些组件而不必重新实现相同的功能,如果需要更改逻辑,也只需在一个地方进行修改。
          • 模板和代码生成:使用模板引擎或代码生成工具,以减少手动编写重复代码的工作。这对于生成相似的代码结构非常有用。
        • 维护一致性:确保相同的逻辑在整个代码库中保持一致。如果发现某处逻辑需要修改,确保在所有使用相同逻辑的地方都进行相应的更新。
      • SOLID(以下链接皆为示例代码)
        • S 单一功能原则: 对象或类应该仅具有一种单一功能(关注点分离思想)。
        • O 开闭原则: 对象或实体应该对扩展开放,对修改封闭。
        • L 里氏替换原则: 任何一个子类或派生类应该可以替换它们的基类或父类(参考契约式设计)(通常与 I 接口隔离原则互为表里)。
        • I 接口隔离原则: 多个特定客户端接口要好于一个宽泛用途的接口(不应该强迫一个类实现它用不上的接口)(通常与 L 里氏替换原则互为表里)。
        • D 依赖反转原则: 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。High level 实体不应该依赖于 Low level 实体。
      • IOC(控制反转)
        • DI 依赖注入 - 依赖注入是一种实现控制反转的具体方式,它通过将一个组件的依赖传递给它,而不是让组件自己创建或查找依赖。这通常通过构造函数注入、方法注入或属性注入来实现。依赖注入的目的是将组件解耦,使得组件不需要关心如何获取它所依赖的其他组件,而是通过外部注入的方式获取。
      • 复杂性管理
        • 封装:将内部实现细节对外部隐藏,只提供有限的接口供外部使用。
        • 抽象:通过筛选只保留共有的关键属性(特征或行为),形成一个概念上的模型或接口。
        • 关注点分离:每个部分都解决一个单独的问题。
        • 高内聚性:将相似的功能或责任归组到同一个模块或组件中。高内聚性有助于确保模块内部的代码是相关且一致的,提高了模块的可读性和可维护性。
        • 低耦合性:减少系统中各个模块或组件之间的强依赖关系(级联)。低耦合性的设计有助于提高系统的灵活性、可维护性和可扩展性。当各个模块之间的耦合性较低时,修改其中一个模块不太可能引起对其他模块的影响,从而降低了变更的风险。
    • OOD 面试应对方法
      • 6 步解题法
        • Clarify requirement - 问清楚那些是必须的,那些可以不用考虑
        • Define class (core object) - 整个时间中一共会出现那些类?
        • Define field (properties) - 每个类有那些filed?
        • Define method, how data flow works - 这些类与类之间是如何交互的? (inheritance为了好扩展, abstraction)
        • Implementing 核心关键 method - above all no implementation, only definition
        • Optimize with design pattern - 炫技时间,trade off show time 注意这是锦上添花,不写也可以过
      • 5C 解题法
    • OOD 系统分类
      • 管理类(停车场问题 Parking Lot)
      • 预定类(餐厅管理问题 Restaurant、酒店预订系统设计 Hotel Reservation)
      • 实物类(Vending Machine 自动售货机、Coffee Maker 咖啡机、Kindle 设计)
      • 游戏棋牌类(Tictactoe、Chinese Chess、Black Jack)

其他重要资源:

详细掌握 OOD 的基本方法与步骤

前置知识点:一些基本设计分工

参考链接 1
参考链接 2

  • Use Case Diagram - 即小人图,关心主要对象之间的互动(着重 Communication 与 Abstraction)。
  • ER Diagram 数据库图,只关注类、对象的数据以及类、对象互相间的关系(比如一对多关系,不包含互动关系)。
  • Class Diagram 类似 ER Diagram,但是还多了对接口、类、对象之间依赖、继承、组成、调用(关联)、互动(动作)、实现等等关系描述。
  • Sequence Diagram - 样子类似 TCP 协议握手解释图(Use Case Diagram 与 Sequence Diagram 的区别是前者描述最高、抽象层级的系统功能与用例/用况,是纯产品设计最高层;后者包括更多实际流程的考虑,是产品设计的底层并考虑、包括许多业务领域、工程领域的专业程序与逻辑)。
  • Activity Diagram、State Diagram - 类比状态机的图解(Sequence Diagram 与 Activity Diagram 的区别是前者关注在某个流程里对象之间的互动走向;后者关注于某个或全部流程里系统、应用的状态之间的互动走向),亦即 Flow Charts(类似编程语言教程的 if else 程序判断流程图)。
  • Swimlane Diagram - 类似 Sequence Diagram 与 Activity Diagram 的结合。

OOD 通常不需要画 ER Diagram,除非要把数据库也考虑进去。以上 Diagram 均为 UML 所包含。

画图有用,但是没必要严格遵守 UML 规范。Uber 的架构师对 UML 以及其它「标准化」架构方法的评价:不用任何标准化的软件架构工具。不用 UML,我们画了许多图,但是没有一个按照严格的标准绘制。只用最朴素的框图和箭头。 链接

做程序设计的时候还有人画 UML 图吗?会画,不过不是专业的那种。只是很随意。也就写明:实体类名,类注释;继承关系;表关联关系,强关联或者弱关联;每个主要类,实现了怎样的功能之类的。最后留在项目目录里。没人强制要求,也并不规范。目的是,一方面,自己将来看到的时候能回忆起来这个项目的相关内容;另一方面,也是为了将来别人接手这个项目的时候能有个简单参照。周围画 UML 的不多,一方面是这玩意需要学,忘了的时候还要去回忆那些箭头,虚线之类的规范。另一方面,是需求变化太快,uml 更新来不及。不过不需要那么规范,能配合着代码阅读就好。而且还可以辅以文字。例如:不知道继承是怎样的箭线,那就随便画一条箭线,然后旁边写个 extends,相信这样 90% 的开发人员都能看明白了。关于需求变更也同理,就算需求变更,至少表关系不会有太大的变化。有大的更新的时候再更新一下 uml 就好。 链接

OOD 基本步骤

基本还是应该参考上面的 Object Oriented Design 实战步骤,下面再总结、补充一下这些步骤:

  1. 类的认定 - OOD 中关于类的认定与 OOA 中关于对象的认定有着密切关系。但是 OOD 中对类的认定,不能像 OOA 中那样以准确反映问题空间为衡量准则,更多的要考虑通过对类以及类层次结构的认定,寻找解空间的基本结构,并为实现提供有效的支持。以下这些准则有助于更好的认定、定义类与方法:
    1. 对于问题空间中自然出现的实体,用类进行模型化;
    2. 将方法设计成单用途的;
    3. 如果需要对已有方法进行扩展,就设计一个新的方法;
    4. 避免冗长的方法;
    5. 把那些为多个方法或某个子类所需要的数据,存贮存实例变量中;
    6. 为类库设计,不要只为你自己或者你目前的应用设计
  2. 类的设计 - 在任何的面向对象应用中,类实例是系统的主要部分,而且如果采用纯面向对象的方法,那么整个系统就是由类实例组成的。因此,每个独立的类的设计对整个应用系统都有影响。在进行类的设计时,应考虑下面一些因素:
    1. 类的公共接口的单独成员应该是类的操作符;
    2. 类 A 的实例不应该直接发送消息给类 B 的成分;
    3. 操作符是公共的当且仅当类实例的用户可用;
    4. 属于类的每个操作符要么访问要么修改类的某个数据;
    5. 类必须尽可能少地依赖其他类;
    6. 两个类之间的互相作用应该是显式的;
    7. 采用子类继承超类的公共接口,开发子类成员为超类的特化;
    8. 继承结构的根类应该是目标概念的抽象模型。
  3. 类层次结构的组织 - OOD 中类层次结构的组织与 OOA 采用的策略是相似的,但在涉及递增开发时将有不同。支持重用是 OOD 的主要任务,继承机制支持两种层次的重用。在高层设计阶段,继承性可用作泛化特化关系的建模工具。使用继承机制促进开发出有意义的高级抽象,进而有助于重用。继承关系的重用性使得设计者能够在抽象中识别一般性,并从一般产生高级抽象。通过识别这种一般性,并把它从的较高的抽象中移出来,它就在当前或今后的设计中变成可重用。在详细设计阶段,继承性支持已有类作为新定义类的重用基础,可以把已有的部分代码复制到新子类中并修改,以适应其新的目的。继承性在已有类和新的类之间建立了一种依赖关系,子类的新代码不引起旧代码失效,继承的代码被自动地包含在新定义中,并作为新类的定义被编译。对已有的类的任何修改都被归并到下次编译的新类中。
  4. 类模块之间的接口技术 - 类之间的接口是中的一个关键,接口的方法大致有以下几类:
    1. 通过继承机制实现类之间的接口 - 第一种方法是可定义两层或多层:描述接口的通用类以及提供各种实现的子类(例如以列表作为通用类,以堆栈,队列等作为列表的实现),从而实现同一接口,不同实现的接口方法。第二种方法使用继承机制实现类模块接口对称目的:采用几种接口到基本模块中,通过继承的正交性与输出机制来实现此方法。通用类不作输出,而多个子类执行不同的输出。例如银行的账目作为通用类,而由不同的用户来实现对它的查询。
    2. 使类实例具有人工智能的状态机和主动数据结构 - 在定义类实现抽象数据类型及数据抽象时,将这些抽象设置于“主动”方式。也就是说,类实例不仅作为信息的被动集合,而且可看作具有内部状态及局部存储的状态机。这为类之间接口提供了有用的方法。
  5. 对类库和应用构架的支持 - OOD 的最终目标是把方法和实例变量放在类库中抽象层次尽可能高的类中,一个方法在类库的类层次结构中的层次越高,能够共享这个方法的子类就越多,以这种方式进行设计,就使重用达到了最大的可能限度。由于类库的目标是支持重用,所以纳入类库的类层次结构必须仔细加以推敲。这里主要指从有利于重用的角度来设计。尽管这方面还没有形式化的方法论,因而也没有完全自动化的工具,但可从下面 3 个方面着手:
    1. 改善标准的协议,在面对象系统中,消息传递是对象之间通信的唯一方式,从通信的角度来看,消息的内容便是对象之间的通信协议。应提高协议的标准化程度,如:为相应的方法设计一致的接口;限制消息中的参数个数;简化方法的功能等。
    2. 提高类的抽象程度,对于一个健全的类库来说,它的层次结构在进行若干层次的子类设计后,应当是深而窄的。这是因为,如果类层次结构中的层次较多。而每一层上的类少,就表明对象的共有特性经过了比较细致的分层次抽象,使用类的特殊性逐渐增强,因而能够提供较多的、在特定应用范畴内可普遍适用的类。
    3. 认定和培育构架,类库中的类就象一般建筑预制件,可以复杂到整个单元居室,也可以简单到梁柱,规格比较标准,容易被独立使用。但需要应用开发人员自己根据应用特征进行组装,因此类库本身并不是重用的基本单位。相对地,构架则是以构件之间有密切的联系为特征,面向特定的应用范畴,以整个构架而不是其中的单个构件来体现它的能量,因此构架本身是重用的基本单位,一旦与应用特征相符,就可以整体被重用。所以,构架是 OOD 是理想的目标。

OOD 与系统设计的区别

软件工程面试(主要)侧重于面试中的编码和软件设计技能。数据结构和算法轮检查候选人的问题解决技能和编码技能,而设计轮测试系统设计技能,可以是高级设计(High Level Design - HLD)或低级设计(Low Level Design - LLD)。
LLD 讨论类图以及给定系统的类、程序规范和其他低级细节之间的方法和关系。它也被称为面向对象设计(OOD)。

候选人的期望
在 LLD 面试中,他们将通过应用面向对象的设计原则和设计模式,根据你创建模块化、灵活、可维护和可重用软件的知识来评判你。这些问题(如设计停车场、设计国际象棋游戏等)是为了证明你了解如何创建优雅、可维护的面向对象代码。这些问题(有意)是非结构化和开放式的,并且没有标准答案。

如何准备 LLD 面试

  1. 至少学习一门面向对象语言(C++/Java/Python 或 C#)
  2. SOLID 等面向对象原理研究
  3. 学习所有常见的设计模式及其应用
  4. 探索一些开源项目并尝试了解最佳实践
  5. 练习常见的 LLD 面试问题

如何解决面试中的 LLD 问题

  1. 通过提出相关问题来澄清问题。收集完整的需求并从基本功能开始
  2. 定义核心类(和对象)
  3. 通过观察类/对象之间的交互来建立类/对象之间的关系
  4. 尝试通过定义方法来满足所有要求
  5. 应用面向对象的设计原则和设计模式,使系统可维护和可重用
  6. 编写结构良好的干净代码(如果被告知要实现一个功能)

Books:《Head first object-oriented analysis and design》《Head first design patterns》《Clean Code》《Clean Architecture》《Refactoring: Improving the Design of Existing Code》《Patterns of Enterprise Application Architecture》《Design Patterns: Elements of Reusable Object-Oriented Software》

以上引用自:https://www.linkedin.com/pulse/cracking-he-low-level-design-lld-interview-shashi-bhushan-kumar/

其他面向对象设计原则

  • CARP(Composition/Aggregation Reuse Principle),设计者首先应当考虑复合/聚合,而不是继承。这个就是所谓的 Favor Composition over Inheritance,在实践中复合/聚合会带来比继承更大的利益,所以要优先考虑。
  • LoD or LKP(Law of Demeter or Least Knowlegde Principle),迪米特法则或最少知识原则,这个原则首次在 Demeter 系统中得到正式运用,所以定义为迪米特法则。即一个对象应当尽可能少的去了解其他对象。也就是又一个关于如何松耦合(Loosely-Coupled)的法则。
  • 61 条面向对象设计的经验原则(来源于《OOD启思录》,不必严格遵守这些原则,违背它们也不会被处以宗教刑罚。但应当把这些原则看成警告,若违背了其中的一条,警告就会响起。)
    1. 所有数据都应该隐藏在所在的类的内部。
    2. 类的使用者必须依赖类的共有接口,但类不能依赖它的使用者。
    3. 尽量减少类的协议中的消息。
    4. 实现所有类都理解的最基本公有接口(例如,拷贝操作 - 深拷贝和浅拷贝、相等性判断、正确输出内容、从ASCII描述解析等等)。
    5. 不要把实现细节(例如放置共用代码的私有函数)放到类的公有接口中。如果类的两个方法有一段公共代码,那么就可以创建一个防止这些公共代码的私有函数。
    6. 不要以用户无法使用或不感兴趣的东西扰乱类的公有接口。
    7. 类之间应该零耦合,或者只有导出耦合关系。也即,一个类要么同另一个类毫无关系,要么只使用另一个类的公有接口中的操作。
    8. 类应该只表示一个关键抽象。包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包影响,则将对包中的所有类产生影响,而对其他的包不造成任何影响。
    9. 把相关的数据和行为集中放置。设计者应当留意那些通过get之类操作从别的对象中获取数据的对象。这种类型的行为暗示着这条经验原则被违反了。
    10. 把不相关的信息放在另一个类中(也即:互不沟通的行为)。朝着稳定的方向进行依赖。
    11. 确保你为之建模的抽象概念是类,而不只是对象扮演的角色。
    12. 在水平方向上尽可能统一地分布系统功能,也即:按照设计,顶层类应当统一地共享工作。
    13. 在你的系统中不要创建全能类/对象。对名字包含 Driver、Manager、System、Susystem 的类要特别多加小心。规划一个接口而不是实现一个接口。
    14. 对公共接口中定义了大量访问方法的类多加小心。大量访问方法意味着相关数据和行为没有集中存放。
    15. 对包含太多互不沟通的行为的类多加小心。这个问题的另一表现是在你的应用程序中的类的公有接口中创建了很多的get和set函数。
    16. 在由同用户界面交互的面向对象模型构成的应用程序中,模型不应该依赖于界面,界面则应当依赖于模型。
    17. 尽可能地按照现实世界建模(我们常常为了遵守系统功能分布原则、避免全能类原则以及集中放置相关数据和行为的原则而违背这条原则) 。
    18. 从你的设计中去除不需要的类。一般来说,我们会把这个类降级成一个属性。
    19. 去除系统外的类。系统外的类的特点是,抽象地看它们只往系统领域发送消息但并不接受系统领域内其他类发出的消息。
    20. 不要把操作变成类。质疑任何名字是动词或者派生自动词的类,特别是只有一个有意义行为的类。考虑一下那个有意义的行为是否应当迁移到已经存在或者尚未发现的某个类中。
    21. 我们在创建应用程序的分析模型时常常引入代理类。在设计阶段,我们常会发现很多代理没有用的,应当去除。
    22. 尽量减少类的协作者的数量。一个类用到的其他类的数目应当尽量少。
    23. 尽量减少类和协作者之间传递的消息的数量。
    24. 尽量减少类和协作者之间的协作量,也即:减少类和协作者之间传递的不同消息的数量。
    25. 尽量减少类的扇出,也即:减少类定义的消息数和发送的消息数的乘积。
    26. 如果类包含另一个类的对象,那么包含类应当给被包含的对象发送消息。也即:包含关系总是意味着使用关系。
    27. 类中定义的大多数方法都应当在大多数时间里使用大多数数据成员。
    28. 类包含的对象数目不应当超过开发者短期记忆的容量。这个数目常常是6。当类包含多于6个数据成员时,可以把逻辑相关的数据成员划分为一组,然后用一个新的包含类去包含这一组成员。
    29. 让系统功能在窄而深的继承体系中垂直分布。
    30. 在实现语义约束时,最好根据类定义来实现。这常常会导致类泛滥成灾,在这种情况下,约束应当在类的行为中实现,通常是在构造函数中实现,但不是必须如此。
    31. 在类的构造函数中实现语义约束时,把约束测试放在构造函数领域所允许的尽量深的包含层次中。
    32. 约束所依赖的语义信息如果经常改变,那么最好放在一个集中式的第3方对象中。
    33. 约束所依赖的语义信息如果很少改变,那么最好分布在约束所涉及的各个类中。
    34. 类必须知道它包含什么,但是不能知道谁包含它。
    35. 共享字面范围(也就是被同一个类所包含)的对象相互之间不应当有使用关系。
    36. 继承只应被用来为特化层次结构建模。
    37. 派生类必须知道基类,基类不应该知道关于它们的派生类的任何信息。
    38. 基类中的所有数据都应当是私有的,不要使用保护数据。类的设计者永远都不应该把类的使用者不需要的东西放在公有接口中。
    39. 在理论上,继承层次体系应当深一点,越深越好。
    40. 在实践中,继承层次体系的深度不应当超出一个普通人的短期记忆能力。一个广为接受的深度值是 6。
    41. 所有的抽象类都应当是基类。
    42. 所有的基类都应当是抽象类。
    43. 把数据、行为和/或接口的共性尽可能地放到继承层次体系的高端。
    44. 如果两个或更多个类共享公共数据(但没有公共行为),那么应当把公共数据放在一个类中,每个共享这个数据的类都包含这个类。
    45. 如果两个或更多个类有共同的数据和行为(就是方法),那么这些类的每一个都应当从一个表示了这些数据和方法的公共基类继承。
    46. 如果两个或更多个类共享公共接口(指的是消息,而不是方法),那么只有他们需要被多态地使用时,他们才应当从一个公共基类继承。
    47. 对对象类型的显示的分情况分析一般是错误的。在大多数这样的情况下,设计者应当使用多态。
    48. 对属性值的显示的分情况分析常常是错误的。类应当解耦合成一个继承层次结构,每个属性值都被变换成一个派生类。
    49. 不要通过继承关系来为类的动态语义建模。试图用静态语义关系来为动态语义建模会导致在运行时切换类型。
    50. 不要把类的对象变成派生类。对任何只有一个实例的派生类都要多加小心。
    51. 如果你觉得需要在运行时刻创建新的类,那么退后一步以认清你要创建的是对象。现在,把这些对象概括成一个类。
    52. 在派生类中用空方法(也就是什么也不做的方法)来覆写基类中的方法应当是非法的。
    53. 不要把可选包含同对继承的需要相混淆。把可选包含建模成继承会带来泛滥成灾的类。
    54. 在创建继承层次时,试着创建可复用的框架,而不是可复用的组件。
    55. 如果你在设计中使用了多重继承,先假设你犯了错误。如果没犯错误,你需要设法证明。
    56. 只要在面向对象设计中用到了继承,问自己两个问题:(1)派生类是否是它继承的那个东西的一个特殊类型?(2)基类是不是派生类的一部分?
    57. 如果你在一个面向对象设计中发现了多重继承关系,确保没有哪个基类实际上是另一个基类的派生类。
    58. 在面向对象设计中如果你需要在包含关系和关联关系间作出选择,请选择包含关系。
    59. 不要把全局数据或全局函数用于类的对象的薄记工作。应当使用类变量或类方法。
    60. 面向对象设计者不应当让物理设计准则来破坏他们的逻辑设计。但是,在对逻辑设计作出决策的过程中我们经常用到物理设计准则。
    61. 不要绕开公共接口去修改对象的状态。

其他

工作

OOD,从表象上理解是用class来抽象和模拟事物之间的关系,实际上是借助接口与继承这种抽象机制来 decouple (解耦合)代码,使得代码结构清晰易于维护。在实践中,大家发现有一些解耦合的方式经常出现,就把它们总结为 design patterns (设计模式)。很多人在读设计模式这本书的时候会有一种“啊,原来这种写法叫做这个模式”的感觉——然而一本系统性的总结书籍是必须的,1 大家总有没用过见过的模式,2 设计模式给出了一个交流的基础,举例来说当你讨论一段代码的写法时的时候不用自己在组织内部发明一套语言,或者绕一个大圈子描述细节,而是可以简单地说,我们可以用xxx模式
学 OOD,就两本书,一本是大名鼎鼎的 Design Patterns: Elements of Reusable Object-Oriented Software,因为作者有四个所以又称 GOF/四人帮
另外一本是 Head First Design Patterns,相对来说浅显易懂
比较重要的常用的 pattern 只有:Factory, Singleton, Adapter, Composite, Observer(Listener), Template Method, Proxy, Visitor, Iterator, Builder. 如果是临阵抱佛脚的可以多多关注这几个 pattern。State,Bridge 和 Strategy 也可以大体看看。
具体的 pattern 这里就不去讨论了,大家静下心来看书就是。要达到的目标大体上是 1 能够回答,xxx 模式如何实现的 2 为什么要使用 xxx 模式(其优点何在) 3 在什么场景下使用 xxx 模式。其实不单 OOD,任何一项技术能够回答这三个问题,也就算是有一个 big picture 了。
下面说点关于 OOD 的闲话。
OOD 刚刚被发明的时候,是被很多人认为是 Silver Bullet 的。大家觉得它从根本上解决了代码 reuse (复用)的问题 —— 低耦合高内聚的代码,使得代码的模块化、重复使用不再复杂,进而解决了软件界一直存在的重造轮子的问题 —— 结果我们都看到了,软件界到今天也仍然在重造轮子。软件界没有银弹 —— OOD 不是,SOA 不是,PaaS 不是,IaaS 也不是。说点哲学的,软件的复杂是内秉的:因为人类的需求是基于人类的高阶逻辑的,而计算机至今能满足的是一阶逻辑演算,所以软件界在强 AI 出现之前不能解决软件的统一解决方案是非常正常的。
当然我们不能说 OOD 没有解决问题,它毕竟是一种 proven technique ,在代码层面解决代码的可维护性问题一直都是码农手中的得力工具。相比 procedure,它提供了有力的抽象工具,能够使得代码不是一坨互相调用来调用去的函数,而是具有抽象的接口,各司其职;相比 functional,OOD 的代码可能会显得不够简洁,然而 Object Oriented 的形式使得它更加贴近人类的思维模式 —— functional 编程虽然强大但至今不成主流,也就是因为这一点了。
另一方面,我这些年在软件业中发现新人普遍具有模式沉迷的问题。这是难免的,因为设计模式都是精炼的前人经验,确实有一些精妙之处。但是设计模式本身是提炼解决问题的方式,实际工作中千万不要用设计模式去解决并不存在的问题。当应用一个设计模式的时候,尤其是感觉到啊这个设计模式用在这里简直精妙的时候,就要警觉一点了。这时候必须谨慎地想清楚,为什么要用这种设计模式,使用这种模式是否多余。举例来说,在两种行为模式之间,是选择 if else 好,还是用 strategy 模式?在两个数据库实现之间切换,是否要用 adapter 模式?
(这样的问题其实并没有统一的答案,都是 case by case 分析的。)
另外滥用模式也逐渐成为了现在 Java 程序员的通病。比如用 factory,不要直接 new 之类的已经成为了部分程序员的真理信条;搞个 singleton 到处用结果成了包了一层的全局变量这种更是比比皆是。一言以蔽之,为了模式而模式,认为使用了模式就提高了代码质量,都是误解。
那么跑题都跑到这里了,代码质量究竟是什么?我本人对代码大概有如下几个方面的要求(很多人也许有不同意见):

  1. 可读性——代码即使以后不被改动,也是有人要读的,如果一段代码三个月后作者自己都不知道写的是个什么玩意,必然不合格
  2. 易扩展型——注意,是易扩展型,而不是扩展性。因为扩展性可能是一个不存在的问题,我们不应该为未来不存在的扩展浪费过多的时间,但是如果能够把代码组织成很容易改写成具有扩展性的代码,就可以在开发速度和维护性上做出一个平衡了。
  3. 难改动性——想了想没有想到太好的词。这里的意思是说,通过组织代码的结构,使得未来的人难以做出危害代码质量的改动。举个简单的例子,在某个初始化函数里面我们初始化了三个有关联的成员变量,里面还夹了一坨逻辑,这时候把它们 extract 成一个 method,给它一个有意义的名字,这样可以i)使得原有的逻辑不会因为后面的改动被打散,而变得不可读;ii)在增加逻辑的时候,后人会被这个 method 的名字所约束,不会破坏 contract。当然如果你的初始化代码是纯逻辑的话这也是一个加入 unit test 的理想位置。
  4. 无重复性——如果说写代码的时候有什么必须遵守的铁律,这就是其中一条。任何形式的重复代码都是绝不可取的,必须改掉。无重复性和低依赖有时是一对不可调和的矛盾:一段代码,如果有 10 个其他模块都需要用,重用性是高了,却变成了 single point failure,稍微一变整个系统就挂了,如果第十一个也要用但需要多加个参数,怎么测那 10 个模块就是门艺术了,这个代码就没法重用;无重复性很容易产生高依赖而违背OO低耦合这个目标,哪怕用重载多态解决也会产生相当多的重复,所以没有完美的 solution,而是一种根据经验左右平衡的艺术。
  5. 最后再推荐一本书,叫做《Refactoring: Improving the Design of Existing Code》,这本书并不是用来面试的,而是讲如何提高代码质量,避免写出烂代码。作为一名合格的程序员,不可不察。

所以回到主题上来,OOD 要用,很好,然而你先要权衡用与不用之间,究竟利弊如何。

一般工作中,我们最重视的就是代码的可读性。我的经验是写代码的时间远远低于后面维护和 debug 的时间,因此,代码的可读性非常非常的重要。代码的层次,以及必要的 high level 的注解。

不能简单的将 OOD 等同于 Design Patterns, OOD 是运用面向对象思想将现实世界映射到程序世界的一整套方法论,而 Design Patterns 只是根据这套方法论总结出的一些固定招式。推荐从《UML模式与应用》开始,之后再读《敏捷软件开发: 原则、模式与实践》以及楼主说的《设计模式》。更进阶的还可以去读《领域驱动设计》《重构》《企业应用架构模式》之类的。

以上来源:https://www.1point3acres.com/bbs/thread-176958-1-1.html

面向对象 vs 面向过程 vs 函数式编程

深入理解函数式编程
无需设计模式的函数式编程

编程说穿了就是两个方面,data 和这些 data 上的 operation。
oop,就是让 operation 围绕 data,这样的好处是,当要添加新的 data type 的时候,好方便!原来写的代码都不用改。但是要给已经写好的 data type 添加方法怎么办?比如要你给 java 自带的 string 加个 python 那种乘法。
functional programming 采取的是另一种思路,data 更多的围绕 operation,所以添加新的方法很容易。
这就是著名的 the expression problem。谁优谁劣,要看应用场景,写 GUI 用 oop 好不畅快,写 interpreter 函数式可能更方便。总结就是两者能力都是一样的,只是一些场景下其中一个比另一个更好实现、好写好看、少副作用一些。
这些都是 idiom,不是宗教,具体问题具体分析。但是学习 functional programming 绝对大有裨益,因为国内的计算机教育太强调图灵模型(也就是 C 语言一脉)了,而对 lambda calculus 涉及太少,造成了很多偏见和误解。参考链接

例如五子棋,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用分别的函数来实现,问题就解决了。
而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为 1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的i变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。

所以,面向对象的方法讲究的是抽象、对象化。
对于简单的业务需求,使用哪种方法的差别其实并不大。但是如果业务需求比较复杂,以后希望有较好的可扩展性,“面向对象”会更加适合。

而现实是,工作中面对的基本上都没什么简单的业务需求。并且因为未来的不确定性非常大,变化又多又快,现在 OO 的方法就更加适用了。作为 DEV,会使用 OO 进行设计和开发。而作为 BA,会使用 OO 进行分析,简写也就是 OOA(面向对象的分析)。

面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix 等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展

面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低

通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
而函数式编程(请注意多了一个“式”字)—— Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。
首先要搞明白计算机(Computer)和计算(Compute)的概念。
在计算机的层次上,CPU 执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。
而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。
对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如 C 语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如 Lisp 语言。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数。

函数式编程可能乍看起来以为和面向过程是类似的,但是还是有所不同。
和函数式对比的应该的命令式编程,函数式编程关心数据的映射,命令式编程关心解决问题的步骤。

函数式编程(Functional Programming)的关键特点包括:

  1. 纯函数(Pure Functions):函数的输出完全取决于输入,不依赖于外部状态或全局变量。给定相同的输入,纯函数总是返回相同的输出,不会产生副作用。
  2. 不可变性(Immutability):数据在创建后不能被修改。一旦创建,数据将保持不变,这有助于减少状态的复杂性和副作用。
  3. 高阶函数(Higher-order Functions):函数可以接受其他函数作为参数,也可以返回函数作为输出。这种能力使得函数能够更加灵活地组合和重用。
  4. 递归(Recursion):函数式编程鼓励使用递归而不是循环来进行迭代。递归是一种强大的工具,它允许编写简洁、优雅的代码。
  5. 引用透明度(Referential Transparency):表达式或函数的结果只取决于其输入,不受上下文环境的影响。具有引用透明度的代码更容易推理和测试。
  6. 惰性评估(Lazy Evaluation):延迟计算,只在需要时才对表达式进行计算,这有助于提高性能和资源利用率。
  7. 模式匹配(Pattern Matching):一种可以用于检查数据结构的方式,可以根据数据结构的形状进行分支处理。

来源:

代码质量

什么是好代码,不好定义,但是关于什么是代码里的"坏味道",比较容易搞清楚。避免代码里的“坏味道",离好的代码就不远了,坏味道一二三及推荐做法:  
* 代码重复
* 函数太长 - 如果太长(一般不宜超过 200 行,但不绝对),自己都不太容易读懂,请拆成小函数吧
* 类太大 - 一般不宜超过 1000 行,同样不绝对,jdk 源码过千行的不少
* 数据泥团 - 即很多地方有相同的三四项、两个类中有相同的字段、许多函数签名中有相同的参数。把这些应该捆绑在一起的数据项,弄到一个新的类里。这样,函数参数列表会变短不少,简单化了
* 函数参数列表太长 - 工作中有 7 个参数的函数调用,搞清楚每个参数的业务含意和顺序有点困难。尽管可能有默认函数参数,仍然容易出错
* 变量名、函数名称、类名、接口等命名含义不清晰 - 函数名能让人望名知义,看名字就知道函数的功能几乎不需要多少 comments 最好,通常 DAO 层函数的命令规范是“操作+对象+通过+什么”,如:updateUserById, insertQuarter,delteteUserByName
* 太多的 if else
* 在循环里定义大量耗资源的变量 - 大对象,如果可以放在循环外,被共享,节省时间空间
* try 块代码太长 - try 块只包住真的可能发生异常的语句,最小原则,同样因为 try 包起来的代码要有额外开销
* 不用的资源未及时清理掉,流及时关闭 - 如 IO 句柄、数据库连接、网络连接等。不清理掉可能后果严重
* try-finally 不如 try-with-resources
* 循环里字符串的拼接不要用”+“
* 太巨量的循环,看情况用乘除法和移位运算 - 移位运算通常速度略微快于乘除法
* 避免运行时大量的反射 - 反射的不好的地方:编译时没法检查了、反射的代码冗长和丑陋、性能损耗
* 基本类型优于装箱基本类型 - 基本类型更快,更省空间。避免不经意引起自动装箱和拆箱。是否相等的比较,"装箱基本类型"可能会出错
* 避免创建不必要的对象
* 未作参数有效性检查 - ArrayIndexOutOfBoundsException、NullPointerException 等等,是否为空的检查推荐 Java 8 的 Optional
* 延迟初始化和懒加载 - 这个的确是一种优化,即需要用到它的值时,才初始化。如果永远不用到,就永远不会被初始化。但要慎用,只有在初始化这个数据域开销很大的时候才用。在大多数情况下,正常的初始化要优于延迟初始化
* LinkedHashMap、HashMap、ArrayList、HashSet、HashTable 等集合类,没有初始化容量
* 方法和类如果确实有业务场景需求不会被覆盖、不会被继承,用 final 修饰 - final method 在某些处理器下得到优化,跑得更快
* 合理数据库连接池和线程池 - 一个减少数据库连接的建立和断开(耗时),一个减少线程的创建和销毁,动态根据请求分配资源,提高资源利用率
* 多用 buffer 等缓冲提高输入输出 IO 效率及 FileChannel.transferTo、FileChannel.transferFrom 和 FileChannnel.map - 诸如 BufferedReader 、BufferedWriter、BufferedInputStream 和 BufferedOutputStream 等
* synchronized 修饰符最小作用域 - synchronized 要耗费性能,因此 synchronized 代码块优于 synchronized 方法,最小原则
* enum 代替 int 枚举模式 - int 枚举模式不具有类型安全性,也没有描述性,比较也会出问题
* 合理使用静态工厂方法代替构造器
* 组合优于继承 - 因为继承打破了封装性,overriding 可能导致安全漏洞
* 异常只能用于处理错误,不能用来控制业务流程
* 精准的运算,如货币运算等不要用 float 和 double - 正确的做法,用 BigDecimal、int 和 long
* ArrayList 对于“随机访问较多的场景”性能较高,LinkedListd 对于“删除和插入较多的场景”性能更高
* 使用范围最小的数据类型,redis 源码里大量使用 unsigned int 和 unsigned long,时间和空间效率高于 int 和 long
* 将局部变量最小化
* 并发的数据结构可以降低高并发下的 CPU 时间,但要评估内存消耗 - 并发的数据结构如 ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteHashSet 等,可以在读和写的时候,不用加锁,因而提高了高并发下的处理效率。但是其复杂的数据结构和锁优化,代码了额外的内存消耗
  
参考《Effective java》《重构 —— 改善既有代码的设计》《深入分析Java Web技术内幕》  

转载来源:
https://www.cnblogs.com/NaughtyCat/p/what-is-good-codes.html

何时使用抽象类,何时使用接口

有一个几乎每次都有效的基本规则:如果可以声明 “A 是 B”,则使用抽象类和继承。如果可以声明 “A 能够 doing as”,那么就使用接口。
例如,可以说三角形是多边形,但说三角形能够成为多边形是没有意义的。
不管怎样,与以往一样,经验法则是:运用你的常识。有时,即使上述规则告诉你相反的情况,接口可能更合适(如果只是使用接口,且是在考虑后果之后)。

当涉及抽象类和接口时,以下是它们之间的主要区别(By ChatGPT):

  1. 设计目的:
    • 抽象类用于表示一种“是什么”的关系,表示类之间的继承关系,它可以包含抽象方法和具体方法。
    • 接口用于表示一种“可以做什么”的关系,定义一组行为的规范,它只能包含常量和抽象方法(Java 8 及以后还可以包含默认方法即具体实现和私有方法)。
  2. 继承关系:
    • 一个类只能继承一个抽象类,但可以实现多个接口,这使得接口支持多重继承。
    • 抽象类支持构造函数,而接口不支持,因为接口不能被实例化(抽象类也不可以实例化)。
  3. 属性:
    • 接口不允许包含普通的实例变量(属性),只能包含常量(public static final 修饰的变量)。
    • 抽象类可以包含普通的实例变量和常量。
  4. 使用场景:
    • 使用抽象类时,考虑类之间的继承关系,将一组相关的类抽象出公共行为和属性,同时允许子类实现不同的行为。
    • 使用接口时,考虑一组类实现了相同的行为规范,但在类层次结构上没有明显的继承关系,以及需要支持多重继承的场景。

总的来说,抽象类更适合用于表示类之间的继承关系和提供公共的实现,适合于类层次结构,而接口更适合用于定义一组行为的规范,适合于实现类之间的解耦和多态性。在设计时根据具体情况和设计需求选择使用接口或抽象类,或者它们的组合,以实现更灵活、可维护和可扩展的代码。

当涉及到方法的具体实现和线程安全性时,使用抽象类更为合适(所以可以留意到 Java 官方的一些重要的线程安全组件源代码是使用抽象类,如 AbstractQueuedSynchronizer)。抽象类提供了更多的灵活性和控制权(有自己的字段/属性,用 synchronized 锁住某个字段/属性或锁住对象或锁住整个类,接口只能默认方法可以用 synchronized 修饰),能够更好地满足类之间的继承关系和共享通用实现的需求。

转载:http://www.devfields.com/abstract-classes-and-interfaces/

静态方法

default 关键字目前只能在接口中使用,不能在其他地方使用。接口也可以使用 static 创建静态方法,default 和 static 关键字不能合用。
静态方法是属于类而不是对象的,因此它们不能被对象调用,只能通过类名称直接调用。
静态方法在类加载时就被加载到内存中,并且与类的其他静态成员一样,只有一份副本。无论创建多少个对象实例,静态方法都只有一个,因为它们不属于任何特定的对象,而是属于整个类。