RSS

Monthly Archives: July 2011

关于类继承与Liskov替换原则

续《When I click “object-oriented”…》

派生类重写父类方法可能违反Liskov替换原则?

如果父类仅仅声明了抽象方法,而各派生类分别实现了该方法,那么就如同实现接口一样,可达到多态的目的;

如果父类实现了某方法,那么它对外所描述的行为也就确定了,派生类如果重写这个方法,那么就修改了这个行为,当派生类实例被父类型的引用使用时,表现出的行为与父类本身的实例不相符,即违反了Liskov substitution principle。

举个例子

现实中,正方形是矩形的一种特殊形式。

现在先有Rectangle作为父类,具有height和width两个property(有邪恶的getter/setter);再有Square继承Rectangle做子类,由于Square长与高相等,所以重写height和width的set方法为同时设置长与高。至此似乎是设计与现实完全相符,接着有人这样调用了:

void  tryAreaCalculation(Rectangle rectangle) {

     rectangle.setHeight(4);
     rectangle.setWidth(5);

     assertEquals(20, rectangle.area());
}

....

tryAreaCalculation(new Square());

结果assertion失败,“expect: 20, but was 25.” 也许我们会说,明知道传入的是Square对象还写那样的assertion是不可能出现的,但不清楚程序的人如果只看tryAreaCalculation方法,就会认为“矩形面积=长×高=4×5=20“是理所当然的。这显然不是我们想看到的事。

当然,这个例子并不是Liskov替换原则所针对问题的典型例子,在以“不择手段地复用代码”为目的的继承例子中(class Students extends List<Student>),大家更能明白Liskov替换原则的意义,以及对组合和继承的选择应首先去考虑面向对象的模型。

不要让OO的眼光被具体编程语言遮挡

是不是觉得我把类继承妖魔化了呢?有没有因为担心人类被官员类继承就不敢给人类添加“说话”的方法呢?不必这样。我们在用面向对象编程时要清楚,什么是从面向对象建模角度认为对的事,什么是所使用的编程语言能够做到的事。

如果你手上的是Java语言,发现官员会说话,人也会说话,官员又是人的一种,那么就在人类中实现“说话”这个方法,再让官员类继承人类(即便在未来可能会在官员类中重写说话方法,加入说空话的语句),这一切已是你能做到的合理的事了。但请心里清楚,这并不是使用OOP对待此模型的最正确方案,因为Java语言的限制。

如果编程语言有了如Mixin的特性(例如Ruby中的Mixin、Scala中的trait,下面以Ruby为例),那么你会发现在这个问题上你能有更好的解决方案。为了官员、教授等也是人的事实,我们依旧让这些类继承人类,但除了固有属性如身高、性别,以及固有行为如睡觉,不继承任何可变的因素;把可能共用的因素都单另放在Module中,之后在需要的类中Mixin进来,如为程序员、司机、中学生等类Mixin普通“说话”方法所在的Module,而在官员类中定义含说空话逻辑的“说话”方法。

总结下经验

  • 设计时的考虑,应首先尊重使用面向对象思想的建模,其次参照具体编程语言的特性,第三考虑与已有设计的配合和约定,必要时坚持正确的事去争取。
  • 用接口描述行为,各类通过实现接口来定义行为的具体内容;留意接口隔离(ISP)。
  • 用基类抽象固有属性和行为,减少public方法,合理使用Template method模式;子类(尽量)不去重写继承自基类的行为,只添加新属性和行为(LSP, OCP)。
  • 用组合描述以借用其他对象的行为为目的的设计。
 
Comments Off on 关于类继承与Liskov替换原则

Posted by in Uncategorized

 

When I click “object-oriented” …

它是一种实践

面向对象不是编程模型而是一种人们总结出的实践,它优于过程化或模块化编程方式。

哲学

它的哲学是拉近现实物体或概念与程序实体之间的距离,不仅将程序中的参与单元看作对象并将对象归类,而且在对现实物体以对象的方式建模中,将各对象作为属性的数据纳入各自结构,并与其相关行为组织在一起;另外,还通过继承或组合等方式描述实体间的存在的数据或行为的关系。

