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来促进所选择的低级组件实现的运行时供应到高级组件
- 注入类可以在测试中轻易模拟