Skip to content

Latest commit

 

History

History
200 lines (128 loc) · 17.4 KB

README.md

File metadata and controls

200 lines (128 loc) · 17.4 KB
  1. 在面向对象编程中,一个很重要的目标是高内聚(High Cohesion)和松耦合(Loose Coupling). 那么它意味着什么?为什么这个很重要?如何实现它呢?
  2. 为什么在大部分语言中数组的下标从0开始?
  3. 内聚和耦合有什么区别?
  4. 重构的作用是怎样的?
  5. 代码中注释是有用吗?一些人说我们应该尽可能的避免注释,而且它们大部分是无用的,你同意吗?
  6. 为什么测试驱动开发TDD中的测试时在开发之前?
  7. 在存储过程中使用领域逻辑有什么好处和坏处?
  8. 在你观点来看,使用面向对象编程为什么能够占据市场这么长时间?
  9. 测试和测试驱动开发(TDD)如何影响代码设计?
  10. 设计和架构有什么区别?
  11. C++支持多继承,而JAVA允许一个类实现多个接口。在正交性上有什么影响?使用多继承和多接口有什么区别?使用委托和继承有什么区别?(问题来自The Progmatic Programmer一书中)

1 在面向对象编程中,一个很重要的目标是高内聚(High Cohesion)和松耦合(Loose Coupling). 那么它意味着什么?为什么这个很重要?如何实现它呢?

内聚用来度量一个软件的组成部分所专注的内容或者职责;耦合用来判定一个软件组成部分与其他部分的联系程度。软件的组成成分可以是类,包,组件,子系统或者完成的系统,然后在设计系统的时候,都会建议软件的各个组成拥有高内聚和松耦合。

低内聚将会导致巨大的类将会非常难以维护和理解,并且降低了可读性;同样的,紧耦合导致类之间紧紧联系在一起,每次改动都会涉及到其他的,导致难以改动和重用。

我们假设一个场景,设计一个可监控的类ConnectionPool,虽然它看上非常像一个简单的ConnectionPool,但是它的主要目的是演示如何实现高内聚和松耦合。它将有如下的功能:

  1. 支持获取一个连接;
  2. 释放一个连接;
  3. 获取连接使用情况统计;
  4. 获取连接时间统计;
  5. 保存连接获取和释放信息,并统计汇报;

我们使用低内聚,设计出如下的ConnectionPool类,它生硬地将上述的所有功能和职责装入到一个类中,如下图所示,从中可以看出, 单个类包含连接管理,与数据库交互和维护连接状态等所有信息

使用高内聚,我们可以将这些职责分配给不同的类,来让他们更加可维护和可重用。

为了演示松耦合,我们继续使用紧耦合来设计ConnnectinPool,如果我们仔细看看上面的示意图,虽然它支持了高内聚,但是ConnectionPool仅仅地和ConnectionStatisticsPersistentStore两个类直接联系在一起。为了降低耦合,我们引入了ConnectionListener接口,让两个类分别实现这个接口,并且注册到ConnecitonPool类中。ConnecitonPool将会迭代这些监听器并且通知这些连接设置和释放事件,通过这样来完成解耦。

**注意点:**对于上述简单的场景,这个看上去好像有点过于设计,但是我们想象一下如果在真实的场景中,如果我们的应用程序需要和许多第三方的服务来完成事务,将我们的代码和第三方服务的进行直接耦合在一起也就意味着第三方服务的任何改变都会导致我们自己的代码发生改变,所以我们可以需要Facade设计模式来将我们的代码和不同的第三方服务隔离开来。

2 为什么在大部分语言中数组的下标从0开始?

大部分程序语言中,比如C/C++, Java等的数组都是从0开始,数组最后一个索引的为数组的长度减去1。对大部分程序开发人员而言,这个算是习以为常了。为什么数组的索引从0开始呢?这个与程序语言设计有关,比如在C语言中,数组的名字本质上就是指针,指向内存开始的位置,所以表达式array[n]表示为内存位置离数组开始位置,也就是偏移量。所以第一个元素也就是在数组的名字指向的位置,所以采用array[0]表示数组的第一个元素。 Dijkstra曾经解释过为什么数组从0开始,问题在于我们如何表示自然数,比如1,2,3,...,10,我们有四种可行的方案:

  • a. 0<i<11
  • b. 1<=1<11
  • c. 0<i<=10
  • d. 1<=i<=10

Dijkstra提出了表示表示应该可以表示下面两种情况:

  1. 序列必须包含最小的自然数0
  2. 序列应该是空的 第一个情况要求我们排除ac,当然也可以使用,当然也可以使用-1<i来表示,不过这个太丑陋了;第二个情况可以排除d,剩下的就是b方案,而且�两端相减就是序列的长度。 所以当你写如下的代码
for (i=0; i < N; i++){
    sum += a[i]
}

这个代码就遵循了上述的�语言设计规则。

3 内聚和耦合有什么区别?

  • 内聚 用来度量一个软件的组成部分所专注的内容或者职责
  • 耦合 用来判定一个软件组成部分与其他部分的联系程度。软件的组成成分可以是类,包,组件,子系统或者完成的系统,然后在设计系统的时候,都会建议软件的各个组成拥有高内聚和松耦合。