形式

面向对象编程语言有两种形式,基于类型(class-based)和基于原型(prototype-based)。

  • 基于类型的形式自C++时代就确立了在静态语言中的绝对地位,它明确提出类型的概念,认为对象都是类型的实例化;类型通过继承的方式重用其他类型的属性和方法。可以用“is-a”来解释上述两关系:我们可以说对象是一个类型的具体化,也可以说继承自类型B的类型A是类型B的一个派生。基于类型的形式在重用其他类型的行为方面的还有另一种方式叫组合,它将类型B的对象作为类型A的属性,以便直接使用。可以用”has-a”来解释组合:类型A中有一个类型B的对象做属性。
  • 基于原型的形式也可被称为类型无关的或基于实例的编程形式,它最出名的实现语言莫过于javascript。除了直接创建自身对象之外,它以其他对象作为原型通过克隆(clone)的方式重用其他对象的行为。通过对象得到新型对象,并在被调用时追溯原型对象中的方法,支持这种编程方式的语言特性是委托(delegation)。

OOP核心

  • 封装(encapsulation),它是能称为面向对象编程的最重要的特点,它将对象自有的属性和行为封在类型中,限制内外的访问接口,在不同种类的类型之间划清界线,明确了模型,保证了数据安全。
  • 继承(inheritance),它实现了类型级别的抽象和代码重用。
  • 多态(polymorphism),它为面向对象设计提供了充足的自由度。它能通过接口级别的抽象,利用类型匹配和晚绑定,实现从多种表现形式中为对象或方法做出选择。

OOD原则

  • 单一职责原则(single responsibility principle)。封装的目的是将对象自身的行为限制在自身类型中,而该原则进一步强调类型应当专注,只因一个变数而变化,所有的行为集合体只在扮演一个角色,承担单一的职责。引申的,其中所有的方法也只各做一件事。
  • 开放封闭原则(open-close principle)。对扩展开放,对修改封闭。经常考虑的是现在如何设计才能在未来做扩展的同时,能尽量不修改现有系统的代码,或能够将修改降低到少数的几个地方。
  • Liskov替换原则(Liskov substitution principle)。它要求所有子类的对象在代替其父类被使用时都不会得到错误的结果,这是在保证以多态方式使用时不会出错;若该原则被打破,那么开放封闭原则多半也同时被打破了。这是一个比较苛刻的原则,子类重写父类方法似乎变得不可能了,可是仔细想想会发现,似乎当初对父类抽象得还不够彻底。
  • 依赖倒置原则(Dependency inversion principle)。高层模块需要调用低层模块的方法,但不能直接依赖,而是应让两者都依赖于接口。这是该原则的第一层意思,它的初衷是强调要面向接口编程,而不是面向实现,达到降低耦合的目的。这基本是对的,不过我觉得接口被过分强调了,应该在未来可能会扩展的地方考虑接口的使用,不要过度设计徒增代码。该原则还有第二层意思,抽象不应依赖于细节,细节应该依赖于抽象,这才是“倒置”二字的本意。因为我们往往是由简单到复杂,由单一类型到多种类型地在扩展程序,所以为有共同特点的类做抽象常是在已有实现类之后决定的,这时要考虑抽象是否彻底、是否有对实现的依赖。不过我觉得这种随机应变的抽象手法问题不大,大可不必一开始就死磕接口,那才是过度设计了,拥抱变化不是白说的。可以实现分析下问题,选择合适的设计模式是个好主意。
  • 接口隔离原则(Interface segregation principle)。这个原则比较简单,它针对的是过多的接口集中在一起的问题。就像为类划分单一的职责一样,也应为接口考虑较单一的分类,不要吧所有接口方法都堆在一个接口中。好处是客户可以有选择地实现特定的接口,而不实现无关的,实现解耦合。
———————-
后面一篇文章中对类继承和Liskov替换原则有更多讨论
 
Comments Off on When I click “object-oriented” …

Posted by in Uncategorized

 

利用Mailbox为日志型web应用服务

