Skip to main content

面向对象设计 - 入门

info

OOD 面向对象设计 对于 Entry level SDE 考察综合素质. 实现系统的 Viability, 典型例题是 Design Elevator System.

前置知识

封装 Encapsulation

  • Class
  • Object
class Animal {};

Animal a = new Animal();
class Employee {
// access modifier
private String name;
private float salary;
private int level;

// 外部可调用
public void raiseSalary();
public void printName();
public void promoteLevel();

public Sring getName();
}

继承 Inheritance

  • It describes a IS-A relationship.
  • 子类继承父类里所有不是 private 的 attributes.
  • 父类 = Base class / Parent class, 子类 = Sub class.
  • super keyword will call Base class's same name method.
  • final class 不能被继承.
  • abstract class can not 初始化.
class Animal {

public void description(){
System.out.println("This is a general animal");
}

protected String name;
public int id;
private String privacy;
}

class Dog extends Animal {

// override
public void description() {
System.out.println("This is a Dog");
System.out.println("Name -> " + name);
System.out.println("Id -> " + id);
// System.out.println("Privacy -> " + privacy); // WRONG
}

// overload
public void description(String type) {
System.out.println("This is a " + type);
}

}
Dog dog = new Dog();
dog.description();
dog.description("Cat");
class Dog extends Animal {
public void description() {
super(); // This will call Base class's same name method.
}
}

Interface

  • Interface can have only abstract methods.
  • Interface can't provide the implementation of abstract class.
interface Service {
// No constructor
// interface里的函数都是抽象函数,不可以有Implement.
// Interface can have only abstract methods.
public void serve();
public void retire();
}


class Dog implements Service {
public void serve() {
// dog in service
}

public void retire() {
// dog retire from service
}
}

多态 Ploymorphism

abstract class Animal {
public abstract void makeSound();
}

final class Dog extends Animal {
public void makeSound() {
System.out.println("Woof !");
}
}


class Cat extends Animal {
public void makeSound() {
System.out.println("Meeow !");
}
}

Animal animal1 = new Dog(); // CORRECT
Animal animal2 = new Cat(); // CORRECT

// the same Animal class, the same makeSound() method -> the different output.
animal1.makeSound(); // Woof !
animal2.makeSound(); // Meeow !

枚举变量 Enum

  • 可读性好.
  • 防止被外部传入错误的参数, like 4, 5, 6.
public enum TrafficSignal {
// defined in compile time
RED, YELLOW, GREEN
}

public class Testing {
TrafficSignal signal = TrafficSignal.RED;
}

异常处理 Exception

  • Checked Exception (IO Exception, Compile time exception)
  • Unchecked Exception (Runtime, Exception, NPE)
class MyException extends Exception {
public MyException(String s) {
super(s);
}
}

class Testing {
public void test() {
try {
throw new MyException("My exception");
}
catch (MyException ex) {
System.out.println(ex.getMessage()); // you will get "My exception"
}
}
}

Testing test1 = new Testing();
test1.test();


public class ExceptionTest2 {

public void test() throws MyException {
if (true) {
throw new MyException("My exception");
}
}

public void test1() {
// test(); // WRONG
}

public void test2() throws MyException {
test(); // CORRECT
}

// 用try catch handle exception
public void test3() {
try {
test(); // CORRECT
} catch (MyException ex) {
System.out.println(ex.getMessage());
}
}

public static void main(String[] args) throws MyException {
ExceptionTest2 t = new ExceptionTest2();
t.test2();
t.test3();
}
}

S.O.L.I.D 原则

Single responsibility principle 单一责任原则

一个类应该有且只有一个去改变它的理由,意味着一个类应该只有一项工作.

// 只负责 calculate area
public class AreaCalculator {
private float result;

public float getResult() {
return this.result;
}

public float calculateArea(Triangle t) {
this.result = h * b / 2;
}
}

// 只负责 print
public class Printer {
public printInJson(float number) {
jsonPrinter.initialize();
jsonPrinter.print(this.result);
jsonPrinter.close();
}
}

