Cet article a participé à l'événement "Newcomer Creation Ceremony" pour commencer ensemble la route de la création d'or.
3. Principes de conception de logiciels
Dans le développement de logiciels, afin d'améliorer la maintenabilité et la réutilisabilité des systèmes logiciels et d'augmenter l'évolutivité et la flexibilité des logiciels, les programmeurs doivent faire de leur mieux pour développer des programmes selon six principes, améliorant ainsi l'efficacité du développement logiciel et réduisant les coûts et la maintenance. frais.
3.1. Le principe d'ouverture et de fermeture
Ouvert pour extension, fermé pour modification . Lorsque le programme doit être étendu, le code d'origine ne peut pas être modifié pour obtenir un effet hot-plug. En bref, il s'agit de rendre le programme extensible, facile à maintenir et à mettre à jour.
Pour obtenir cet effet, nous devons utiliser des interfaces et des classes abstraites.
Parce que l'abstraction est flexible et adaptable, tant que l'abstraction est raisonnable, la stabilité de l'architecture logicielle peut être fondamentalement maintenue. Les détails volatils du logiciel peuvent être étendus à partir de la classe d'implémentation dérivée de l'abstraction.Lorsque le logiciel doit changer, il suffit de re-dériver une classe d'implémentation pour l'étendre en fonction des besoins.
Ce qui suit prend 搜狗输入法
la peau de comme exemple pour introduire l'application du principe ouvert-fermé.
【Exemple】搜狗输入法
La conception de la peau.
Analyse : 搜狗输入法
un habillage est une combinaison d'éléments tels que l'image d'arrière-plan de la méthode de saisie, la couleur de la fenêtre et le son. Les utilisateurs peuvent modifier l'habillage de leur méthode de saisie en fonction de leurs propres préférences et peuvent également télécharger de nouveaux habillages sur Internet. Ces skins ont des caractéristiques communes, pour lesquelles une classe abstraite (AbstractSkin) peut être définie, et chaque skin spécifique (DefaultSpecificSkin et HeimaSpecificSkin) en est une sous-classe. Les formulaires utilisateur peuvent choisir ou ajouter de nouveaux thèmes selon les besoins sans modifier le code d'origine, de sorte qu'il satisfait le principe ouvert-fermé.
Code:
// 抽象皮肤类
public abstract class AbstractSkin {
public abstract void display();
}
// 默认皮肤类
public class DefaultSkin extends AbstractSkin {
@Override
public void display() {
System.out.println("默认皮肤");
}
}
// 自定义皮肤
public class HeiSkin extends AbstractSkin {
@Override
public void display() {
System.out.println("自定义皮肤");
}
}
// 搜狗输入法
public class SougouInput {
private AbstractSkin skin;
public void setSkin(AbstractSkin skin) {
this.skin = skin;
}
public void display() {
skin.display();
}
}
public class Client {
public static void main(String[] args) {
// 1. 创建搜狗输入法对象
SougouInput sougouInput = new SougouInput();
// 2. 创建皮肤对象
// DefaultSkin skin = new DefaultSkin();
HeiSkin skin = new HeiSkin();
// 3. 将皮肤设置到输入法中
sougouInput.setSkin(skin);
// 4. 显示皮肤
sougouInput.display();
}
}
3.2 Principe de responsabilité unique
Une classe n'est responsable que des responsabilités correspondantes dans un domaine fonctionnel, ou peut être définie comme suit : pour une classe, il ne devrait y avoir qu'une seule raison pour qu'elle change.
Contrôlez la granularité des classes, découplez les objets et améliorez leur cohésion.
3.3 Principe de substitution de Liskov
Le principe de substitution de Liskov est l'un des principes de base de la conception orientée objet.
Principe de substitution de Liskov : Partout où une classe de base peut apparaître, une sous-classe doit apparaître. Compréhension populaire : les sous-classes peuvent étendre les fonctions de la classe parent, mais ne peuvent pas modifier les fonctions d'origine de la classe parent. En d'autres termes, lorsqu'une sous-classe hérite de la classe parent, essayez de ne pas remplacer les méthodes de la classe parent, sauf en ajoutant de nouvelles méthodes pour compléter les nouvelles fonctions.
如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
下面看一个里氏替换原则中经典的一个例子
【例】正方形不是长方形。
在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形。
代码如下:
长方形类(Rectangle):
public class Rectangle {
private double length;
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
正方形(Square):
由于正方形的长和宽相同,所以在方法setLength和setWidth中,对长度和宽度都需要赋相同值。
public class Square extends Rectangle {
@Override
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setLength(width);
}
}
类RectangleDemo是我们的软件系统中的一个组件,它有一个resize方法依赖基类Rectangle,resize方法是RectandleDemo类中的一个方法,用来实现宽度逐渐增长的效果。
public class RectangleDemo {
public static void main(String[] args) {
// 创建长方形
Rectangle rectangle = new Rectangle();
rectangle.setLength(20);
rectangle.setWidth(10);
resize(rectangle);
printLengthAndWidth(rectangle);
System.out.println("=============");
// 创建正方形
Square square = new Square();
square.setLength(10);
resize(square);
printLengthAndWidth(square);
}
// 扩宽方法
public static void resize(Rectangle rectangle) {
// 判断宽如果比长小,进行扩宽的操作
if (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getLength() + 1);
}
}
// 打印长和宽
public static void printLengthAndWidth(Rectangle rectangle) {
System.out.println(rectangle.getLength());
System.out.println(rectangle.getWidth());
}
}
我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。
我们得出结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。
如何改进呢?此时我们需要重新设计他们之间的关系。抽象出来一个四边形接口(Quadrilateral),让Rectangle类和Square类实现Quadrilateral接口
public interface Quadrilateral {
double getLength();
double getWidth();
}
public class Square implements Quadrilateral {
private double side;
public double getSide() {
return side;
}
public void setSide(double side) {
this.side = side;
}
@Override
public double getLength() {
return side;
}
@Override
public double getWidth() {
return side;
}
}
public class Rectangle implements Quadrilateral {
private double length;
private double width;
public void setLength(double length) {
this.length = length;
}
public void setWidth(double width) {
this.width = width;
}
@Override
public double getLength() {
return length;
}
@Override
public double getWidth() {
return width;
}
}
public class RectangleDemo {
public static void main(String[] args) {
// 创建长方形
Rectangle rectangle = new Rectangle();
rectangle.setLength(20);
rectangle.setWidth(10);
resize(rectangle);
printLengthAndWidth(rectangle);
}
// 扩宽方法
public static void resize(Rectangle rectangle) {
// 判断宽如果比长小,进行扩宽的操作
if (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getLength() + 1);
}
}
// 打印长和宽
public static void printLengthAndWidth(Quadrilateral quadrilateral) {
System.out.println(quadrilateral.getLength());
System.out.println(quadrilateral.getWidth());
}
}
3.4、依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
下面看一个例子来理解依赖倒转原则
【例】组装电脑
现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。
类图如下:
代码如下:
希捷硬盘类(XiJieHardDisk):
public class XiJieHardDisk implements HardDisk {
public void save(String data) {
System.out.println("使用希捷硬盘存储数据" + data);
}
public String get() {
System.out.println("使用希捷希捷硬盘取数据");
return "数据";
}
}
Intel处理器(IntelCpu):
public class IntelCpu implements Cpu {
public void run() {
System.out.println("使用Intel处理器");
}
}
金士顿内存条(KingstonMemory):
public class KingstonMemory implements Memory {
public void save() {
System.out.println("使用金士顿作为内存条");
}
}
电脑(Computer):
public class Computer {
private XiJieHardDisk hardDisk;
private IntelCpu cpu;
private KingstonMemory memory;
public IntelCpu getCpu() {
return cpu;
}
public void setCpu(IntelCpu cpu) {
this.cpu = cpu;
}
public KingstonMemory getMemory() {
return memory;
}
public void setMemory(KingstonMemory memory) {
this.memory = memory;
}
public XiJieHardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(XiJieHardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public void run() {
System.out.println("计算机工作");
cpu.run();
memory.save();
String data = hardDisk.get();
System.out.println("从硬盘中获取的数据为:" + data);
}
}
测试类(TestComputer):
测试类用来组装电脑。
public class TestComputer {
public static void main(String[] args) {
Computer computer = new Computer();
computer.setHardDisk(new XiJieHardDisk());
computer.setCpu(new IntelCpu());
computer.setMemory(new KingstonMemory());
computer.run();
}
}
上面代码可以看到已经组装了一台电脑,但是似乎组装的电脑的cpu只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的,这对用户肯定是不友好的,用户有了机箱肯定是想按照自己的喜好,选择自己喜欢的配件。
根据依赖倒转原则进行改进:
代码我们只需要修改Computer类,让Computer类依赖抽象(各个配件的接口),而不是依赖于各个组件具体的实现类。
类图如下:
电脑(Computer):
public class Computer {
private HardDisk hardDisk;
private Cpu cpu;
private Memory memory;
public HardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(HardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public Cpu getCpu() {
return cpu;
}
public void setCpu(Cpu cpu) {
this.cpu = cpu;
}
public Memory getMemory() {
return memory;
}
public void setMemory(Memory memory) {
this.memory = memory;
}
public void run() {
System.out.println("计算机工作");
}
}
面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象, 实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。
3.5、接口隔离原则
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。
下面看一个例子来理解接口隔离原则
【例】安全门案例
我们需要创建一个黑马
品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。类图如下:
public interface SafetyDoor {
// 防盗
void antiTheft();
// 防火
void fireProof();
// 防水
void waterProof();
}
public class HeimaSafetyDoor implements SafetyDoor {
@Override
public void antiTheft() {
System.out.println("防盗");
}
@Override
public void fireProof() {
System.out.println("防火");
}
@Override
public void waterProof() {
System.out.println("防水");
}
}
public class Client {
public static void main(String[] args) {
HeimaSafetyDoor door = new HeimaSafetyDoor();
door.antiTheft();
door.fireProof();
door.waterProof();
}
}
上面的设计我们发现了它存在的问题,黑马品牌的安全门具有防盗,防水,防火的功能。现在如果我们还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能呢?很显然如果实现SafetyDoor接口就违背了接口隔离原则,那么我们如何进行修改呢?看如下类图:
代码如下:
AntiTheft(接口):
public interface AntiTheft {
void antiTheft();
}
Fireproof(接口):
public interface Fireproof {
void fireproof();
}
Waterproof(接口):
public interface Waterproof {
void waterproof();
}
HeiMaSafetyDoor(类):
public class HeiMaSafetyDoor implements AntiTheft,Fireproof,Waterproof {
public void antiTheft() {
System.out.println("防盗");
}
public void fireproof() {
System.out.println("防火");
}
public void waterproof() {
System.out.println("防水");
}
}
ItcastSafetyDoor(类):
public class ItcastSafetyDoor implements AntiTheft,Fireproof {
public void antiTheft() {
System.out.println("防盗");
}
public void fireproof() {
System.out.println("防火");
}
}
3.6、迪米特法则
迪米特法则又叫最少知识原则。
只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
下面看一个例子来理解迪米特法则
【例】明星与经纪人的关系实例
明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。
类图如下:
代码如下:
明星类(Star)
public class Star {
private String name;
public Star(String name) {
this.name=name;
}
public String getName() {
return name;
}
}
粉丝类(Fans)
public class Fans {
private String name;
public Fans(String name) {
this.name=name;
}
public String getName() {
return name;
}
}
媒体公司类(Company)
public class Company {
private String name;
public Company(String name) {
this.name=name;
}
public String getName() {
return name;
}
}
经纪人类(Agent)
public class Agent {
private Star star;
private Fans fans;
private Company company;
public void setStar(Star star) {
this.star = star;
}
public void setFans(Fans fans) {
this.fans = fans;
}
public void setCompany(Company company) {
this.company = company;
}
public void meeting() {
System.out.println(fans.getName() + "与明星" + star.getName() + "见面了。");
}
public void business() {
System.out.println(company.getName() + "与明星" + star.getName() + "洽淡业务。");
}
}
public class Client {
public static void main(String[] args) {
Agent agent = new Agent();
Star star = new Star("林青霞");
agent.setStar(star);
Fans fans = new Fans("李四");
agent.setFans(fans);
Company company = new Company("黑马公司");
agent.setCompany(company);
agent.meeting();
agent.business();
}
}
3.7、合成复用原则
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 对象间的耦合度低。可以在类的成员位置声明抽象。
- Grande flexibilité de réutilisation. Cette réutilisation peut être effectuée dynamiquement au moment de l'exécution, et de nouveaux objets peuvent référencer dynamiquement des objets du même type que les objets constitutifs.
Regardons un exemple pour comprendre le principe de la réutilisation composite
[Exemple] Programme de gestion de la classification automobile
Les voitures peuvent être divisées en voitures à essence, voitures électriques, etc. selon la "source d'énergie" ; selon la "couleur", elles peuvent être divisées en voitures blanches, voitures noires et voitures rouges. Si les deux classifications sont considérées ensemble, il existe de nombreuses combinaisons. Le diagramme de classes est le suivant :
D'après le diagramme de classes ci-dessus, nous pouvons voir qu'il existe de nombreuses sous-classes générées par héritage et réutilisation. S'il y a une nouvelle source d'alimentation ou une nouvelle couleur, nous devons définir une nouvelle classe. Essayons de changer la réutilisation de l'héritage en réutilisation de l'agrégation.