Java的SOLID编程原则 - Filippo Buletto


SOLID阐述了五种设计原则,可帮助开发人员轻松扩展和维护软件:
S - 单一责任原则
O - 开放原则
L - Liskov替代原理
I - 界面隔离原理
D - 依赖倒置原则

单一责任原则

一个类应该有一个,而且只有一个改变的理由。
一个类应该只有一个责任,这意味着类应该高度凝聚并实现强相关的逻辑。实现功能1和功能2和功能3(依此类推)的类违反了SRP。

SRP示例:

// BAD
public class UserSettingService {
  public void changeEmail(User user) {
    if (checkAccess(user)) {
       
//Grant option to change
    }
  }
  public boolean checkAccess(User user) {
   
//Verify if the user is valid.
  }
}

// GOOD
public class UserSettingService {
  public void changeEmail(User user) {
    if (securityService.checkAccess(user)) {
       
//Grant option to change
    }
  }
}
public class SecurityService {
  public static boolean checkAccess(User user) {
   
//check the access.
  }
}

SRP味道

  • 单个类中不止一个上下文分隔的代码
  • 测试中的大型setup初始化设置(TDD在检测SRP违规时非常有用)

SRP好处
  • 负责给定用例的分隔类现在可以在应用程序的其他部分中重用
  • 负责给定用例的分隔类现在可以单独测试

开/闭原则
您应该能够扩展类行为,而无需对其进行修改。
类应该打开以进行扩展并关闭以进行修改。您应该能够扩展类行为而无需修改其实现:

// BAD
public class Logger {
  String logging;
  public Logger(String logging) {
    this.logging = logging;
  }
  public void log() {
    if (
"console".equals(logging)) {
     
// Log to console
    } else if (
"file".equals(logging)) {
     
// Log to file
    }
  }
}

// GOOD
public interface Log {
    void log();
}
public class ConsoleLog implements Log {
  void log() {
   
// Log to console
  }
}
public class FileLog implements Log {
  void log() {
   
// Log to file
  }
}
public class Logger {
  Log log;
  public Logger(Log log) {
    this.log = log;
  }
  public void log() {
    this.log.log();
  }
}

OCP代码味道:
  • 如果你注意到类X直接引用其代码库中的其他类Y,则表明类Y应该传递给类X(通过构造函数/单个方法),例如通过依赖注入
  • 复杂的if-else或switch语句

OCP好处:
  • 使用封装在单独类中的新功能可以轻松扩展X类功能,而无需更改类X实现(它不知道引入的更改)
  • 代码松散耦合
  • 注入的Y类可以在测试中轻易模拟


利斯科夫替代原则
派生类必须可替代其基类。
这是开/闭原则的​​延伸。派生类不应更改基类的行为(继承方法的行为)。如果类Y是类X的子类,则任何引用类X的实例也应该能够引用类Y(派生类型必须完全替代它们的基类型)。

// BAD
public class DataHashSet extends HashSet {
  int addCount = 0;
  public boolean function add(Object object) {
    addCount++;
    return super.add(object);
  }
 
// the size of count will be added twice!
  public boolean function addAll(Collection collection) {
    addCount += collection.size();
    return super.addAll(collection);
  }
}

// GOOD: Delegation Over Subtyping
public class DataHashSet implements Set {
  int addCount = 0;
  Set set;
  public DataHashSet(Set set) {
    this.set = set;
  }
  public boolean add(Object object) {
    addCount++;
    return this.set.add(object);
  }
  public boolean addAll(Collection collection) {
    addCount += collection.size();
    return this.set.addAll(collection);
  }
}

LSP代码味道:

  • 如果它看起来像一只鸭子,嘎嘎叫像鸭子但需要电池才能达到这个目的 - 这可能违反了LSP
  • 修改子类中的继承行为
  • 在重写的继承方法中引发的异常

LSP好处:
  • 避免意外和不正确的结果
  • 明确区分共享继承接口和扩展功能

接口隔离原理
制作客户端特定的细粒度接口。
一旦接口变得太大/太胖,我们绝对需要将其拆分为更具体的小接口。接口将由将使用它的客户端定义,这意味着接口的客户端将只知道与它们相关的方法。

// BAD
public interface Car {
  Status open();
  Speed drive(Gas gas);
  Engine changeEngine(Engine newEngine);
}
public class Driver {
  public Driver(Car car) {}
  public Speed ride() {
    this.car.open();
    return this.car.drive(new Gas(10));
  }
}
public class Mechanic {
  public Mechanic(Car car) {}
  public Engine fixEngine(Engine newEngine) {
    return this.car.changeEngine(newEngine);
  }
}

// GOOD
public interface RidableCar {
  Status open();
  Speed drive(Gas gas);
}
public interface FixableCar {
    Engine changeEngine(Engine newEngine);
  }
public class Driver {
 
// Same with RidableCar
}
public class Mechanic {
 
// Same with FixableCar
}

ISP代码味道
  • 由许多类实现的一个胖接口,其中没有一个类实现100%的接口方法。这种胖接口应该分成适合客户需求的较小接口

ISP好处
  • 高度凝聚力的代码
  • 避免使用单个胖接口在所有类之间进行耦合(一旦单个胖接口中的方法得到更新,所有类 - 无论是否使用此方法 - 都被迫相应地更新)
  • 通过将职责分组到单独的界面中,明确分离业务逻辑

依赖倒置原则
依赖于抽象,而不是实现
如果您的实现细节将取决于更高级别的抽象,它将帮助您获得正确耦合的系统。而且,它将影响该系统的封装和内聚。

// BAD
public class SQLDatabase {
  public void connect() {
    String connectionstring = System.getProperty(
"MSSQLConnection");
   
// Make DB Connection
  }
  public Object search(String key) {
   
// Do SQL Statement
    return query.find();
  }
}
public class DocumentDatabase {
 
// Same logic but with document details
}

// GOOD
public interface Connector {
  Connection open();
}
public interface Finder {
  Object find(String key);
}
public class MySqlConnector implements Connector {}
public class DocumentConnector implements Connector {}
public class MySqlFinder implements Finder {}
public class DocumentFinder implements Finder {}

public class Database {
  public Database(Connector connector,
                  Finder finder) {
    this.connector = connector;
    this.finder = finder;
  }
  public Connection connect() {
    return connector.open();
  }
  public Object search(String key) {
    return finder.find(key);
  }
}

DIP味道:

  • 在高级模块中实例化低级模块
  • 调用低级模块/类的类方法

DIP好处:

  • 通过使更高级别的模块独立于较低级别的模块来提高其可重用性
  • 可以采用依赖性注入1来促进所选择的低级组件实现的运行时供应到高级组件
  • 注入类可以在测试中轻易模拟