很多时候,我们都以类似日志的方式做记录,而回顾历史时,我们想看到的不只是一条条的历史记录,更多的是期望能以某种方式对记录信息做聚集,并有组织地、最好是华丽丽地展现出来。为这类需求去开发web应用,如果仍采用典型的Browser <–> Web server <–> Database 的设计(如古老的S.S.H.常做的那样)是不好的。

  • 原因一,数据库大材小用。单纯的数据内容加上更加单纯的关系,如果用一关系型数据库,那真是浪费;
  • 原因二,Web form提交记录的方式死板。日志型的信息何必要以登录后提交页面的方式记录,太过正式了吧,为什么不是发一条微博或者一封邮件就顺带着记录了呢?

如果考虑到微博的私密性不够,那么就邮件吧:让你的邮箱为你忠实记录下这些日志型的记录——用约定的字眼为邮件打上标签后发送,让设计的web应用从你指定的收件箱中抓取此类邮件,分析内容获得历史记录,适当地做信息聚集,然后将你的记录信息华丽地展现在页面上(比如这样)。

应用的设计框架画成下图。

可以看到设计中很重要的一点是周期性地将内存维护的数据与mailbox同步,这一步的工作应该是交给独立的后台线程去做的。不采用实时访问mailbox取数据的原因是显而易见的——慢,并发就更难了;同时,考虑到记录本身的数据量不大,实时性也无苛求,所以维护内存数据库在这里十分划得来。

这种设计的好处还有:

  • 多个用户的记录都通过邮件发送到同一个邮箱,邮件本身除了标题内容之外还记录了发件人、时间等,我们的web应用可以直接利用它们;
  • 通常web应用都不免被基于数据库的CRUD困住脱不开身,而上图的设计却没有为我们这个需求简单的应用添麻烦,开发人员可把精力更多地放在提取信息和创建丰富的页面展现上。

最后回归到代码。使用“mail.jar”访问邮件的代码可以参考下面的demo. 不要错引或多引关于mail的jar包哦,因为疏忽曾引入了两个不同的但都关于mail的jar包,以致跑起的程序取不到邮件却又不报错误,本人吃过这个亏了,所以希望读者小心。

import com.sun.security.sasl.Provider
import java.security.Security
import java.util.Properties
import javax.mail._

class MailFetcher {

  def connect(username: String, password: String) : Store = {
    Security.addProvider(new Provider)

    val SSL_FACTORY: String = "javax.net.ssl.SSLSocketFactory"
    val pop3Props: Properties = new Properties

    pop3Props.setProperty("mail.pop3.socketFactory.class", SSL_FACTORY)
    pop3Props.setProperty("mail.pop3.socketFactory.fallback", "false")
    pop3Props.setProperty("mail.pop3.port", "995")
    pop3Props.setProperty("mail.pop3.socketFactory.port", "995")

    val session = Session.getInstance(pop3Props, null)
    val store = session.getStore("pop3")
    store.connect("pop.gmail.com", 995, username, password)

    store
  }

  def fetchMailsFromFolder(folderName: String, store: Store) : Array[Message] = {
    val folder = store.getFolder(folderName)
    folder.open(Folder.READ_ONLY)
    folder.getMessages();
  }

  def printMessagesInfo(messages: Array[Message]) {
    printTotalCount(messages.length)
    printSubjects(messages)
  }

  private def printTotalCount(count: Int) {
    println("Total count of emails: " + count)
  }
  private def printSubjects(messages: Array[Message]): Unit = {
    messages.foreach(msg => println(msg.getSubject))
  }

}

object MailFetcher {
  def main(args: Array[String]) {

    val mailFetcher = new MailFetcher

    val store = mailFetcher.connect("your.account@gmail.com", "your.password")

    val messages = mailFetcher.fetchMailsFromFolder("INBOX", store)

    mailFetcher.printMessagesInfo(messages)
  }
}
 
Comments Off on 利用Mailbox为日志型web应用服务

Posted by in Uncategorized

 

Scala 初学乍练

《Programming in Scala》确实不是本新书,而且它700+页的厚度也挺吓人,但对以初学一门语言为目的的我来说,它的思路和讲解却已足够清楚。这本书或许不能痛快地直奔scala核心思想,但从语言层面开始学习和实践的角度,有了它确实挺好。