4 重构的作用是怎样的?

重构是一种提高现有代码设计的的控制手段,在功能保证前提下代码形式改变。但是这样累积而成改变的代码是非常重要的。通过这样微小的改变可以避免引入错误。这样也可以避免因为重建系统将整个系统垮掉,可以在长时间段将整个系统渐进式改变。 在重构的时候要注意到如下

  • 在修改之前和之后,所有的单元测试必须通过;
  • 应该没有必要修改和增加任何测试;
  • 在完成之后,必须要让代码更加清晰;
  • 应该不能增加新的功能

5 代码中注释是有用吗?一些人说我们应该尽可能的避免注释,而且它们大部分是无用的,你同意吗?

好的代码应该是 self-document,也就是说通过代码完成注释所需要的工作。注释应该要遵循以下规范

  • 注释应该解释它做了什么;
  • 注释应该解释它是如何完成它所需要的做内容;
  • 注释应该解释为什么它是这样的;
  • 对于变量和常量,注释应该关注它的内容而不是目的;
  • 对于公共API必须增加文档注释;
  • 对于糟糕的代码,去重写它,而不是添加注释;
  • 对于做出修改的代码,必须重新注释,错误的注释比没有注释更加糟糕;

6 为什么测试驱动开发TDD中的测试时在开发之前?

  • 在开发之前创建测试,可以很好的帮助开发人员考虑具体需要完成什么;
  • 开发人员在完成功能后,可以立即得到反馈;
  • 对于系统设计也有帮助,测试先行可以对所有不同阶段的开发人员进行约束;
  • 通过一个个测试添加,对系统开发完成节奏有帮助;

7 在存储过程中使用领域逻辑有什么好处和坏处?

存储过程在处理比较复杂的业务的时候比较实用,具体分为两个方面

  1. 响应时间:如果前台处理的话,可能涉及到多次数据库连接,但是如果实用存储过程的话,只需要一次数据库连接。
  2. 安全性:存储过程的系统更加稳定,而应用程序容易出现 BUG 而不稳定,存储过程如果数据库不出现问题,就不会有问题。

但是存储过程往往定制化于特定的数据库上,因为支持的编程语言不通,当切换到其他厂商的数据库的时候,需要重写原有的存储过程。而且存储的性能调校与撰写,受限于各种数据库系统。

8 在你观点来看,使用面向对象编程为什么能够占据市场这么长时间?

面向对象编程(Object Oriented Programming)开始于1980s,其包含了三大主要特性也是其目前�流行的主要 原因。

  • 封装 (encapsulation)

面向对象将数据和方法封装起来,通过控制符来控制它们的可访问性。对于面向过程的语言中,如果一个变量被越多的函数 能够访问,就越变得难以掌控,当程序变得越来越大,就会导致整个系统越发难以维护。现代软件系统通常包含的代码量非常大, 面向过程编程减低了开发效率,而面向对象降低了开发的门槛,提高了开发进度。

  • 继承 (inherition)

继承的好处是让我们能够重用我们已有的代码,子类能够使用父类的方法,并且增加自己特有的方法。随着GUI兴起,面向对象 编程处理起来更加得心应手,对GUI控件对象继承体系,大大地降低开发的难度,并且提供了自定义控件开发的基础。

  • 多态 (polymorphism)

子类通过重载(override)父类的同名方法,达到不同的行为的目的。接口是多态的一种展示形式,每个实现接口的对象,可以完成 各自的所需的行为,这也是各种设计模式精髓所在。

但是目前针对面向对象编程也出现了一些反对面向对象编程的声音,主要有以下几点

  1. 虽然面向对象编程封装了数据和方法,但是所有的操作仍然也是命令式(imperative)语句,而不是声明式(declarative)语句。这样对于并行编程(parallel programming)不够优化

  2. 状态(state)是邪恶的,包含状态的程序非常难以并行运行,而面向对象编程非常鼓励可变性(mutablility)。

  3. 在现实世界面向对象编程中,常常很少包含真正的实体名称,常常将一些所谓动词来冒充名词:比如策略, 工厂或者命令

  4. 组合往往比继承更好地方式来编程,继承往往是增加的概念上复杂度。

