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类可以在测试中轻易模拟
Liskov替换原则
派生类必须可替代其基类。
这是开/闭原则的延伸。派生类不应更改基类的行为(继承方法的行为)。如果类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好处:
- 避免意外和不正确的结果
- 明确区分共享继承接口和扩展功能
接口隔离原理
制作客户端特定的细粒度接口。
一旦接口变得太大/太胖,我们绝对需要将其拆分为更具体的小接口。接口将由将使用它的客户端定义,这意味着接口的客户端将只知道与它们相关的方法。
// 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好处:
- 通过使更高级别的模块独立于较低级别的模块来提高其可重用性
- 可以采用依赖性注入来促进所选择的低级组件实现的运行时供应到高级组件
- 注入类可以在测试中轻易模拟