面向对象设计7大原则(十二)
面向对象设计原则
总原则:高内聚、低耦合
1、单一职责原则
就一个类而言,应该仅有一个引起它变化的原因。例如一台山寨手机的功能:拍照、摄像、手机游戏、网络摄像头、GPS、炒股等等,虽然功能多,但是每一个功能都不强。
每一个职责都是一个变化的轴线,当需求变化时会反映为类的职责的变化。如果一个类承担的职责多于一个,那么引起它变化的原因就有多个。一个职责的变化甚至可能会削弱或者抑制类完成其他职责的能力,从而导致脆弱的设计。
一、单一职责原则示例
需求:接受客户端输入并提交到数据库
原设计:一个类负责接受客户端输入,对客户端输入进行校验,连接数据库,并提交数据到数据库
现设计:一个功能也就是一个职责由一个类来负责
1.1 SaveToDB
public class SaveToDB {
private String username;
private String password;
//接收客户端输入
public void input(){
//接收客户端的输入
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
username = scanner.nextLine();
System.out.println("请输入密码");
password = scanner.nextLine();
}
//校验
public boolean judge(){
if(username==null || "".equals(username.trim())){
System.out.println("用户名不能为空");
return false;
}
if(password==null || "".equals(password.trim())){
System.out.println("密码不能为空");
return false;
}
return true;
}
//连接数据库:用输入输出代替
public void getConnection(){
System.out.println("获得数据库连接");
}
//存储数据
public void saveData(){
System.out.println("存储数据成功");
}
}
1.2 MainClass
public class MainClass {
public static void main(String[] args){
SaveToDB saveToDB = new SaveToDB();
saveToDB.input();
if(saveToDB.judge()){
saveToDB.getConnection();
saveToDB.saveData();
}
}
}
对代码进行改造,实现一个职责由一个类来负责
1.3 Input
public class Input {
private String username;
private String password;
//接收客户端输入
public void input(){
//接收客户端的输入
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
username = scanner.nextLine();
System.out.println("请输入密码");
password = scanner.nextLine();
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
1.4 Vilidator
public class Vilidator {
public static boolean judge(String username,String password){
if(username==null || "".equals(username.trim())){
System.out.println("用户名不能为空");
return false;
}
if(password==null || "".equals(password.trim())){
System.out.println("密码不能为空");
return false;
}
return true;
}
}
1.5 DBManager
public class DBManager {
//连接数据库:用输入输出代替
public static void getConnection(){
System.out.println("获得数据库连接");
}
}
1.6 DBImp
public class DBImp {
//存储数据
public static void saveData(String username,String password){
System.out.println("存储数据成功");
System.out.println("姓名为:"+username);
System.out.println("密码为:"+password);
}
}
2、开放封闭原则
开放封闭原则(Open-Closed Principle):一个软件实体应当对扩展开放,则修改关闭。在设计一个模块时,应当使得这个模块可以在不被修改的前提下被扩展。也就是说,应当可以在不必修改源代码的情况下修改这个模块的行为。设计的目的便在于面对需求的改变而保持系统的相对稳定,从而使得系统可以很容易的从一个版本升级到另一个版本。
一、怎么做到开放封闭原则
实际上,绝对封闭的系统是不存在的。无论模块是怎么封闭,到最后,总还是有一些无法封闭的变化。而我们的思路就是:既然不能做到完全封闭,那我们就应该对那些变化封闭,那些变化隔离做出选择。我们做出选择,然后将那些无法封闭的变化抽象出来,进行隔离,允许扩展,尽可能的减少系统的开发。当系统变化来临时,我们要及时的做出反应。
我们并不害怕改变的到来。当变化到来时,我们首先需要做的不是修改代码,而是尽可能的将变化抽象出来进行隔离,然后进行扩展。面对需求的变化,对程序的修改应该是尽可能通过添加代码来实现,而不是通过修改代码来实现。
二、例子一,繁忙的银行业务员
根据代码会发现,如果增加了其他业务员,就需要在BankWorker中添加一个业务方法,也让银行业务员“业务”繁忙,容易混乱,扩展性也不好
2.1 BankWoker
//银行业务员
public class BankWorker {
//负责存款业务
public void saving() {
System.out.println("进行存款业务");
}
//负责取款业务
public void drawing(){
System.out.println("进行取款业务");
}
//负责转账业务
public void transfering(){
System.out.println("进行转账业务");
}
//负责基金申购
public void buying(){
System.out.println("进行基金购买");
}
}
2.2 MainClass
public class MainClass {
public static void main(String[] args){
BankWorker bankWorker = new BankWorker();
//存款
bankWorker.saving();
//取款
bankWorker.drawing();
}
}
三、例子二,轻松的银行业务员
将银行业务员设置为接口,下面为实现的子类,分为存款业务员、取款业务员、转账业务员等,将业务逻辑分割,改造后分工明确,各施其职,不容易混乱。
例如下面需要增加业务的时候,只需要继续添加子类进行扩展,实现了开放封闭原则,没有大面积修改之前的代码,
3.1 BankWorker
//银行业务员
public interface BankWorker {
public void operation();
}
3.2 SavingBankWorker
//负责存款业务员
public class SavingBankWorker implements BankWorker{
public void operation() {
System.out.println("进行存款业务");
}
}
3.3 DrawingBankWorker
//负责取款业务员
public class DrawingBankWorker implements BankWorker {
public void operation() {
System.out.println("进行取款业务");
}
}
3.4 TransferingBankWorker
//负责转装业务员
public class TransferingBankWorker implements BankWorker {
public void operation() {
System.out.println("进行转装业务");
}
}
3.5 MainClass
public class MainClass {
public static void main(String[] args){
//改造后分工明确,各施其职,不容易混乱
BankWorker bankWorker = new SavingBankWorker();
bankWorker.operation();
BankWorker bankWorker1 = new DrawingBankWorker();
bankWorker1.operation();
}
}
四、优点
通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需求,是变化中的软件有一定的适应性和灵活性。
已有的软件模块,特别是最重要的抽象模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性。
3、里氏代换原则
一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类和子类对象的区别。也就是说,在软件里面,把父类替换成它的子类,程序的行为没有变化。一个软件实体如果使用的是一个子类的话,那么它不能适用于其父类。
一、里氏代换原则例子
1.1 Person
public class Person {
public void display(){
System.out.println("this is person");
}
}
1.2 Man
public class Man extends Person {
public void display(){
System.out.println("this is man");
}
}
1.3 MainClass
如果使用一个父类,一定适合其子类
public class MainClass {
public static void main(String[] args){
//如果使用一个父类,一定适合其子类
Person person = new Person();
display(person);
Person man = new Man();
display(man);
}
public static void display(Person person){
person.display();
}
}
1.4 MainClass
如果使用一个子类,不能适合其父类
public class MainClass {
public static void main(String[] args){
//如果使用一个父类,一定适合其子类
Person person = new Person();
display(person); //报错
Person man = new Man();
display(man); //报错
}
//修改参数为Man
public static void display(Man man){
man.display();
}
}
二、问题分析:企鹅是一种鸟类吗
2.1 Bird
public interface Bird {
public void fly();
}
2.2 LaoYing
public class LaoYing implements Bird {
public void fly() {
System.out.println("老鹰在飞");
}
}
2.3 MaQue
public class MaQue implements Bird {
public void fly() {
System.out.println("麻雀在飞");
}
}
2.4 QiE
//企鹅不能飞,但是鸟类,被强制实现了接口,飞的方法就必须实现,这时候就不符合里氏代换原则
//所以企鹅不应该实现该接口
public class QiE {
}
2.5 MainClass
public class MainClass {
public static void main(String[] args){
fly(new LaoYing());
fly(new QiE()); //报错,因为企鹅是鸟类,但是不能飞,不应该实现该接口
}
public static void fly(Bird bird){
bird.fly();
}
}
三、问题分析:正方形是一种长方形吗
现实生活中认为,正方形是一种特殊的长方形,但该例子中,我们把长方形跟正方形看成两个独立的类,长方形里面有width、height两个属性,正方形里面有side一个属性,所以正方形不是长方形的子类,不存在里氏代换关系。
如果我们让正方形继承长方形,并覆盖长方形的getWidth、getHeight等方法,设置和返回的都是side
3.1 CFX
public class CFX {
private long width;
private long height;
public long getWidth() {
return width;
}
public void setWidth(long width) {
this.width = width;
}
public long getHeight() {
return height;
}
public void setHeight(long height) {
this.height = height;
}
}
3.2 ZFX
public class ZFX extends CFX {
private long side;
public long getWidth() {
return this.getSide();
}
public void setWidth(long width) {
this.setSide(width);
}
public long getHeight() {
return this.getSide();
}
@Override
public void setHeight(long height) {
this.setSide(height);
}
public long getSide() {
return side;
}
public void setSide(long side) {
this.side = side;
}
}
3.3 MainClass
在方法test中,打印出高和宽,编译器可以通过,正方形跟长方形都可以使用这个方法,但在resize方法中,对于正方形是死循环,这时候便不符合一定适合子类这个规则
public class MainClass {
public static void main(String[] args){
//编译器通过,可以使用
CFX cfx = new CFX();
cfx.setHeight(10);
cfx.setWidth(20);
test(cfx);
resize(cfx);
CFX zfx = new ZFX();
zfx.setWidth(20);
test(zfx);
//程序一直执行
resize(zfx);
}
public static void test(CFX cfx){
System.out.println("高:"+cfx.getHeight());
System.out.println("宽:"+cfx.getWidth());
}
//当长方形的宽高小于等于高的时候,宽就会递增
//对于正方形就是死循坏
//这时候便不符合适合子类这个规则
public static void resize(CFX cfx){
while(cfx.getHeight()<=cfx.getWidth()){
cfx.setHeight(cfx.getHeight()+1);
test(cfx);
}
}
}
4、依赖倒转原则
传统过程
传统的过程式设计倾向于使高层次的模块依赖于低层次的模块,抽象层依赖于具体的层次。例如上图中的,高级业务逻辑依赖中层模块,中层模块依赖于底层模块。
依赖倒转
实现层来依赖抽象层,抽象不应该依赖于细节,细节应该依赖于抽象,高层模块不依赖底层模块,两者都依赖抽象。
一、示例:组装电脑
1.1 MainBoard
//主板的抽象类
public abstract class MainBoard {
public abstract void doSomething();
}
1.2 Memory
//内存的抽象类
public abstract class Memory {
public abstract void doSomething();
}
1.3 HardDisk
//硬盘的抽象类
public abstract class HardDisk {
public abstract void doSomething();
}
1.4 HuaSuoMainBoard
public class HuaSuoMainBoard extends MainBoard {
public void doSomething() {
System.out.println("华硕的主板");
}
}
1.5 WeiXingMainBoard
public class WeiXingMainBoard extends MainBoard {
public void doSomething() {
System.out.println("微星的主板");
}
}
1.6 JinShiDunMemory
public class JinShiDunMemory extends Memory {
public void doSomething() {
System.out.println("金士顿的内存");
}
}
1.7 JinBangMemory
public class JinBangMemory extends Memory {
public void doSomething() {
System.out.println("金邦的内存");
}
}
1.8 XiJieHardDisk
public class XiJieHardDisk extends HardDisk {
public void doSomething() {
System.out.println("希捷的硬盘");
}
}
1.9 XiShuHardDisk
public class XiShuHardDisk extends HardDisk {
public void doSomething() {
System.out.println("西数的硬盘");
}
}
第一种情况:
违反规则:高层模块依赖了底层模块,电脑是个高层模块,主板、内存、硬盘是底层模块,高层模块属性里面直接引用了底层,例如当主板故障需要更换时,不可以更换成其他品牌的了,只能是华硕
public class Computer {
private HuaSuoMainBoard mainBoard;
private JinShiDunMemory memory;
private XiJieHardDisk disk;
public String display() {
return "Computer{" +
"mainBoard=" + mainBoard +
", memory=" + memory +
", disk=" + disk +
'}';
}
... getter and setter
}
第二种情况:
改造代码,让代码符合高层模块也依赖抽象模块
public class Computer {
private MainBoard mainBoard;
private Memory memory;
private HardDisk hardDisk;
public String display() {
return "Computer{" +
"mainBoard=" + mainBoard +
", memory=" + memory +
", disk=" + hardDisk +
'}';
}
... getter and setter
}
主函数:
public class MainClass {
public static void main(String[] args){
//这里符合里氏代换原则,父类一定适合子类
Computer computer = new Computer();
computer.setMainBoard( new HuaSuoMainBoard() );
computer.setMemory( new JinShiDunMemory() );
computer.setHardDisk( new XiJieHardDisk() );
computer.display();
}
}
二、实现依赖倒转原则
工厂方法模式:https://blog.csdn.net/weixin_43912883/article/details/97378705
模板方法模式:https://blog.csdn.net/weixin_43912883/article/details/101841035
迭代模式:https://blog.csdn.net/weixin_43912883/article/details/101803964
5、接口分离原则
Shubho:今天我们学习"接口分离原则",这是海报:
接口分离原则海报
Farhana:这是什么意思?
Shubho:它的意思是:"客户端不应该被迫依赖于它们不用的接口。"
Farhana:请解释一下。
Shubho:当然,这是解释:
假设你想买个电视机,你有两个选择。一个有很多开关和按钮,它们看起来很混乱,且好像对你来说没必要。另一个只有几个开关和按钮,它们很友好,且适合你使用。假定两个电视机提供同样的功能,你会选哪一个?
Farhana:当然是只有几个开关和按钮的第二个。
Shubho:对,但为什么?
Farhana:因为我不需要那些看起来混乱又对我没用的开关和按钮。
Shubho:以便外部能够知道这些类有哪些可用的功能,客户端代码也能根据接口来设计.现在,如果接口太大,包含很多暴露的方法,在外界看来会很混乱.接口包含太多的方法也使其可用性降低,像这种包含了无用方法的"胖接口"会增加类之间的耦合.你通过接口暴露类的功能,对.同样地,假设你有一些类,
这也引起了其他问题.如果一个类想实现该接口,那么它需要实现所有的方法,尽管有些对它来说可能完全没用.所以说这么做会在系统中引入不必要的复杂度,降低可维护性或鲁棒性.
接口隔离原则确保实现的接口有他们共同的职责,它们是明确的,易理解的,可复用的.
Farhana:你的意思是接口应该仅包含必要的方法,而不该包含其它的.我明白了.
Shubho:非常正确.一起看个例子.
下面是违反接口隔离原则的一个胖接口
注意到IBird接口包含很多鸟类的行为,包括Fly()行为.现在如果一个Bird类(如Ostrich)实现了这个接口,那么它需要实现不必要的Fly()行为(Ostrich不会飞).
Farhana:确实如此。那么这个接口必须拆分了?
Shubho:是的。这个"胖接口"应该拆分未两个不同的接口,IBird和IFlyingBird,IFlyingBird继承自IBird.
这里如果一种鸟不会飞(如Ostrich),那它实现IBird接口。如果一种鸟会飞(如KingFisher),那么它实现IFlyingBird.
Farhana:所以回头看包含了很多开关和按钮的电视机的例子,电视机制造商应该有一个电视机的图纸,开关和按钮都在这个方案里。不论任何时候,当他们向制造一种新款电视机时,如果他们想复用这个图纸,他们将需要在这个方案里添加更多的开关和按钮。那么他们将没法复用这个方案,对吗?
Shubho:对的。
Farhana:如果他们确实需要复用方案,它们应当把电视机的图纸份为更小部分,以便在任何需要造新款电视机的时候复用这点小部分。
Shubho:你理解了。
6、迪米特法则
迪米特法则(Law of Demeter )又叫做最少知识原则,也就是说,一个对象应当对其他对象尽可能少的了解。狭义的解释为,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一类的某一个方法的话,可以通过第三者转发这个调用。
一、和陌生人讲话
这里someone,直接跟stranger进行通信,通过public void play(Stranger stranger)直接跟陌生人打交道,但是迪米特负责规定说两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一类的某一个方法的话,可以通过第三者转发这个调用
1.1 Someone
public class Someone {
public void play(Friend friend){
System.out.println("someone play");
friend.play();
}
public void play(Stranger stranger){
System.out.println("someone play");
stranger.play();
}
}
1.2 Stranger
public class Stranger {
public void play(){
System.out.println("stranger play");
}
}
1.3 Friend
public class Friend {
public void play(){
System.out.println("friend play");
}
}
1.4 MainClass
public class MainClass {
public static void main(String[] args){
Someone derrick = new Someone();
derrick.play(new Friend());
//直接通信
derrick.play(new Stranger());
}
}
二、不要和陌生人讲话
改造代码,让某人跟陌生人不直接交互,通过朋友作为第三者,完成某人与陌生人的通话
2.1 Someone
public class Someone {
private Friend friend;
public void play(Friend friend){
System.out.println("someone play");
friend.play();
}
public Friend getFriend() {
return friend;
}
public void setFriend(Friend friend) {
this.friend = friend;
}
}
2.2 Friend
public class Friend {
public void play(){
System.out.println("friend play");
}
//frind与stranger进行交互
public void playWithStranger(){
Stranger stranger = new Stranger();
stranger.play();
}
}
2.3 Stranger
public class Stranger {
public void play(){
System.out.println("stranger play");
}
}
2.4 MainClass
public class MainClass {
public static void main(String[] args){
Someone derrick = new Someone();
derrick.setFriend(new Friend());
derrick.getFriend().playWithStranger();
}
}
//运行结果
stranger play
三、与依赖倒转原则结合
第二点那里,我们为了某人和陌生人通信,编写了public void playWithStranger()这个方法,这个方法仅仅是为了调用stranger的play方法,这里可以使用依赖倒转原则,让高层与底层都依赖于抽象,让某人与抽象陌生人进行交互。
这里紧密联系的是抽象类,是陌生人抽象类,也提供了扩展性,可以增加陌生人的种类,如StrangerA、StrangerB等。
3.1 Stranger
public abstract class Stranger {
public abstract void play();
}
3.2 StrangerA
public class StrangerA extends Stranger{
public void play() {
System.out.println("Stranger play");
}
}
3.3 Someone
public class Someone {
private Friend friend;
//紧密联系的是抽象类,是陌生人抽象类
private Stranger stranger;
public void play(){
System.out.println("someone play");
friend.play();
stranger.play();
}
public Friend getFriend() {
return friend;
}
public void setFriend(Friend friend) {
this.friend = friend;
}
public Stranger getStranger() {
return stranger;
}
public void setStranger(Stranger stranger) {
this.stranger = stranger;
}
}
3.4 Friend
public class Friend {
public void play(){
System.out.println("friend play");
}
}
3.5 MainClass
public class MainClass {
public static void main(String[] args){
Someone derrick = new Someone();
derrick.setFriend(new Friend());
derrick.setStranger(new StrangerA());
derrick.play();
}
}
7、组合/聚合复用原则
尽量使用合成/聚合达到复用,尽量少用继承。原则: 一个类中有另一个类的对象。
1.概念:
合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)经常又叫做合成复用原则。合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。它的设计原则是:要尽量使用合成/聚合,尽量不要使用继承。
2.合成/聚合解析:
聚合概念:
聚合用来表示“拥有”关系或者整体与部分的关系。代表部分的对象有可能会被多个代表整体的对象所共享,而且不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏,部分的生命周期可以超越整体。例如,Iphone5和IOS,当Iphone5删除后,IOS还能存在,IOS可以被Iphone6引用。
聚合关系UML类图:
代码演示:
namespace TestLibrary.ExtensionsClass
{
class IOS
{
}
class Iphone5
{
private IOS ios;
public Iphone5(IOS ios)
{
this.ios = ios;
}
}
}
合成概念:
合成用来表示一种强得多的“拥有”关系。在一个合成关系里,部分和整体的生命周期是一样的。一个合成的新对象完全拥有对其组成部分的支配权,包括它们的创建和湮灭等。使用程序语言的术语来说,合成而成的新对象对组成部分的内存分配、内存释放有绝对的责任。一个合成关系中的成分对象是不能与另一个合成关系共享的。一个成分对象在同一个时间内只能属于一个合成关系。如果一个合成关系湮灭了,那么所有的成分对象要么自己湮灭所有的成分对象(这种情况较为普遍)要么就得将这一责任交给别人(较为罕见)。例如:水和鱼的关系,当水没了,鱼也不可能独立存在。
合成关系UML类图:
代码演示:
namespace TestLibrary.ExtensionsClass
{
using System;
class Fish
{
public Fish CreateFish()
{
Console.WriteLine("一条小鱼儿");
return new Fish();
}
}
class Water
{
private Fish fish;
public Water()
{
fish = new Fish();
}
public void CreateWater()
{
// 当创建了一个水的地方,那这个地方也得放点鱼进去
fish.CreateFish();
}
}
}
3.模拟场景:
比如说我们先摇到号(这个比较困难)了,需要为自己买一辆车,如果4S店里的车默认的配置都是一样的。那么我们只要买车就会有这些配置,这时使用了继承关系:
不可能所有汽车的配置都是一样的,所以就有SUV和小轿车两种(只列举两种比较热门的车型),并且使用机动车对它们进行聚合使用。这时采用了合成/聚合的原则:
其实整个设计模式就是在讲如何类与类之间的组合/聚合。在一个新的对象里面通过关联关系(包括组合关系和聚合关系)使用一些已有的对象,使之成为新对象的一部分,新对象通过委派调用已有对象的方法达到复用其已有功能的目的。也就是,要尽量使用类的合成复用,尽量不要使用继承。
如果为了复用,便使用继承的方式将两个不相干的类联系在一起,违反里氏代换原则,哪是生搬硬套,忽略了继承了缺点。继承复用破坏数据封装性,将基类的实现细节全部暴露给了派生类,基类的内部细节常常对派生类是透明的,白箱复用;虽然简单,但不安全,不能在程序的运行过程中随便改变;基类的实现发生了改变,派生类的实现也不得不改变;从基类继承而来的派生类是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。
所以:
组合/聚合复用原则可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

浙公网安备 33010602011771号