SOLID: The Principles of Object Oriented Design
Single Responsibility Principle
A class should have one and only one reason to change, meaning that a class should only have one job
一個 class(或一支 function)只專注在做一件事情,這裡的一件事情是要根據你的 business logic 去定義的,為什麼要職責分明?因為這又關係到到時候你寫的測項範圍能足夠收斂,讓你在測試出錯的時候,盡快找出問題。
Bad
class SalaryCalculator{
public int calculateSalary(Employee employee) {
//...
int taxDeduction = calculateTax(employee);
//...
}
private int calculateTax(Employee employee) {
switch (employee.getType()) {
case FULL_TIME:
//...
case CONTRACTOR:
//...
}
}
}
Better
分開計算薪資跟計算稅務的邏輯
class SalaryCalculator{
private TaxCalculator taxCalculator;
public int calculateSalary(Employee employee) {
//...
int taxDeduction = taxCalculator.calculateTax(employee);
//...
}
}
class TaxCalculator{
private int calculateTax(Employee employee) {
switch (employee.getType()) {
case FULL_TIME:
//...
case CONTRACTOR:
//...
}
}
}
Open Closed Principle
Depend on stable abstractions and modify system’s behavior by providing different realizations
close 是指要有一個不會改變的抽象,open 是指要能夠提供彈性的實作。
Bad
class SalaryCalculator{
private TaxCalculator taxCalculator;
public int calculateSalary(Employee employee) {
//...
int taxDeduction = taxCalculator.calculateTax(employee);
//...
}
}
class TaxCalculator{
private int calculateTax(Employee employee) {
switch (employee.getType()) {
case FULL_TIME:
..
case CONTRACTOR:
..
}
}
}
Better
利用簡單工廠模式跟策略模式來實現
interface TaxCalculator{
int calculateTax(Employee employee);
}
class TaxCalculatorFullTime implements TaxCalculator{
int calculateTax(Employee employee){
// calculate full time tax
}
}
class TaxCalculatorContractor implements TaxCalculator{
int calculateTax(Employee employee){
// calculate contractor tax
}
}
class TaxCalculatorFactory{
public TaxCalculator newTaxCalculator(EmployeeType employeeType) {
switch (employeeType) {
case FULL_TIME:
return new TaxCalculatorFullTime();
case CONTRACTOR:
return new TaxCalculatorContractor();
default:
return new TaxCalculatorContractor();
}
}
}
class SalaryCalculator{
private TaxCalculatorFactory taxCalculatorFactory;
public int calculateSalary(Employee employee) {
//...
TaxCalculator taxCalculator = taxCalculatorFactory.newTaxCalculator(employee.getType());
int taxDeduction = taxCalculator.calculateTax(employee);
//...
}
}
Liskov Substitution Principle
If you have a function, that works for a base type, it should work for a derived type
如果 parent class 的某個 method 可以 work, 商業邏輯沒錯, 那繼承的 child class 的此 method 也同樣要能 work。
Bad
class Rectangle {
constructor(width, height) {
this._width = width;
this._height = height;
}
getWidth() {
return this._width;
}
getHeight() {
return this._height;
}
setWidth(value) {
this._width = value;
}
setHeight(value) {
this._height = value;
}
getArea() {
return this._width * this._height;
}
}
class Square extends Rectangle {
constructor(size) {
super(size, size);
}
}
Better
class Rectangle {
constructor(width, height) {
this._width = width;
this._height = height;
}
getWidth() {
return this._width;
}
getHeight() {
return this._height;
}
setWidth(value) {
this._width = value;
}
setHeight(value) {
this._height = value;
}
getArea() {
return this._width * this._height;
}
}
class Square extends Rectangle {
constructor(size) {
super(size, size);
}
setWidth(value) {
this._width = this._height = value;
}
setHeight(value) {
this._width = this._height = value;
}
}
Interface Segregation Principle
A client should never be forced to implement an interface that it doesn’t use A client shouldn’t be forced to depend on methods they do not use
不應該實作多餘用不到的 method,可以再想想該怎麼定義(切細)你的 interface。
Bad
interface IAnimal {
fly(): void;
swim(): void;
}
class Bird implements IAnimal {
fly() {
console.log("I can fly!");
}
swim() {
throw Error("I cannot swim!");
}
}
class Frog implements IAnimal {
fly() {
throw Error("I cannot fly!");
}
swim() {
console.log("I can swim!");
}
}
Better
interface Flyable {
fly(): void;
}
interface Swimable {
swim(): void;
}
class Bird implements Flyable {
fly() {
console.log("I can fly!");
}
}
class Frog implements Swimable {
swim() {
console.log("I can swim!");
}
}
class SuperMan implements Flyable, Swimable {
fly() {
console.log("I can fly very high!");
}
swim() {
console.log("I can swim very fast!");
}
}
Dependency Inversion Principle
High-level modules should not import anything from low-level modules. They should both depend on abstractions
高階模組不直接依賴於低階模組的具體實現,而是抽象化後的 interface,為什麼要這樣做? 因為可以不必急著實作,藉由先定義好介面,以幫助你確認商業邏輯有沒有問題,也能夠幫助團隊有效分工(比如說 Architect 先定義好 interface,確認好沒問題,實作接著交給 RD)
Bad
classDiagram
UserService ..> Logger: use
class UserService {
-logger: Logger
+getAll()
}
class Logger {
+log(message)
+error(message)
}
class UserService {
private logger: Logger;
constructor(logger: Logger) {
this.logger = logger;
}
}
Better
classDiagram
direction DT
UserService ..> ILogger: use
Logger ..|> ILogger: implement
class UserService {
-logger: ILogger
+getAll()
}
class ILogger {
<<interface>>
log(message)
error(message)
}
class Logger {
+log(message)
+error(message)
}
interface ILogger {
log(message: string): void;
error(message: string): void;
}
class MyLogger implements ILogger {
log(message: string) {
console.log(message);
}
error(message: string) {
console.error(message);
}
}
class UserService {
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
}