Open close principle 开放封闭原则

对象或者实体应该对扩展开发,对修改封闭 (Open to extension, close to modification).

public interface Shape {
public float getArea();
}

public class Triangle implements Shape {
public float getArea() {
return b * h / 2;
}
}

// 不需要改变,只需要取定义新的形状
public class AreaCalculator {

private float result;

public float getResult() {
return this.result;
}

public float calculateArea(Shape s) {
this.result = s.getArea();
}
}

Liskov substitution principle 里氏替换原则

任何一个子类或者派生类应该可以替代它们的基类或者父类.

Interface segregation principle 接口分离原则

不应该强迫一个类去实现它用不上的接口.

public class 2DShape {
abstract public float calculateArea();
}

public class 3DShape {
abstract public float calculateVolumn();
}

public class Rectangle extends 2DShape {
// Good design
}

public class Cube extends 3DShape {
// Good design
}

Dependency inversion principle 依赖反转原则

抽象不应该依赖于具体实现,具体实现应该依赖于抽象.

// interface是抽象的class
// 有class想实现接口的时候, 需要实现接口内所有的函数
public interface Shape {
public float getArea();
}

public class Triangle implements Shape {
public float getArea() {
return b * h / 2;
}
}

public class AreaCalculator {

private float result;

public float getResult() {
return this.result;
}

public float calculateArea(Shape s) {
this.result = s.getArea();
}
}

UML 类图

UML 类符号 (Symbols)

类名称 (Class Name):

  • 类的名称出现在第一个分区中。

类属性 (Attribute):

  • 属性显示在第二个分区中。
  • 属性类型显示在冒号后面。
  • 属性映射到代码中的成员变量(数据成员)。

类操作 Operation (方法 Method):

  • 操作显示在第三个分区中。它们是班级提供的服务。
  • 方法的返回类型显示在方法签名末尾的冒号后面。
  • 方法参数的返回类型显示在参数名称后的冒号后面。操作映射到代码中的类方法

类可见性 (Visibility)

  • "+" 表示 public
  • "-" 表示 private
  • "#" 表示 protected
  • 不带符号表示 default

实战 - Design Elevator System

5C 解题法

  • Clarify: 与面试官交流,去除题目中的歧义,确定答题范围.
  • Core objects: 确定题目中涉及的类,以及类之间的映射关系.
  • Cases: 确定题目中所需要实现的场景和功能.
  • Classes: 通过类图的方式,具体填充题目中涉及的类.
  • Correctness: 检查自己的设计,是否满足关键点.

Clarify

Example: Glass of water?

water or juice or tea?

Step1. What

通过关键词(一般是名词),来确定题目范围.

关键词 1: Evelator

  • AirStairs 客梯 or Goods elevator 货梯
  • 是否需要设计两种类,如果需要它们之间是什么关系?

关键词 2: Building

  • 楼有多大/楼有多高/多少人? 通用属性,对于题目帮助不大.
  • 是否有多处可以搭乘的电梯口? 当收到电梯请求的时候,有多少电梯能够响应.

Step2. How

针对问题的规则来提问,帮助自己明确解题方向.

  • 按下电梯时,如果有多台电梯,哪个电梯会响应? 同方向 > 静止 > 反向.
  • 是否能按反向的楼层? 不能.
  • 信息: 电梯至少需要三种状态,并且要知道当前在哪一层.

Step3. Who

Optional, 通过思考是否会有人的出现,来帮助确定解题范围. 一般可以考虑人的角色和人的属性,看是否题目需要.

  • 电梯系统如何获取每个乘客的重量? 电梯能够自动感应.

Core Object

如何定义 Core Object?

  • 以一个 Object 作为基础,线性思考.
  • 确定 Objects 之间的映射关系.

Cases

为什么要写 Use cases? 将要实现的功能写下来给面试官看,帮助理清思路,按照 Case 实现,作为检查的标准. 如何写 Use cases? 利用定义的 Core Object, 列出每个 Object 对应产生的 use case, 并用一句简单的话来描述.

ElevatorSystem

  • Handle request

