文章目录
高内聚低耦合
提高代码的可读性、可维护性和可扩展性,降低开发和维护的成本,并减少系统的风险
内聚:
内聚表示一个模块内部各个元素之间相互关联的程度。
高内聚意味着模块内部的功能紧密相关,所有部分都共同完成一个特定的任务或功能。
低内聚则意味着模块内部包含多种不相关的功能。
耦合:
耦合表示不同模块之间相互依赖的程度。
高耦合意味着模块之间相互依赖紧密,一个模块的变化会影响到其他模块。
低耦合则意味着模块之间相对独立,一个模块的变化对其他模块的影响较小。
举个例子:
汽车是由许多不同的部件组成的,比如发动机、轮胎、刹车等等。
在高内聚低耦合的设计中,每个部件都应该专注于自己的功能,同时尽可能减少与其他部件之间的依赖关系。
++高内聚意味着每个部件都应该有一个清晰的责任和功能。++发动机的责任是提供动力,而刹车系统的责任是提供制动。这样设计的好处是,每个部件都可以独立工作,而不需要过多依赖其他部件的内部细节。
++低耦合意味着部件之间的相互依赖应该尽可能减少。++刹车系统不需要知道发动机如何工作,它只需要知道何时需要制动。这样设计的好处是,如果需要更改或替换一个部件,不会对其他部件产生太大影响,因为它们之间的依赖关系很少。
高内聚:意味着一个类或模块的内部元素(包括变量、方法和属性)应该紧密相关,并且共同服务于一个明确且集中的目的。
低耦合:模块或类之间的依赖关系应该尽可能少
- 内紧(高内聚):程序内的模块或类应该紧密相关,形成一个高效的功能单元。
- 外松(低耦合):程序之间的模块或类应该尽可能不关联,各自实现各自的功能。
设计原则
在进行软件系统设计时所要遵循的一些经验准则,应用该准则的目的通常是为了避免某些经常出现的设计缺陷,提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性
包括单一职责原则,开放封闭原则,里氏替换原则,依赖倒置原则,迪米特原则,接口隔离原则
开闭原则
(Open-Closed Principle, OCP)
定义:类,模块,函数等应该是可以拓展的,但是不可修改
对扩展开放,对修改关闭。
我们需要使用抽象实现(接口和抽象类)达到这样的效果。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。
而软件中易变的细节可以++从抽象派生来的实现类来进行扩展++,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
举个例子
java
//AbstractSkin.java
public abstract class AbstractSkin {
//显示皮肤
public abstract void display();
}
//DefaultSkin.java
public class DefaultSkin extends AbstractSkin{
@Override
public void display() {
System.out.println("默认皮肤");
}
}
//WhiteSkin.java
public class WhiteSkin extends AbstractSkin{
@Override
public void display() {
System.out.println("白色皮肤");
}
}
//SougouInput.java
public class SougouInput {
private AbstractSkin skin;
public AbstractSkin getSkin() {
return skin;
}
//通过setSkin方法设置不同的皮肤实现
public void setSkin(AbstractSkin skin) {
this.skin = skin;
}
public void display() {
skin.display();
}
}
//Client.java
public class Client {
public static void main(String[] args) {
//1.创建输入法对象
SougouInput input = new SougouInput();
//2.创建皮肤对象
DefaultSkin skin = new DefaultSkin();
//WhiteSkin skin = new WhiteSkin();
//3.将皮肤设置到输入法中
input.setSkin(skin);
//4.显示皮肤
input.display();
}
}
SougouInput
类通过其 setSkin
方法允许添加不同类型的皮肤(如 DefaultSkin
或 WhiteSkin
),这体现了对扩展的开放性。
由于 SougouInput
类依赖于 AbstractSkin
抽象类而不是具体的皮肤实现类,因此添加新的皮肤类型只需要创建新的子类并实现 AbstractSkin
的 display
方法,而++不需要修改 SougouInput
类的代码++。这体现了对修改的封闭性。
皮肤的类不用修改,再创建新皮肤,让他继承AbstractSkin
抽象类,在客户端类代码进行修改即可。
作用:
- 提高软件的可维护性
- 增强软件的扩展性
- 减少代码的耦合度
单一职责原则
(Single Responsibility Principle,SRP)
定义:一个类(或模块、函数等)应该只有一个引起它变化的原因。换句话说,一个类应该只负责一个功能领域中的相关职责,或者变化的原因应该只有一个。
下面是不符合单一职责原则的例子:
java
public class TelPhone {
public void Dial(String phone_number){
System.out.println("给"+phone_number+"打电话");
}
public void HangUp(String phone_number) {
System.out.println("挂断" + phone_number + "打电话");
}
public void SendMessage(String message) {
System.out.println("发送" + message);
}
public void ReceiveMessage(String message) {
System.out.println("接收" + message);
}
}
可能发生的变化:
-
内部的变化,如果
TelPhone
类中的任何一个方法发生改变,,都需要修改TeIPhone
、由于它负责了多个职责,所以一个职责的变化可能会导致其他无关职责的代码也需要被修改或重新测试。 -
外部的变化,如果
TeIPhone
要添加新的的方法,需要修改TeIPhone
类
为了符合单一职责原则,我们可以做出如下修改:
但中间的依然不符合,存在一个以上引起类变化的原因,所以我们可以考虑最右边的,一个类中只有一个方法
但这样比较极端,会导致类的数量大幅增加,使得管理和维护代码变得复杂。
因此,我们通常要在单一职责原则和实际应用之间找到一个平衡点。
实现方式:
给每个方法,都提炼成一个接口,抽象成一种能力,然后分别写类,去实现接口,最终在TelPhone
中只进行调用。
java
package com.feng.test01;
interface Dialer {
void Dial(String phoneNumber);
}
interface Hanger {
void HangUp();
}
interface Sender {
void SendMessage(String text);
}
interface Receiver {
void ReciveMessage(String text);
}
class DialerImpl implements Dialer {
public void Dial(String phoneNumber) {
System.out.println("给 " + phoneNumber + " 打电话");
}
}
class HangerImpl implements Hanger {
public void HangUp() {
System.out.println("挂断电话");
}
}
class SenderImpl implements Sender {
public void SendMessage(String text) {
System.out.println("发送 " + text);
}
}
class ReceiverImpl implements Receiver {
public void ReciveMessage(String text) {
System.out.println("接收 " + text);
}
}
class TelPhone {
private Dialer dialer;
private Hanger hanger;
private Sender sender;
private Receiver receiver;
public TelPhone(Dialer dialer, Hanger hanger, Sender sender, Receiver receiver) {
this.dialer = dialer;
this.hanger = hanger;
this.sender = sender;
this.receiver = receiver;
}
public void Dial(String phoneNumber) {
dialer.Dial(phoneNumber);
}
public void HangUp() {
hanger.HangUp();
}
public void SendMessage(String text) {
sender.SendMessage(text);
}
public void ReciveMessage(String text) {
receiver.ReciveMessage(text);
}
}
public class Main {
public static void main(String[] args) {
// 创建接口实现类实例
Dialer dialer = new DialerImpl();
Hanger hanger = new HangerImpl();
Sender sender = new SenderImpl();
Receiver receiver = new ReceiverImpl();
// 创建 TelPhone 对象并使用其接口
TelPhone telphone = new TelPhone(dialer, hanger, sender, receiver);
// 电话呼叫操作
telphone.Dial("123456789");
telphone.HangUp();
// 消息操作
telphone.SendMessage("Hello, World!");
telphone.ReciveMessage("Hi there!");
}
}
好处:
- 提高代码的可读性,提高系统的可维护性。
- 降低类的复杂性,一个模块只负责一个职责,提高系统的可扩展性和可维护性。
- 降低变更引起的风险。变更是不然的,如果单一职责做得好,当修改一个功能的时候可以显著的降低对另一个功能的影响。
里氏代换原则
(Liskov Substitution Principle,LSP)
里氏代换原则:任何基类可以出现的地方,子类一定可以出现。
通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。
要点:
功能保持:子类应当能够替代基类,并且子类对象应能代替基类对象使用,而不会导致程序运行出现问题。
行为一致:子类可以扩展基类的功能,但不能改变基类的功能。即子类的行为应该与基类保持一致
要求:
- 子类可以实现父类的抽象方法,但不要去覆盖(重写)父类的非抽象方法
- 子类可以增加自己特有的方法
- 当子类的方法重写父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松
- 当子类的方法实现父类的方法时(重写或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或与父类一样
java
package com.feng.test05;
public abstract class Coder {
public void coding() {
System.out.println("我会敲代码!");
}
}
class JavaCoder extends Coder {
public void play() {
System.out.println("喜欢玩XXXX");
}
//重写了父类的非抽象方法
public void coding() {
System.out.println("我只会敲JAVA代码!");
}
}
里氏代换原则指出,如果程序中的对象使用的是基类(父类)的话,那么无论是使用基类对象还是其子类对象,程序的行为都是一致的。
用 JavaCoder
替代 Coder
时,本来会敲很多代码,但现在只会敲JAVA了。
所以尽量不要去重写父类非抽象方法,不要改变父类原有的功能。
可以这样修改:
- 保留父类方法的行为,并且扩展子类方法的功能
java
public void coding() {
super.coding(); // 调用父类的coding方法
System.out.println("我会敲JAVA代码!");
}
- 或者再写一个
javaCoding
方法 - 写
JavaCoder
和Coder
的抽象父类People
,把coding
这一行为定义在People
中,放弃JavaCoder
和Coder
的继承关系
好处:
- 开放性:是实现开放封闭原则的的具体手段之一
- 提高代码的可复用性
依赖倒置原则
(Dependence Inversion Principle,DIP)
定义:高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
旨在通过依赖于抽象而不是具体实现来降低系统的耦合度,提高系统的可维护性和可扩展性。
下面是一个违背依赖倒置原则的例子
java
package com.feng.test04;
class FuelCar{
public void run(){
System.out.println("开的是燃油车");
}
}
class Driver{
public void drive(FuelCar car) {
car.run();
}
}
public class Client {
public static void main(String[] args) {
FuelCar fuelCar = new FuelCar();
Driver xiaowang = new Driver();
xiaowang.drive(fuelCar);
}
}
Driver
类直接依赖于 FuelCar
类,而没有使用抽象,违反了依赖倒置原则。
如果我们想要支持其他类型的车辆,比如电动车或者公交车,就需要修改 Driver
类,这样会增加代码的耦合度和维护成本。
高层模块(Driver
)不应该直接依赖于低层模块(FuelCar
),而是应该依赖于抽象。在这个例子中,Driver
类应该依赖于一个抽象的 ICar
接口,而不是具体的 FuelCar
类。
我们可以引入一个接口来表示所有类型的车,并让FuelCar
实现这个接口。然后,Driver
的drive
方法可以接受任何实现了ICar
接口的对象作为参数。
java
package com.feng.test04after;
interface ICar {
void run();
}
class FuelICar implements ICar {
@Override
public void run() {
System.out.println("开的是燃油车");
}
}
class ElectricICar implements ICar {
@Override
public void run() {
System.out.println("开的是电车");
}
}
class Driver {
public void drive(ICar car) {
car.run();
}
}
public class Client {
public static void main(String[] args) {
ICar su7 = new ElectricICar();
ICar benz = new FuelICar();
Driver driver = new Driver();
driver.drive(su7);
driver.drive(benz);
}
}
作用:
- 提高代码的可维护性
- 降低代码的耦合度
- 提高系统的可扩展性
迪米特原则
(Law of Demeter,LoD)
也称为最少知识原则:一个对象应该对其他对象有最少的了解。
定义:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用
一个对象应该对其他对象知之甚少,只与"朋友"通信,而不与"陌生人"直接通信。
- 租房者-中介-房东
- 要做软件的公司-软件公司-软件工程师
这里的"朋友"指的是当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等
这些对象同当前对象存在关联、依赖、聚合或组合关系,可以直接访问这些对象的方法。
下面是一个
java
import java.util.ArrayList;
import java.util.List;
class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
// 购物车类
class ShoppingCart {
private List<Product> products;
public ShoppingCart() {
this.products = new ArrayList<>();
}
// 添加商品到购物车
public void addProduct(Product product) {
products.add(product);
}
// 打印购物车中的商品信息
public void printCart() {
for (Product product : products) {
System.out.println("商品:" + product.getName() + " 价格:" + product.getPrice());
}
}
}
public class Test {
public static void main(String[] args) {
Product product1 = new Product("banana", 2);
Product product2 = new Product("apple", 5);
ShoppingCart cart = new ShoppingCart();
cart.addProduct(product1);
cart.addProduct(product2);
cart.printCart();
}
}
ShoppingCart
类对 Product
类的了解仅限于 getName
和 getPrice
方法,这是符合迪米特原则的。ShoppingCart
不需要知道 Product
类的内部实现细节,也不需要与其他任何与 Product
类相关的"陌生人"对象进行交互。
好处:
- 降低耦合性
- 提高模块独立性
- 增强系统的可维护性
接口隔离原则
(Interface Segregation Principle,ISP)
定义:一个类对另一个类的依赖应该建立在最小的接口上。
建立单一接口,不要建立庞大臃肿的接口;尽量细化接口,接口中的方法尽量少。
也就是说,我们要为各个类建立专用的接口,而不要试图建立一个很庞大的接口件所有依赖它的类通用。
下面是一个不符合接口隔离原则的例子:
java
package com.feng.test03;
interface Device {
String getCpu();
String getType();
String getMemory();
}
class computer implements Device{
@Override
public String getCpu() {
return "i7";
}
@Override
public String getType() {
return "笔记本电脑";
}
@Override
public String getMemory() {
return "16GB";
}
}
class fan implements Device{
@Override
public String getCpu() {
return null; //不需要的方法
}
@Override
public String getType() {
return "电风扇";
}
@Override
public String getMemory() {
return null; //不需要的方法
}
}
虽然定义了一个Device
接口,但是由于此接口的粒度不够细化,类依赖于不需要的方法。虽然比较契合电脑这种设备,但是不适合风扇,要对其进行更细粒度的划分。
接口的粒度:描述了接口所提供功能的大小和复杂度
下面是一个符合接口隔离原则粒度更细的代码:
java
// 通用设备接口
interface GenericDevice {
String getType();
}
// 电脑设备接口
interface ComputerDevice extends GenericDevice {
String getCpu();
String getMemory();
}
// 风扇设备接口
interface FanDevice extends GenericDevice {
void adjustSpeed(int speed);
}
注意:
- 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计的灵活性;但是如果过小,则会造成接口数量过多,使设计复杂化。所以,接口的大小一定要适度。
- 为依赖接口的类定制服务,只暴露给调用的类需要的方法,不需要的方法则隐藏起来只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
- 提高内聚,减少对外交互。接口方法尽量少用
public
修饰。接口是对外的承诺,承诺越少,对系统的开发越有利,变更风险也会越少。
作用:
- 降低耦合性
- 提高灵活性
- 增强可维护性
- 开闭原则:抽象架构,扩展实现
- 单一职责:一个类和方法只做一件事
- 里氏替换: 多态,子类可扩展父类
- 依赖倒置:细节依赖抽象,下层依赖上层
- 接口隔离:建立单一接口
- 迪米特原则:最少知道,降低耦合
参考:
如有错误烦请指正。
感谢您的阅读