类型推断
    è®°å¾—java中一次一次地写对象引用的类型声明吗?对象自实例化之初就明确了它自己的类型,为什么还非要指定其引用的类型呢?即便是如Fruit fruit = new Apple();使用抽象类型或接口,在更新语言看来也是多此一举。如C#3.0起引入var作为为引用声明的类型的placeholder,为编译服务,var fruit = new Apple(); Scala自然也看这种类型显示指定不顺眼,于是直接省略掉,辅助以类型推断来理解省略的类型信息, val fruit = new Apple. val为不可变变量的标志。

val 、var声明
    val array = Array(1,2,3)  中,array这个引用不能改为引用其他对象,而Array对象本身可更改。
    for(param <- params)  …  句中,param是val的,即不能通过此for循环直接遍历params数组并逐一改变元素值。

operator
    æ²¡æœ‰operator,所有如+,-,*,/的操作都是方法,1+2 实际上是 (1).+(2),即1上调用+方法且参数为2
    è§„则:如果一个方法被用作操作符标注,如 a * b,那么方法被左操作数调用,就像 a.*(b)——除非方法名以冒号结尾。这种情况下,方法被右操作数 调用。因此,1 :: twoThree里,::方法被twoThree调用,ä¼ å…¥1,像这样:twoThree.::(1)。

tuple 现代怪胎
    ä½¿ç”¨ï¼ˆelement1,element2,….)的方式创建的不可变多元组,元素类型不统一,使用诡异tupleObject._N方式访问各元素,其中N从1开始而非起自0的典型方式。源自Haskellå’ŒML语言从1开始计索引的现代怪胎。

绕人的mutable和immutable
    å…ˆä¸è¦æç‰¹è´¨ï¼ˆtrait)了,只看immutable.Set å’Œ mutable.Set到底差多少?前者的元素内容不可被更改,后者可以,且其set对象是可扩充的。以含有 += 操作的如下代码为例
————————————————————————————————————————————————–
    var immutableSet = Set(1, 2)    // 不特别import的情况下,scala.collection.immutable.Set是默认Set类型
    val mutableSet = scala.collection.mutable.Set(a, b)

    immutableSet += 3                    // 等同于 immutableSet = immutableSet + 3
    mutableSet += c                        // 调用mutable Set的方法,等同于mutableSet.+=(3)
                                                    // 因为是val声明,所以保证mutableSet仍是原对象

    immutableSet.foreach(print)     // 输出:123
    mutableSet.foreach(print)         // 输出:abc
————————————————————————————————————————————————–

函数式编程方式
    æŒ‡ä»¤å¼çš„编程方式也是scala语法的一部分,但却常常闻不到scala的美味。
    å“²å­¦
        æŠŠæ¯ä¸ªæ–¹æ³•当作是创建返回值的表达式。鼓励你分解大的方法,制造多个小方法。
    è®¾è®¡/重构的目标
        ä½¿ç”¨val而非var
        ä½¿ç”¨immutable对象
        ç¼–写易于测试的且职责单一的方法
    ç¨‹åºé£Žæ ¼
        æ–¹æ³•最后一个表达式的计算结果即是返回值,显示调用return是备用方案
        å•行方法体合并到方法声明同一行
        ç”¨è¿‡ç¨‹ï¼ˆprocedure)风格编写无结果类型(Unit型)的方法
        [编译原因] 中缀操作符放在行尾而不是行首,如拼接字符串时,放在行首是单独的语句

伴生对象(companion object)
    val list = List(1,2,3)        ==>    val list = List.apply(1,2,3)    其中apply方法是List的伴生对象的方法,而非java中的static方法。scala没有静态成员。
    å½“单例对象与某个类共享同一个名称时,他被称作是这个类的伴生对象(companion object)。你必须在同一个源文件里定义类和它的伴生对象。类被称为是这个单例对象的伴生类(companion class)。类和它的伴生对象可以互相访问其私有成员。

 
Comments Off on Scala 初学乍练

Posted by in Uncategorized