Request

  • N/A

Elevator

  • Take external request 接受电梯系统的分配任务
  • Take internal request 电梯内部可以按按钮
  • Open gate
  • Close gate
  • Check weight

ElevatorButton

  • Press button

Class

为什么要画类图?

  • 可交付, Minimal Viable Product.
  • 节省时间, 不容易在 Coding 上挣扎.
  • 建立在 use case 上,和之前的步骤层层递进,条例清晰,便于交流和修改.
  • 如果时间允许,便于转化为 code.

怎么画类图?

  • 遍历所有列出的 use cases.
  • 对于每一个 use case, 更加详细的描述这个 use case 在做什么事情.
  • 针对这个描述, 在已有的 Core objects 里填充进所需要的信息.

Use case: Take external request

An elevator takes an external request, inserts in its stop list.

如果电梯目前在 1L,有人按下了 5L 向上,之后又有人按下了 3L 向上,电梯会怎么样行动?

  • stops will be 3
  • Expected is 5 因为 3 楼更近,我们希望先去 3 楼
  • Solution: use priority queue (heap) instead of list

如果电梯目前在 1L,有人按下了 5L 向上,之后又有人按下了 3L 向上,紧接着这台电梯又被分配了一个 2L 向下的 request。这台电梯会如何行动?

  • stops will be 5 如果使用了 heap 会变成2,3,5 但是这是不合理的
  • Expected is 2 向上的动作是先按的,所以我们应该先执行向上,再执行向下的动作
  • Solution: 创建 List<Integer> upStops 存储所有向上的 request, 创建 List<Integer> downStops 存储所有向下的 request. 创建 Status status 保存当前电梯的状态,当 5L 向上的 request 进来时,我们更新 status = UP, 当 upStops 被清空了之后,我们再将 status = DOWN 来执行 downStops 中的动作

Use case: Take internal request

An elevator takes an internal request, determine if it's valid, inserts in its stop list.

  • 定义 private boolean isRequestValid(InternalRequest r)函数来判断请求是否合法,因为其是由void handleInternalRequest(InternalRequest r) 来调用,所以不需要public

如何判断一个 Internal request 是否为 Valid?

  • If elevator going up?

    • requested level lower than current level? invalid
  • If elevator going down?

    • requested level higher than current level? invalid
  • 需要新状态 int currentLevel 来表示当前的楼层

Use case: Open gate

An elevator reaches the destination level, open gate

  • 设置 void openGate函数控制开门, boolean gateOpen 状态表明门是否开了

Use case: Close gate

  • An elevator checks if overweight -> close the door;
  • then check stops corresponds to current status 如果当前 status = UP 并且 upStops中没有剩余楼层了,则去检查 downStops是否还有,如果 downStops不为空,则修改 status = DOWN if no stops left, check the reserve direction stops;
  • change status to reserve direction or IDLE 如果有没有楼层了,则 status = IDLE

Use case: check weight

An elevator checks its current weight and compare with limit to see if overweight

  • 添加新的方法 private float getCurrentWeight() 与 限重属性private float weightLimit 进行比较判断是否超重了

Use case: press button

A button inside elevator is pressed, will generate an internal request and send to the elevator. 在电梯内部按下按钮,会发送内部请求给电梯

  • ElevatorButton类需要 private int levelprivate Elevator elevator,来表明楼层和 request 发送给哪一台电梯
  • 使用 public InternalRequest pressButton() 来实现按下按钮

Correctness

从以下方面检查

  • Validate use cases 是否支持所有的 use case
  • Follow good practice 加分项,展现程序员的经验
    • E.g., InternalRequestExternalRequest均可以继承Request
  • S.O.L.I.D
  • Design pattern

Challenge

What if I want to apply different ways to handle external requests during different time of a day?

Solution: Strategy design pattern 策略设计模式

可参考设计模式-策略模式

  • 封装了多种 算法/策略, 算法/策略之间能够相互替换
  • 你可以将算法的实现和使用算法的代码隔离开来
  • 开闭原则。 你无需对上下文进行修改就能够引入新的策略