Skip to content

Latest commit

 

History

History
356 lines (291 loc) · 23.3 KB

File metadata and controls

356 lines (291 loc) · 23.3 KB
  1. 为什么全局或者静态对象是邪恶的?能够有代码的例子?
  2. 介绍一下控制反转(IoC),它是如何提高代码设计的?
  3. 迪米特法则(Law of Demeter)表述每个单元最外部了解的越少越好,编写违反这个原则的代码,说明它是不好的设计模式,并且修复它。
  4. 活动记录(Active-Record)是一种设计模式,它表述了代表数据库中表的对象应该拥有Insert,Update和Delete等相关操作。在你的观点和工作经验中,这中设计模式有什么限制和缺陷?
  5. 数据映射(Data-Mapper)是另外一种设计模式,它鼓励使用Mapper层用来在内存对象和数据库之间移动数据,用来保证各自的独立。这个与活动记录Active-Record模式相反,你对这两种设计模式怎么看?在什么情况下使用其中一个,而不是另一个?
  6. 为什么说引入null类型是一个Billion dollar mistake,你能说说有什么技术来避免它?比如在GOF书中提到的Null Object Pattern方法,Option 类型。
  7. 许多观点是这样的:在面向对象编程(OOP)中,组合往往是比继承更好的选择,你观点是怎样的?
  8. 什么叫反腐化(Anti-corruption)层?
  9. 单例模式设计模式限制了每一个类只能创建唯一的对象,你能否写一个线程安全的单例模式?
  10. 如何处理依赖灾难(Dependency Hell)
  11. goto语句是邪恶的吗?你或许听过一篇由Edsger Dijkstra写的著名论文 Go To Statement Considered Harmful,在这篇论文中他批评了goto语句,并且推崇结构化编程。 使用goto通常非常有争议,甚至Dijkstra这篇文章也被批评了,诸如'GOTO Considered Harmful' Considered Harmful,那么你的观点是怎样的?
  12. 鲁棒性原则是软件开发中广泛的采用的原则,通常用 对你的输出按照约定,对你的输入保持宽容(Be conservative in what you send, be liberal in what you accept),你能说说这个原则的合理性吗?
  13. 改变实现方式而不影响客户端的能力叫做数据抽象 Data Abstraction,那么编写一个违反这个属性的例子,并且修复它。
  14. 编写一个代码片段并且违法DRY (Don't Repeat Yourself)原则,并且修复它。
  15. 问题拆分(Separation of Concerns)是一种设计原则,它将编程问题划分到不同的领域,每一个领域关注自己的的问题。有许多不同的机制来完成这个目标,比如使用对象,函数,模块,或者MVC等等。 你能讨论一下这个话题吗?

1 为什么全局或者静态对象是邪恶的?能够有代码的例子?

对于一个对象,其影响范围越小越好,也就是说其作用域越小越好。对于全局变量,对于所有的模块都有可见性;而静态成员,对于其所有的实例成员也都有可见性,这些将会增加软件的维护的复杂度。

// tiger.go
package animal
var count = 0
func CreateTiger(){
    count++
}
// lion.go
package animal
func CreateLion(){
    count++
}

在这里count为包animal的全局变量,那么包类所有的成员都能访问该对象,一旦对count做出修改,就会影响包类全部使用该变量的成员。

2 介绍一下控制反转(IoC),它是如何提高代码设计的?

控制反转(Inverse of Control, IoC)是将程序控制流交出去,比如使用控制台进行用户信息请求输入

puts 'What is your name?'
name = gets
process_name(name)
puts 'What is your quest?'
quest = gets
process_quest(quest)

process_nameprocess_quest 两个函数处理流都是用户程序控制,如果使用window窗口界面完成用户信息请求

require 'tk'
root = TkRoot.new()
name_label = TkLabel.new() {text "What is Your Name?"}
name_label.pack
name = TkEntry.new(root).pack
name.bind("FocusOut") {process_name(name)}
quest_label = TkLabel.new() {text "What is Your Quest?"}
quest_label.pack
quest = TkEntry.new(root).pack
quest.bind("FocusOut") {process_quest(quest)}
Tk.mainloop()

现在所有的程序控制逻辑交给了TK.mainloopprocess_nameprocess_quest 被绑定到相应窗口的控件,至于什么时候执行相应的函数无法控制。

框架(framework)是控制反转的代表,框架包含了大量的抽象设计,应用程序在使用过程中,自定义所需的行为,然后被框架调用,整个自定义行为的动作控制流程为框架所接管。在.NET中,通过事件Event机制,用户注册相应的事件即可完成控制反转。

3 迪米特法则(Law of Demeter)表述每个单元最外部了解的越少越好,编写违反这个原则的代码,说明它是不好的设计模式,并且修复它。

迪米特法则也就是最少知识原则,该原则认为任何一个对象或者方法,它应该只能调用下列对象

  • 该对象本身
  • 作为参数传进来的对象(也可以是该对象的字段)
  • 在方法内创建的对象

这个原则用以指导正确的对象协作,分清楚哪些丢向产生写作,哪些对象则对于该对象而言又应该是无知的。现在假设一个购物的场景,主要有客户(Customer),收营员(Paper Boy)负责收钱。

public class Customer {
    public string FirstName { get; init; }
    public string SecondName { get; init; }
    public Wallet Wallet { get; init; }
}

public class Wallet {
    public double Balance { get; set; }
    public void AddMoney(double deposit) => this.Balance += deposit;
    public void SubtractMoney(double debit) => this.Balance -= debit;
}

public class Paperboy {
    public void Pay(Customer customer, double payment)
    {
        Wallet wallet = customer.Wallet;
        if (wallet.Balance > payment)
        {
            wallet.SubtractMoney(payment);
        }
    }
}

对于 PaperBoy 而言,Wallet 不满足迪米特法则的三个条件中的任何一个,让 PaperBoyWallet 对象直接交互是错误的行为,WalletCustomer的隐私,不能交接交给收银员。从职责角度来看,对于收银员,他的职责是负责收钱,而不用管客户钱包的钱是否足够。信息专家模式告诉我们,信息的持有者为操作该信息的专家,数据和行为应该封装在一起。

所以我们应该这样重构,将 pay 的方法移动到 Customer

public class Customer
{
    public string FirstName { get; init; }
    public string SecondName { get; init; }
-   public Wallet Wallet { get; init; }
+   public Wallet Wallet { private get; init; }

+   public void Pay(double payment)
+   {
+       if (Wallet.Balance > payment)
+       {
+           Wallet.SubtractMoney(payment);     
+       }
+       else
+       {
+           //money not enough 
+       } 
+   }
}

在这里,我们将 pay 的责任交给了 Customer,并且我们不再需要暴露 Wallet 这个属性。

判断一段代码是否违背了迪米特法则,有一个小窍门。查看代码中是否出现形如 a.m1().m2().m3().m4() 子类的代码,在 Refactor 一书中,称之为消息链条。但是对于 Linq 组成的处理流,我们称之为 流畅接口或者连贯接口(Fluent Interface),两者的区别是这些是否返回相同的对象。

4 活动记录(Active-Record)是一种设计模式,它表述了代表数据库中表的对象应该拥有Insert,UpdateDelete等相关操作。在你的观点和工作经验中,这中设计模式有什么限制和缺陷?

Active Record通常用在MVC或者MVVM模式中的M,也就是Model层,它将对象的业务层和持久化层封装在一起,每一个实例对应数据库表中的一行,每一个字段对应这个数据库表的中列。同时也包含数据库操作的CRUD操作。在一些不复杂的业务逻辑中,这种模式大大的提高了开发效率,在Ruby on Rails框架中广泛使用。但是这种设计模式违反了Single Responsibility Principle原则,将业务层和持久化层耦合在一起,如果更换数据库,所有的的Model都需要修改,而且还不利于编写单元测试。

5 数据映射(Data-Mapper)是另外一种设计模式,它鼓励使用Mapper层用来在内存对象和数据库之间移动数据,用来保证各自的独立。这个与活动记录Active-Record模式相反,你对这两种设计模式怎么看?在什么情况下使用其中一个,而不是另一个?

Active Record模式不同,使用Data Mapper通过增加Mapper层,将业务层的数据对象和持久化层的数据进行解耦,使它们相互独立。对于包含复杂的业务逻辑和多个对象关联,使用Data Mapper能够有效减低系统的复杂度,但是Mapper层如何访问数据对象的需要考虑,通常使用反射机制,但是反射性能比较差。

6 为什么说引入null类型是一个Billion dollar mistake,你能说说有什么技术来避免它?比如在GOF书中提到的Null Object Pattern方法,Option 类型。

null可以理解两层含义:

  • 无效的
  • 空值

正因如此,在程序运行中中出现 NulPointerException 的异常,比如Java中实例对象调用方法的时候,如果实例为null,抛出NPE;在程序编写过程中,包含大量的 obj != null 的判断语句;而且在设计API过程中,如何正确地处理null类型也需要单独设计。

通常NPE出现的问题在于程序员在开发过程中,并没有区分两者的正确含义,对于空值,正确的做法应当是抛出异常;或者采用go语言中返回多个值,其中最后一个error接口表前面的返回值是否有效。在GOF中的Null Object Pattern模式,定义了Option<T>类型,包含了两个子类型:

// interface
type Option interface {
    GetValue() interface{}
    IsNull() bool
}

// `Some` struct
type Some struct {
    val interface{}
}

func (s *Some) GetValue() interface{}{
    return s.val
}
func (s *Some) IsNull() bool {
    return false
}

// `None` struct
type None struct{

}
func (n *None) GetValue() interface{}{
    return nil
}
func (n *None) IsNull() bool {
    return true
}

7 许多观点是这样的:在面向对象编程(OOP)中,组合往往是比继承更好的选择,你观点是怎样的?

继承(Inheritance)是面向对象编程的基础,如果没有继承就不能面向对象编程,另外两个是封装多态。 组合(Composition)几乎所有语言都支持,也是人们思维的方式,比如一张椅子,包含了四条腿;一堵墙由砖头和水泥合成等等。

继承包含了语义上的继承,通常将一个概念从抽象到具体化排列开来,通过一个子树的形式将继承的组织起来;而且继承也将对象中的字段和方法 能够被重用。 继承往往被错误使用,下面的例子

class Stack extends ArrayList {
    public void push(Object value) { … }
    public Object pop() { … }
}

这个类完成Stack功能那个,这个类只需要提供 poppush 方法,但是通过继承的方式,也获得了get, set, add等方法,这样通过继承获得缺陷有:

  • 在语义上,Stack并不是ArrayList, 也就是不满足is-a的条件;
  • 在机制上,继承破坏了封装,ArrayList的应该向Stack使用者隐藏起来;
  • 通过ArrayList来实现Stack,这是一种跨领域的关系,ArrayList是随机访问的结构,而Stack则是FILO访问的结构。

那么下面再举一个误用继承的例子

class CustomGroup extends ArrayList<Customer> {
    //....
}

这也是一个跨领域继承,ArrayList<Customer>是一个集合,是一个implmentation类,而CustomGroup则是domain类, 任何domain类应该使用implementation类,而不是继承它们。 总而言之

除了你在创建implementation类,否则都不应该使用继承。

8 什么叫反腐化(Anti-corruption)层?

当一个应用程序从从原先的设计中向新的架构设计迁移,因为迁移的过程是逐步的,所以新的架构仍然需要调用原先接口。但是新的架构维持调用接口是非常费时费力,所以在调用中间增加一个反腐化层(Anti-corruption Layer)。同样问题也会出现在我们调用的外部模块的接口,该模块在设计上有质量的问题,通过反腐化层,来避免设计上的缺陷。 下图是反腐化层设计的示意图:

上图中应用程序包含了两个子系统,子系统A在调用子系统B的时候通过了反腐化层。子系统A和反腐化层的通信使用的子系统A的数据模型和架构,而反腐化层和子系统B的通信使用子系统B的数据模型和架构。在使用反腐化层设计的时候,需要考虑以下几点:

  • 反腐化层可能增加两个子系统之间调用的延迟;
  • 反腐化层增加的服务必须可以被管理和维护的;
  • 考虑反腐化层如何扩展;
  • 考虑是否可以增加多个反腐化层,因为可能将一个功能拆分成多个服务;
  • 确保在反腐化层数据一致性保证可监控;
  • 考虑反腐化层需要处理不同子系统的消息;
  • 考虑反腐化层在架构迁移后是否退出整个应用程序;

那么反腐化层设计和设计模式的Adapter模式和Facade模式有什么区别呢?反腐化层用在哪些设计有缺陷的子系统或者模块中,而Adapter模式和Facade模式则不认为原先设计有什么问题,而是目前的需求无法满足,需要进行改造来完成特定需求。

9 单例模式设计模式限制了每一个类只能创建唯一的对象,你能否写一个线程安全的单例模式?

go语言中,使用字母大小写来控制访问权限,因此最简单的单例模式是将类型的和字段名全部小写,使在package外面无法构造实例,通过唯一的Instance方法获取实对象实例。同时为了保证线程安全,使用锁机制。

package animal
type dog struct {
    name string
}

var instance *dog
var mux sync.Mux

func Instance() *dog {
    if instance == nil {
        mux.Lock()
        defer mux.Unlock()
        //double check
        if instance == nil {
            instance = new(dog)
        }
    }
    return instance
}

该单例模式事项为懒汉模式,在需要的时候调用Instance方法才会构造单例,还有一种方法是饿汉模式,在初始化的时候就创建好单例。在go中,每一个package包中init函数是初始化执行的。因此我们可以这样设计:

package animal
type dog struct {
    name string
}
var Dog *dog
func init(){
    if Dog == nil {
        Dog = new(dog)
    }
}

那么所有引用这个animal.Dog都是同一个实例对象。

10 如何处理依赖灾难(Dependency Hell)

依赖灾难主要有以下几种形式:

  1. 大量依赖:一个应用程序依赖大量的外部模块,运行程序需要安装外部依赖,这些依赖占用大量的磁盘空间,而且很多应用只使用了一小部分功能;
  2. 长依赖链:一个应用程序依赖liba,它又依赖libb, 而他又依赖libc等等,运行程序需要依次安装多个依赖,一旦这些依赖产生冲突就会导致接下来的问题:
  3. 依赖冲突:如果应用程序A依赖libfoo 1.2,而应用程序B依赖libfoo 1.3,那么应用程序A,B将不能同时安装运行;
  4. 循环依赖:如果应用程序application A依赖于应用程序applicaiton B,而应用程序application B却依赖于应用程序applicaton A

解决方案

  1. 语义化版本管理,软件版本通常用x.y.z表示,其中xMajor version,当发生API不兼容的时候更新这个Major version;yMinor version,当内部发生功能性改变,更新这个版本号;而zPatch version, 是修复bug时候更新的版本。
  2. 私有应用程序版本,每一个应用程序使用各自的依赖版本,而不是应用公共的或者系统依赖;
  3. 更小的包管理机制。

11 goto语句是邪恶的吗?你或许听过一篇由Edsger Dijkstra写的著名论文 Go To Statement Considered Harmful,在这篇论文中他批评了goto语句,并且推崇结构化编程。 使用goto通常非常有争议,甚至Dijkstra这篇文章也被批评了,诸如'GOTO Considered Harmful' Considered Harmful,那么你的观点是怎样的?

Go To Statement Considered Harmful Dijkstra 发现在程序设计中,程序的质量和使用的GoTo语句数量成法反比,因此呼吁在高级语言中取消GoTo,因为这些完全可以使用选择循环来完成。他给出的理由如下:

  • 程序的生命流程不单单是完成所有代码,还有程序在实际运行的时候全部过程;
  • 开发人员在动态流程掌控能力上开发远远落后于静态关系掌握。

在程序开发中,程序代码的流程称为 Program ,程序在实际运行的流程称为 Process ,两者的差异越小越能容易掌控这个程序。假设一个程序��是顺序执行的,也就是说只有赋值语句,那么 Program 的索引和 Process �的索引是一致的,同样对于条件语句程序也是同样如此。

但是如果程序包含了子过程 Procedure,那么Program 的索引和 Process 的索引开始不一致了,因为每一个过程内部也包含各自的Program和运行的时候的Processs;对于循环语句,从某种程度上来讲也是多余的,因为都可以用递归来表达。但是由于�我们的思维方式更加熟悉归纳模式,循环语句是值得�保留的,每次进入循环,运行时的动态索引发生内置�嵌套,所以变得复杂起来;对于GOTO语句,则完全放弃了动态运行时候的坐标,这个给程序�掌控带来巨大的灾难。

总体而言 GOTO 语句打破了程序的结构化流程,在一般的开发中�应该避免使用,但是在底层的开发中,比如��汇编,类似GOTO的跳转语句被广泛使用。在开发过程中,也可以为了程序的简洁性,�统一的退出可以使用GOTO语句来控制。

12 鲁棒性原则是软件开发中广泛的采用的原则,通常用 对你的输出按照约定,对你的输入保持宽容(Be conservative in what you send, be liberal in what you accept),你能说说这个原则的合理性吗?

该原则的目标是构建稳健的系统,假设开发了一套系统,该系统对输入的集合元素求和,如果实现方式如下

public int Sum(List<int> arr)
{
    int sum = 0;
    foreach(var elem in arr)
    {
        sum += elem;
    }
    
    return sum;
}

看上去还不错,但是这里违反的鲁棒性原则中

对你的输入保持宽容

因为这个方法只接受 List<int> 类型,而在使用程序过程中没有使用 List 其他功能。这个方法并没有对其他类型的宽容。

- public int Sum(List<int> arr)
+ public int Sum(IEnumberable<int> arr)
{
    int sum = 0;
    foreach(var elem in arr)
    {
        sum += elem;
    }
    
    return sum;
}

在这里将参数类型调整为 IEnumberable<int>,那么这个方法可以接受任何实现 IEnumberable<int> 的类型。这就是 对你的输入保持宽容

对于方法的返回值,鲁棒性体现为

对你的输出按照约定

public FileStream Read(string filePath)
{
    return new FileStream(filePath, FileMode.Read);
}

Read 方法中,我们返回值是 FileStream 类型,但是从语义的角度来看,只需要返回返回 Stream 类型,而不是特定的 FileStream。所以修改为

- public FileStream Read(string filePath)
+ public Stream Read(string filePath)
{
    return new FileStream(filePath, FileMode.Read);
}

13 改变实现方式而不影响客户端的能力叫做数据抽象 Data Abstraction,那么编写一个违反这个属性的例子,并且修复它。

todo

14 编写一个代码片段并且违法DRY (Don't Repeat Yourself)原则,并且修复它。

todo

15 问题拆分(Separation of Concerns)是一种设计原则,它将编程问题划分到不同的领域,每一个领域关注自己的的问题。有许多不同的机制来完成这个目标,比如使用对象,函数,模块,或者MVC等等。 你能讨论一下这个话题吗?

todo