9 测试和测试驱动开发(TDD)如何影响代码设计?

  1. 提升代码质量:TDD要求在编写实际代码之前先编写测试用例,这种做法能够确保开发的功能满足预定要求。通过先写测试,开发者被迫从使用者的角度思考功能,这有助于发现潜在的设计问题和接口的不合理之处。此外,由于测试用例覆盖了代码的各个方面,因此有助于提前发现错误和问题,从而提高代码的可靠性。

  2. 促进模块化设计:为了使代码易于测试,开发者往往会倾向于采用更模块化的设计。这意味着功能被划分为更小、更独立的部分,每部分都有明确的职责。模块化设计不仅使得代码更易于理解和维护,也促进了代码的重用。

  3. 简化接口:TDD鼓励简单和直观的接口设计。因为如果接口复杂难用,编写测试用例本身就会变得困难。通过迫使开发者在编写实际代码之前思考如何测试功能,TDD帮助简化了接口,使其更加用户友好。

  4. 文档作用:测试用例本身可以作为代码的一种形式的文档。通过查看测试用例,可以了解到功能的预期行为,以及如何使用代码中的各个部分。这种方式的“文档”总是与代码保持同步,因为一旦代码发生变化,测试用例也需要更新以反映这些变化。

  5. 促进可维护性和可扩展性:遵循TDD的代码通常更容易维护和扩展。由于代码是为了满足测试用例而编写的,因此任何未来的更改都需要通过现有的测试,这有助于防止新的更改破坏现有功能。此外,由于代码是模块化设计的,添加新功能或修改现有功能变得更加容易。

  6. 改进团队沟通:TDD也可以作为团队成员之间沟通的一种工具。测试用例提供了对功能预期行为的明确说明,有助于团队成员理解彼此的工作。此外,在进行代码审查时,测试用例可以帮助审查者理解代码的目的和功能,从而提高审查的效率和质量。

10 设计和架构有什么区别?

软件架构

软件架构关注的是系统的高层结构。它定义了软件的整体组织,包括系统的各个组成部分、这些部分之间的关系以及它们与外界环境的交互。软件架构涉及的决策包括技术选择(如编程语言、数据库、硬件平台等)、结构模式、性能和安全性等关键问题。架构的目标是确保软件系统能够满足业务需求,同时具备良好的可扩展性、灵活性和可维护性。

焦点:整体结构、系统组件及其交互。 目的:确保系统满足所有非功能性需求,如性能、安全性、可用性等。 视角:从较高的层次审视系统,涉及更广泛的决策。

软件设计

件设计则更加细致和具体,关注的是如何实现软件架构中定义的各个组件。设计过程包括制定算法、定义数据结构、详细规划模块之间的接口以及其他实现细节。软件设计确保组件能够正确、高效地执行其预定功能。好的设计不仅解决了如何实现功能的问题,还考虑了如何使系统易于理解、修改和扩展。

焦点:实现细节、组件设计、代码层面的结构。 目的:确保系统的功能需求得到满足,同时代码清晰、易于维护。 视角:从较低的层次,即组件或模块级别审视系统,着眼于具体实现。

11 C++支持多继承,而JAVA允许一个类实现多个接口。在正交性上有什么影响?使用多继承和多接口有什么区别?使用委托和继承有什么区别?(问题来自The Progmatic Programmer一书中)

接口只定义了一个类要做什么(What to do) 而不是应该怎么去做(How to do)。 比如说如果有有两个类 ToasterNuclearBomb, 他们都有一个 darkness 的配置和 on 的方法,如果你创建了一个类继承了它们,那么你的类中该如何定义 on 这个方法呢? 所以需要在子类中指定这些行为,C++ 中有这样的操作,但是 Java 的设计者觉得这样会让事情变得更加复杂,所以放弃了这个方案。

而委托是组合的一种方案,通过委托在类中包含了所需要的类型的行为。

12 你是如何知道一个代码是坏的设计?

识别代码的坏设计通常涉及到观察代码中的一些特定迹象或“代码坏味道”(code smells)。这些迹象表明代码可能难以维护、扩展或理解。虽然某些情况下这些迹象可能不立即导致问题,但它们通常预示着潜在的设计问题,可能会在未来导致更严重的问题。下面是一些常见的坏设计迹象:

  • 重复代码(Duplicated Code):在多个地方出现非常相似或完全相同的代码,意味着对相同的逻辑进行了多次实现。这不仅增加了维护成本,也意味着未来进行更改时需要在多个地方进行修改。

  • 过长函数(Long Method):单个函数或方法体积庞大,尝试做太多事情。这通常意味着函数缺乏聚焦,难以理解和测试。

  • 过大的类(Large Class):一个类承担了过多的责任,包含了过多的变量和方法。这违反了单一职责原则,使得类难以理解、维护和修改。

  • 过多的参数(Long Parameter List):函数或方法的参数列表过长,难以理解和使用。这通常表明函数承担了过多的责任或者数据结构设计不合理。

  • 依赖混乱(Feature Envy):一个类过分依赖另一个类的内部实现,而不是通过其公共接口交互,导致模块间耦合度过高。

  • 散弹式修改(Shotgun Surgery):对于一个单一的更改,需要修改许多不同的类或文件。这表明功能逻辑分散在了代码库的多个部分,而不是被封装在单一的模块或类中。

  • 僵尸代码(Dead Code):代码库中存在不再被使用的代码,例如未被调用的函数、多余的变量等。这些代码增加了理解和维护的难度。

  • 过度工程(Overengineering):为了解决可能永远不会出现的问题而设计了过于复杂的系统。这通常会导致系统难以理解和维护。

  • 不恰当的封装(Inappropriate Intimacy):模块或类之间共享太多私有数据,破坏了封装性,使得代码难以修改和重用。

  • 神秘命名(Mysterious Name):变量、函数或类的命名缺乏描述性,使得理解它们的用途和功能变得困难。