Design Principles
update Mar 12, 2020
SOLID 五大Design Principles:
- (S) Single Responsibility
- (O) Open/Closed
- (L) Liskov Substitution
- (I) Interface Segregation
- (D) Dependency Inversion
1. Single Responsibility
"A class should have one, and only one, reason to change"
Introduction
使用Single Responsibility可以降低实现难度,并且避免后续change带来的side effects。当我们在开发的过程中遇到需求变化时,如果每个responsibility对应一个独立的class,那么只有当responsibility改变的时候才需要change这个class。但是如果一个class对应了多个responsibility,则它需要被修改的频率就会提高。同时,对一个class的修改可能会导致其他依赖于它的类需要进行相应的修改,同时有可能带来更多的side effects。如何注意
在遇到需求变化的时候,有时候最简单的办法就是在现有的class中添加一个方法,或者在现有的方法中增加一些代码,但是在做这些的时候很容易违背Single Responsibility原则。我们可以在准备修改class之前问自己一个问题:“What is the responsibility of your class/component/microservice?”。如果答案中包含“and”,则需要 take a step back and rethink the current approach。Example
1. Java Persistence API(JPA)Specification
它只有一个responsibility:Defining a standardized way to manage data persisted in a relational database by using the object-relational mapping concept.
2. JPA EntityManager
Its responsibility is to manage the entities that are associated with the current persistence context.
3. JPA AttributeConverter
It converts a data type used in your domain model into one that your persistence provider can persist in the database.拓展思路
有时候当最外层的component/class的功能太过high level或者太过复杂,我们可以利用诸如delegation之类的模式将一个大的responsibility分成多个小的responsibility,每个class仍然只负责一个具体的responsibility。例如我们需要实现一个
class ShopController
的pay
方法,我们需要做很多事情,包括获取所有该用户要购买的商品,获取用户的支付信息,支付,检查支付结果并重定向用户访问的页面。如果我们把每个步骤的逻辑都分别在pay()
方法中实现,则这个方法本身的responsibility就太多了,于是我们可以将每个部分分别交给格子的class来负责,在pay
方法中我们只需要调用各个接口:class ShopController { // This runs when the user presses “pay now” public function pay() { // Create a basket to handle our session basket data $basket = new Basket($_SESSION[‘basket’]); // Get the current user $user = (new Authenticator)->currentUser(); // Create a payment request that holds payment data $paymentRequest = new PaymentRequest($_POST); // Pay and redirect the user (this throws an exception if there are any issues) return $basket->payAndRedirect($user); } }
这里的
pay
方法的responsibility是:“Taking the request data and passing it to the necessary specialists”下面是一个反例:
class ShopController { // This gets run when the presses “pay now” public function pay() { // Get the basket out of the session $basket = $_SESSION[‘basket’]; // Get the user out of the session $user = $_SESSION[‘user’]; // Initialise a variable that will hold the price to pay $totalToPay = 0; // Add up the price of all the items in the basket foreach ($basket[‘items’] as $item) { $totalToPay += $item[‘price’] } // Get the credit card information the user sent through $creditCardInformation = $_POST[‘credit_card_info’]; // Get the payment gateway key and post the customer, price and card data to stripe $paymentGatewayKey = ‘A82hDiha9hhdaonldtumpr2390Jf’; $paymentResponse = API::post(‘stripe’, [ ‘credit_card’ => $creditCardInfo, ‘customer’ => $user, ‘price’ => $totalToPay ]); // If the payment worked, take them to the payment gateway to confirm if ($paymentResponse[‘success’]) { return redirect($paymentResponse[‘redirect_url’]); } else { // Otherwise, show an error throw new Exception(“There was an error when attempting to pay. - ” . $paymentResponse[‘error_message’]); } } }
在反例中,
Pay
方法做了如下事情:- Getting the user from the session
- Handling post data
- Making requests to the payment gateway
- Summing the total cost of the basket up
- Ensuring the payment was successful and throwing an exception if not
- Redirecting the user to the payment gateway if successful
- Storing the payment key (This is a terrible security risk - your payment key is now in your GitHub repository. Great job, George!)
因此,它明显违背了 Single Responsibility principle
2. Open Closed Principle
"The most important principle of object-oriented design." --Robert C.Martin
"Software entities(classes, modules, functions, etc.) should be open for extension, but closed for modification." --Bertrand Meyer
"You should be able to extend the behavior of a system without having to modify that system." --Bob Martin
Introduction
简单来说Open Closed principle就是一个class的功能可以被extend,但是不能通过修改这个class或者module的code自身来实现。对扩展开放,但是对修改关闭。但是需要注意的是由于inheritance是具有强耦合的,而且super class和sub class之间的关系是在编译时确定,不利于在运行时切换实现类,因此在开发中一般利用Interface(abstraction)以及composition来实现对于功能的拓展。Example
举例说明,例如我们需要编写一个软件来控制咖啡机做咖啡,于是我们可以定义两个class
class CoffeeApp
andclass CoffeeMachine
,当需求只需要适配一款咖啡机的时候,我们的程序可能是这样:public class CoffeeMachine { private Coffee getCoffee() {//...} private void brewCoffee(Coffee) {//...} public void makeCoffee() { Coffee coffee = getCoffee(); brewCoffee(coffee); } } public class CoffeeApp { private CoffeeMachine machine; public CoffeeApp() { this.machine = new CoffeeMachine(); } public void prepareCoffee() { CoffeeMachine.makeCoffee(); } public static void main(String[] args) { CoffeeApp app = new CoffeeApp(); app.prepareCoffee(); } }
这样的实现可以满足当前要求,但是如果将来需求变化,需要适配多种咖啡机,这里的getCoffee以及brewCoffee就需要重新实现,同时还有可能加入研磨grind等步骤。为了适应新变化,我们就需要对现有对代码做refactor,这里会用到open closed principle。
首先我们需要让代码可以被extend,则需要将一些logic抽象出来,这里我们发现在CoffeeApp中实际上只需要调用CoffeeMachine::makeCoffee方法就够了,于是我们可以按照如下方法来实现:
Interface BasicCoffeeMachine { void makeCoffee(); } public class CoffeeMachine1 implements BasicCoffeeMachine { private Coffee getCoffee() {//...} private void brewCoffee(Coffee) {//...} @Override public void makeCoffee() { Coffee coffee = getCoffee(); brewCoffee(coffee); } } public class CoffeeMachine2 implements BasicCoffeeMachine { private Coffee getCoffee() {//...} private void brewCoffee(Coffee) {//...} private Coffee grindCoffee(Coffee) {//...} @Override public void makeCoffee() { Coffee coffee = getCoffee(); coffee = grindCoffee(coffee); brewCoffee(coffee); } } public class CoffeeApp { private BasicCoffeeMachine machine; public CoffeeApp(BasicCoffeeMachine machine) { this.machine = machine; } public void prepareCoffee() { CoffeeMachine.makeCoffee(); } public static void main(String[] args) { // 使用第一种咖啡机 CoffeeApp app = new CoffeeApp(new CoffeeMachine1); app.prepareCoffee(); // 使用第二种咖啡机 CoffeeApp app2 = new CoffeeApp(new CoffeeMachine2); app.prepareCoffee(); } }
这样refactor之后,原本对CoffeeMachine就有了可扩展性,我们可以通过在CoffeeApp的constructor中传入不同的concrete CoffeeMachine实现来做到兼容不同的咖啡机。需要注意这里的设计细节,CoffeeApp和CoffeeMachine之间的关系是 has a 的关系,利用了Composite的模式,而不是采用让CoffeeApp来继承某个CoffeeMachine,这样才使得动态替换CoffeeMachine instance成为了可能。
3. Liskov Substitution Principle
"Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T." --Barbara Liskov
Introduction
用人话来说,就是说subclass的object应该可以用来替换super class的object。具体来说,一个subclass的overriden method必须接受和super clas的相同method有相同的parameter,subclass中方法的validation rule可以比super class中的宽松,而不可以更严格。相对应的,subclass中方法的return value可以是super class中方法return value的subclass,或者是super class中方法可能return value的subset。
这个原则在实际开发中其实用到的地方非常多,我们经常需要将子类的object传入父类或者接口类型的argument中。例如在Open/Closed原则中,我们可以通过继承来拓展一个父类的功能,之后在调用的时候,我们可以将我们新建的subclass的object当作原本的base class的object来在原本的地方使用。
岔开一点话题,在Java中当我们在sub class中override父类方法的时候,return type只能是父类方法return type 的sub class,也就是assignable的。而access modifier只能比父类方法的更加visible而不能less visible。这些规则有Java语言本身的限制,但是更多遵循Liskov Substitution的规则涉及到具体业务逻辑则需要在写代码的时候特别注意。
4. Interface Segregation Principle
“Clients should not be forced to depend upon interfaces that they do not use.” --Robert C.Martin
Introduction
这个原则的作用和Single Responsibility类似,都是为了将程序划分成多个独立部分,从而减少side effect以及降低修改代码的频率。
简单来说,这个原则要求我们在设计Interface的时候要将不同的功能隔离开来成为不同的interface,当需求变化或者需要添加新功能的时候,要思考是否需要create新的interface而不是一味的在现有的interface中添加新的方法。因为如果我们修改现有的interface,所有implement这个interface的subclass都需要相应修改,同时还有可能需要对client进行相应修改。另一方面,在选择加入interface中的方法的时候要思考,是否所有client都需要实现这么多方法,哪些是非必要的。
当我们设计interface的时候应该倾向于将可以分为不同类的功能放入不同的interface,这样在写实现类的时候就可以根据需求选择合适的interface的组合来实现。Example
例如上面咖啡机的例子,如果我们除了想要控制咖啡机之外还想控制另一种咖啡机煮茶,原本的BasicCoffeeMachine接口就会变成这样:
public Interface BasicCoffeeMachine { void makeCoffee(); void makeTea(); }
但是这样的话无论是哪种咖啡机都必须要实现这两个方法,即使"CoffeeMachine1"并不能用来煮茶。因此更好的做法其实是将其分割开来,这样可以煮茶的咖啡机就 implement "BasicTeaMachine" 接口,而不能煮茶的咖啡机类也不需要改变。
5. Dependency Inversion Principle
Introduction
这个原则的基本思想并不复杂:提供complex logic的high level module应该可以轻易被reuse,不应被提供utility features的low level module的变化所影响。因此需要引入abstraction来decouple high level module and low level module。
Robert C.Martin's definition:- High-level module should not depend on low-level modules. Both should depend on abstractions;
- Abstractions should not depend on details, details should depend on abstractions;
这里的“depend on abstraction”是很重要的,high-level module depends on abstraction, and low-level depends on the same abstraction。
事实上当我们在设计中使用了"Open/Closed" 和 "Liskov Substitution" 之后,就会自然符合"Dependency Inversion" 原则。例如上面咖啡机的例子,为了遵循"Open/Closed"原则使得程序可以被扩展,支持更多咖啡机,我们需要抽象出咖啡机的interface,之后就可以运用“Liskov Substitution”替换"BasicCoffeeMachine"的不同实现类的instance。此时的程序high-level module (CoffeeMachine) 不会依赖于 low-level (CoffeeApp) 以及具体实现的咖啡机的类,而是依赖于abstraction (BasicCoffeeMachine),整个程序中依赖于具体实现类的只有CoffeeApp。
6. Summary
SOLID Design Principles 提供了设计程序的基本原则,并不具体到语言以及具体问题。而Design Patterns则针对在OOD过程中遇到的一些类型的具体问题提供了切实可行的解决方法,并且经过其他人的测试,safe to follow。而且与此同时,design pattern的本质也都遵循SOLID principles。