真正开发中使用最频繁的模式基本就是【策略】和【工厂】这个两个模式。
按照"国际惯例"先引入些模式的概念和示例。(示例参考Head First,但是力求比它讲的简洁且清晰)
之后在详细讲解优惠券的设计和模式应用。
所有面向对象入门的时候都是以人、动物为示例。讲解什么是【继承】等相关概念。这个是符合直觉的。
但是在实际应用中,继承用到的地方有限,它有它的问题,它是一种【强耦合】方式,一般使用【策略模式】【装饰模式】代替继承。
以鸭子动物设计为例,讲解继承方式存在哪些问题:
所有鸭子都有quack和swim能力,所以超类实现这两个功能。
display是抽象方法,每个子类鸭子自己负责实现自己的display功能。
这样很好的使用了父类继承能【复用】的特性。
(符合直觉的第一想法,而且还是面向对象学习的不错的情况)
有些功能很好界定,有些功能很“尴尬”,例如fly功能。
fly不能加在超类上,因为不是所有鸭子都有fly功能。
如果加在超类上就导致所有的子类都要实现或者继承这个可能不适用的方法。
而且也不是所有鸭子都会quack(例如木头玩具鸭子),那些没有quack的鸭子,同样要实现或继承quack。
想利用继承来达到代码复用的目的有以下问题:
- 同样的display功能代码在子类中重复,代码没有【复用】。
- 这些子类鸭子的display、fly代码是写死的,想运行时候修改很难。
- 由于每个display功能分散在不同的子类鸭子中,很难知道全部的行为。
- 我们修改了父类会导致牵一发而动全身。所有鸭子都受到了影响。同时我们修改某个相同类型display行为的时候,需要每个鸭子去找该相同代码进行修改。
设计升级:
通过接口的形式,让“某些”(而非全部)鸭子类型可飞可叫。
谁有需要谁就去实现相应的接口。
例如:你可以飞你就实现flyable接口,你不能飞,你就什么都不做。
通过接口的形式解决了部分问题,因为不是所有子类鸭子都具有fly,和quack行为。没必要继承或实现自己不适用的功能。
但是代码无法【复用】的问题还是存在。
我们每个子类中都维护了display,quack功能,可能很多子类的功能都是一致的,没有复用起来,修改一类相同行为,要每个类去找,逐个修改。
同时这些代码都散在每个实现类中,不知道全部的行为。
设计思路与原则:
软件项目唯一的共性:【需求不断变化】
我们要做的就是【识别变化】【隔离变化】,每次迭代或者需求变化的时候,修改范围可控,模块之间【松耦合】。
主要最好不要动到那些成熟的已经经过测试和生产验证的代码,尽量遵循【开闭原则】。
是否进行隔离有个【单一职责】原则判断,如果两个模块修改的原因是不同的,彼此的修改不一定牵涉到对方的修改。那他们应该隔离。
所谓隔离即代表,他们代码在不同方法中、或在不同类中、或者不同服务模块中、甚至是不同系统中。
示例中,每个鸭子的fly和quack会随着鸭子的不同而不同。我们建立两组类,一组和fly相关,一组和quack相关。
fly类里面有各种fly的实现方式。例如:用翅膀飞是一个实现类。用火箭飞是另外一个实现类。
这样对于使用翅膀飞的一类鸭子,我想办法把相应的fly类给到它,就实现了fly方法的【复用】和【集中管理】
下面我们要解决的就是如何将这个用翅膀飞的实现类“给到”这个具体的鸭子类。
插播一条概念:
【针对接口编程】
什么是接口?
接口就是约定好的规范、口令、图纸。
就好比,各个地方的人,都听得懂“滚”这个语言接口命令,也有相应的实现。 大家虽然各不相同、想法各异、体能差异。
但是听到你跟他说“滚”,大家都会执行迈腿这个动作,根据人种不同,有的地方人可能迈腿上步揍你,有的地方的人是迈腿跑路。
这种不同人种的不同反应方式,我们称为【多态】。
虽然语言接口相同,都是一个“滚”的语音输入。但是具体实现类不同,反应也是不同的。
例如:电脑主板上有很多接口,这些接口是有明文规定,例如电压、时序、通讯协议、功能等的。
这些就是规范。你按照这个规范走,就能拿到规范定义的结果和返回。
不同的内存厂商都有自己的内存条。他们的内存芯片、板子方案都是不同的,但是他们的插槽是相同的,他们都是实现了内存接口规范。
电脑只要按照内存接口规范,发出同样的指令。任何厂商的内存条都能进行存储操作。
以前经常听说一句话,一流公司定规范,二流公司做产品。
其实规范就是接口,大公司定义实现方案和方案要实现的接口,其他公司根据自己的原材料实现这些接口,这个产品就落地了。
所谓【要针对接口编程,不要针对实现编程】
你学习如何让一个人滚,一定要学习普通话,因为大多数地方的人都能听懂,只不过反应不同。
如果你针对某个特定的人群学习,那你这个技能就限定在少数人上,例如闽南语只有福建那块的人能听懂。
再比如,你这个电脑主板内存接口是针对三星独家的开发的,指令也只有三星认识,其他品牌的内存条甚至都插不上去。
这样的主板谁会买,绑死在三星上,他说涨价你就要掏钱。不然整个电脑都不能运行。
针对接口实现的板子。我可以换同样接口的国产便宜的内存。还是那句“又不是不能用,李姐万岁”。
解释完概念,我们看编程上如何应用。
我们以一个人的一天活动为例子。
class PersonDayAct{ DayAct act = new 码农(); act.dayAct(); act.nightAct(); }
act.dayAct();
act.nightAct();
我们都用的接口方法,都是使用接口在编程。好处是如果我们想打印富二代的一天。
DayAct act = new 富二代(); 只需要修改这一行代码即可。
通过多态,我们就能打印富二代的一天活动。
而且这个new操作,我们能通过稍后的工厂模式代替。如果以后要打印其他人的一天活动。
我们只要新建新的实现类即可。不需要改动以前写好的经过测试的代码。符合【开闭原则】
讲完【面向接口编程】,我们继续讲如何完善鸭子示例。
替代继承的方式就是【组合】,多用组合,少用继承。
“有一个”比“是一个”更好,每一个鸭子都有一个FlyBehavior和一个QuackBehavior,好将飞行和咕咕叫委托给他们处理。
鸭子的行为不是继承来的,而是和“适当”的对象“组合”而来。
组合的好处:
1.将一类行为封装成类
2.运行时动态改变行为。
public abstract class Duck{ FlyBehavior flyBehavior; QuackBehavior quackBehavior; public Duck(){ } public abstract void dispaly(); public void performQuack(){ quackBehavior.quack(); } public void performFly(){ flyBehavior.fly(); } public void swim(){ System.out.println("all ducks float,even decoys"); } }
Duck
public class Bduck extends Duck{ public Bduck(){ quackBehavior = new Quack(); flyBehavior = new FlyWithWings(); } public void setFlyBehavior( FlyBehavior fb){ flyBehavior = fb; } public void setQuackBehavior( QuackBehavior qb){ quackBehavior = qb; } public void display(){ System.out.println("i am Bduck"); } }
Bduck
public class Test{ public static void main(String[] args){ Duck d = new Bduck(); d.performFly(); d.setFlyBehavior(new FlyRocketPowered()); d.performFly(); } }
Test
总结:
策略模式:定义算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
解释:示例中鸭子的飞行就有不同的策略,有的用翅膀飞,有的用火箭飞。
不同的人对于“滚”这个指令也有自己不同的应对策略,有的是跑,有的是上前揍你。
而这些策略是可以【复用】和【统一管理】的。我们通过【组合】的方式,将策略“放入”到类中,运行时可以更换不同策略。
而不是通过继承来获得这个行为。组合比继承更加灵活,和方便。
但是策略模式还留下了一个问题就是,如何“放入”这个策略对象到类中,如果是new对象的形式,这个就和new的那个策略绑定死了。
我们希望的是,在程序运行过程中,通过输入参数的不同,动态组合不同的实现类。从而实现不同的行为。
例如:我们通过优惠券的类型字段获取不同的优惠券实现类。有的是满减,有的是折扣,但是程序不关心这些类型。
他只要将价格计算委托到不同的策略上计算出最终价格即可。
简单工厂模式:
工厂的职责就是新建产品。
以下单匹萨为例。pizza接口定义了pizza的制作方法。不同种类的pizza负责各自的实现,不同pizza有的烤的时间长,有的切的块小。
以下是典型的面向接口编程,甚至还有点策略模式的味道。
Pizza orderPizza(String type){ Pizza pizza; if(type.equals("cheese")){ pizza = new CheesePizza(); }else if(type.equals("greek")){ pizza = new GreekPizza(); }else if(type.equals("pepperoni")){ pizza = new PepperoniPizza(); } pizza.prepare(); pizza.babke(); pizza.cut(); pizza.box(); return pizza; } |
唯一的问题是,如果我pizza的种类有了增删,我需要修改if-else这块代码。这个就违反了【开闭原则】
我们应该将变化的地方【隔离变化】。
简单工厂:
public class PizzaStore{ SimplePizzaFactory factory; public PizzaStore(SimplePizzaFactory factory){ this.factory = factory; } Pizza orderPizza(String type){ Pizza pizza = factory.createPizza(type); pizza.prepare(); pizza.babke(); pizza.cut(); pizza.box(); return pizza; } }
|
public class SimplePizzaFactory{ public Pizza createPizza(String type){ Pizza pizza; if(type.equals("cheese")){ pizza = new CheesePizza(); }else if(type.equals("greek")){ pizza = new GreekPizza(); }else if(type.equals("pepperoni")){ pizza = new PepperoniPizza(); } return pizza; } } |
simplePizzaFactory就干一件事,就是新建比萨。
对于需要单例的我们可以选用单例模式:
1.单例模式的饿汉式[可用] public class Singleton { private static Singleton instance=new Singleton(); private Singleton(){}; public static Singleton getInstance(){ return instance; } } 访问方式 Singleton instance = Singleton.getInstance(); 2.单例模式懒汉式双重校验锁[推荐用] class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } } 访问方式 Singleton instance = Singleton.getInstance(); 3.内部类[推荐用] public class Singleton{ private Singleton() {}; private static class SingletonHolder{ private static Singleton instance=new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.instance; } } 访问方式 Singleton instance = Singleton.getInstance(); 需要实例化时,调用getInstance方法,才会装载SingletonHolder类,从而完成Singleton的实例化。 4.枚举形式 public enum Singleton { INSTANCE; public void doSomething() { System.out.println("doSomething"); } } 调用方法: public class Main { public static void main(String[] args) { Singleton.INSTANCE.doSomething(); } } 直接通过Singleton.INSTANCE.doSomething()的方式调用即可。方便、简洁又安全。 懒汉式单例
单例实现模式
工厂封装的好处:
- 可能很多地方都需要新建pizza对象。如果有pizza种类增删或改变,我们只需要修改simplePizzaFactory这一个地方。【避免多处修改】,
有时新建对象没一行代码那么简单,比如连接池这种对象,集中管理很重要。 - createPizza方法可以是static的。好处是不需要实例化对象就可以使用,缺点是不能通过继承来改变创建方法的行为。
- 工厂模式让我们实现了【依赖倒置】,以前虽然已经面向接口编程,但是我们始终要new出具体实现类,一旦new出了具体实现类,
虽然是面向接口编程,但是相当于和具体实现绑定死了,运行时无法改变的。
有了工厂,我们高层组建现在只依赖接口或者抽象类,底层实现类也是依赖接口或者抽象类。不依赖具体的实现类。具体实现类可以运行时通过传参由工厂动态产生。
工厂封装的缺点:
- 如果有pizza种类增删或改变,虽然只要修改一处,避免了多处修改。但是还是要修改简单工厂的if-else,还是有违【开闭原则】。
为了遵守【开闭原则】,有两种方式:升级简单工厂、工厂方法模式。
升级简单工厂:
工厂也可以是一个接口或者抽象类,我们工厂也可能有很多种实现方式。
我们先实现了一种AStyleSimplePizzaFactory,如果后续需求变更,pizza种类有添加,我们可以在新建一个BStyleSimplePizzaFactory。
你可以认为这是一种分类方式。例如在中国,豆腐脑厂家。南方和北方都是生产豆腐脑,但是一个甜口一个咸口。
pizza店可以按照风味分类:
|
交通工具也可以通过类型分类: |
其实你也可以不按照这个分类。 but,但是。。。。 你可以认为一期只有AStyleSimplePizzaFactory,随着项目迭代,各种B、C工厂都出来了。 个人以为: 老法师都是想着简洁高效,新手才想着一定要高级有逼格。 |
public interface Moveable { void run(); } public class Car implements Moveable{ @Override public void run() { System.out.println("driving....."); } } public class Plane implements Moveable{ @Override public void run() { System.out.println("flying..."); } } //交通工具工厂 public abstract class VehicleFactory { //具体生成什么交通工具由子类决定,这里是抽象的。 public abstract Moveable create(); } //Car工厂类 public class CarFactory extends VehicleFactory{ @Override public Moveable create() { //单例、多例、条件检查自己控制 return new Car(); } } //飞机工厂类 public class PlaneFactory extends VehicleFactory { @Override public Moveable create() { //单例、多例、条件检查自己控制 return new Plane(); } } public class Test{ public static void main(String[] args){ VehicleFactory factory = new PlaneFactory(); Moveable m = factory.create(); m.run(); //换成Car工厂 factory = new CarFactory(); m = factory.create(); m.run(); } } 交通工具工厂 交通工具工厂 |
工厂方法模式:
|
public abstract class PizzaStore{ public Pizza orderPizza(String type){ Pizza pizza; pizza = createPizza(type); pizza.prepare(); pziza.bake(); pizza.cut(); pizza.box(); return pizza; } abstract Pizza createPizza(String type); } public class AStylePizzaStore extends PizzaStore{ public Pizza createPizza(String type){ if(type.equals("chesse")){ pizza = new AStyleChessePizza(); }else if(type.equals("peperoni")){ pizza = new AStylePepperoniPizza(); } } } 调用的时候即: PizzaStore store = new AStylePizzaStore(); store.orderPizza("cheese");
|
工厂方法模式:
|
工厂方法示例:
|
工厂方法好处:
1.将很多方法和流程固化在父类中,有利于标准化操作,将产品的实现和使用【解耦】。
2.当我们新增产品的时候,或者产品有其他风格和实现时,我们能根据【开闭原则】,新加新的子类即可。
3.工厂方法可以不是抽象的,相当于给了一个默认的实现方式。
工厂方法的缺点:
1.随着业务增长,可能子类越来越多,难于管理(有抽象工厂管理)。
2.无论是简单工厂升级版,还是工厂方法。我们很多时候升级不是非黑即白,用新工厂代替旧工厂那么简单,或者新工厂就旧工厂各管各的,而是两个工厂同时存在。
例如:我原来要做甜豆花,现在有要做咸豆花,但是主体业务逻辑不动。如果是新加一个子类。
我们如何动态的指定工厂呢?在搞一个工厂的工厂吗?突然感觉简单工厂YYDS了。
其实我们还是要分清,这个新的产品添加,是原来的业务逻辑不动,还是原来的业务逻辑代码需要变动。
如果原来的主逻辑代码不动,我们应该需要修改if-else的,因为本质是参数有增加。
如果是拓展的,我们应该是要新建子类,然后拓展新加的代码使用新加的子类。
至于什么时候用接口,什么时候用抽象类:
假如这个概念在我们脑子是确确实实存在的,就用抽象类。或者你有可复用的方法希望子类继承直接用。
假如这个概念只是某些方面的特性:比如会飞的,会跑的,就用接口
假如两个概念模糊的时候,不知道选择哪个的时候,就用接口,原因是java是单继承,多接口实现,这个继承能力很宝贵,从实现了这个接口后,还能从其它的抽象类继承,更灵活。
抽象工厂:
为了控制工厂子类的数量。不必给每一个产品分配一个工厂类。可以将产品分组,每组中的不同产品有同一个工厂类的不同方法来创建。
这个和简单工厂的升级版本很像。但是注意抽象工厂是一个工厂生成不同的东西。是按照系列生产。
我们装备美式装备,里面是含有手枪、大炮等一系列的。
我们装备德式装备,里面又是一套手枪、大炮、汽车等。
//交通工具 public abstract class Vehicle { //实现由子类决定 public abstract void run(); } //食物 public abstract class Food { public abstract void printName(); } //武器 public abstract class Weapon { // public abstract void shoot(); }
产品接口
//抽象工厂 public abstract class AbstractFactory { //生产 交通工具 public abstract Vehicle createVehicle(); //生产 武器 public abstract Weapon createWeapon(); //生产食物 public abstract Food createFood(); } //哈利波特的魔法工厂 public class MagicFactory extends AbstractFactory { //交通工具:扫把 public Vehicle createVehicle(){ return new Broom(); } //武器:魔法棒 public Weapon createWeapon(){ return new MagicStick(); } //食物:毒蘑菇 public Food createFood(){ return new MushRoom(); } } //默认的工厂 public class DefaultFactory extends AbstractFactory{ @Override public Food createFood() { return new Apple(); } @Override public Vehicle createVehicle() { return new Car(); } @Override public Weapon createWeapon() { return new AK47(); } }
工厂
public class Car extends Vehicle{ @Override public void run() { System.out.println("冒着烟奔跑中..."); } } //扫帚 public class Broom extends Vehicle{ @Override public void run() { System.out.println("扫帚摇着尾巴呼呼呼..."); } } //食物:毒蘑菇 public class MushRoom extends Food { @Override public void printName() { System.out.println("mushroom"); } } public class Apple extends Food { @Override public void printName() { System.out.println("apple"); } } public class AK47 extends Weapon{ public void shoot(){ System.out.println("哒哒哒...."); } } //武器:魔法棒 public class MagicStick extends Weapon { @Override public void shoot() { System.out.println("fire hu hu hu ..."); } }
产品
//换一个工厂,只需要改动这一处,就可以了,换一个工厂,就把生产的系列产品都换了 AbstractFactory factory = new DefaultFactory(); //new DefaultFactory(); //换一个工厂 Vehicle vehicle = factory.createVehicle(); vehicle.run(); Weapon weapon = factory.createWeapon(); weapon.shoot(); Food food = factory.createFood(); food.printName();
测试
抽象工厂类图:
抽象工厂允许客户使用抽象接口来创建一组相关的产品,而不需要关心实际产出的具体产品是什么。
这样客户从具体的产品中【解耦】
抽象工厂的createProductA这种方法看起来很像工厂方法。父类定义,子类实现。
总结:
简单工厂:唯一工厂类,一个产品抽象类,工厂类的创建方法依据入参判断并创建具体产品对象。
工厂方法:多个工厂类,一个产品抽象类,利用多态创建不同的产品对象,避免了大量的if-else判断。
抽象工厂:多个工厂类,多个产品抽象类,产品子类分组,同一个工厂实现类创建同组中的不同产品,减少了工厂子类的数量。
实际应用举例:
策略和工厂应用的范围实在太频繁了,不用特别举例子。
以优惠券为例。
优惠券分类型:满减券、折扣券、等等。这些券类型就是决定了算价格的时候如何核销。这就是一个策略。和不同的鸭子怎么飞是一样道理。
同样优惠券还有适用范围。到底适用于那些商品、门店、等等。
优惠券有很多投放,这个投放可能在很多渠道和活动是共享的。例如:A券就投放100张,在主页活动中心、线下扫码同时领取。领完为止。
思路:
优惠券最主要的:优惠方式及计算、有效期方式及计算、适用范围及计算。
将优惠打折方式作为一种策略。组合到优惠券的属性中。就如同鸭子组合了一个飞行的策略。
同理优惠券有效期计算,有的是立即生效,有的是固定时间生效等。
优惠券适用范围目前只有默认方式。
通过简单参数化工厂:
通过券类型code来获取不同打折优惠策略实例,
通过券validity_type获取不同有效期计算的策略实例。
适用范围,目前只有默认计算方式。无须参数化工厂。
气氛都哄到这了,就顺道讲下剩下的两种创建型模式:原型模式、建造者模式。
原型模式:
|
public abstract class Shape implements Cloneable { private String id; protected String type; abstract void draw(); public String getType(){ return type; } public String getId() { return id; } public void setId(String id) { this.id = id; } public Object clone() { Object clone = null; try { // 浅拷贝 clone = super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return clone; } } Shape public class Rectangle extends Shape { public Rectangle(){ type = "Rectangle"; } @Override public void draw() { System.out.println("Inside Rectangle::draw() method."); } } public class Square extends Shape { public Square(){ type = "Square"; } @Override public void draw() { System.out.println("Inside Square::draw() method."); } } public class Circle extends Shape { public Circle(){ type = "Circle"; } @Override public void draw() { System.out.println("Inside Circle::draw() method."); } } ConcreteShape public class ShapeCache { private static Hashtable<String, Shape> shapeMap = new Hashtable<String, Shape>(); public static Shape getShape(String shapeId) { Shape cachedShape = shapeMap.get(shapeId); return (Shape) cachedShape.clone(); } // 对每种形状都运行数据库查询,并创建该形状 // shapeMap.put(shapeKey, shape); // 例如,我们要添加三种形状 public static void loadCache() { Circle circle = new Circle(); circle.setId("1"); shapeMap.put(circle.getId(),circle); Square square = new Square(); square.setId("2"); shapeMap.put(square.getId(),square); Rectangle rectangle = new Rectangle(); rectangle.setId("3"); shapeMap.put(rectangle.getId(),rectangle); } } ShapeCache public class PrototypePatternDemo { public static void main(String[] args) { ShapeCache.loadCache(); Shape clonedShape = (Shape) ShapeCache.getShape("1"); System.out.println("Shape : " + clonedShape.getType()); Shape clonedShape2 = (Shape) ShapeCache.getShape("2"); System.out.println("Shape : " + clonedShape2.getType()); Shape clonedShape3 = (Shape) ShapeCache.getShape("3"); System.out.println("Shape : " + clonedShape3.getType()); } } Test
|
原型模式,顾名思义,给你个原型,你根据原型能获得大量相同或相似的对象,该步骤通过克隆对象完成。
对于高净值,创建过程极其复杂的对象,可以使用这种模式大量建造,不用重新new,那样效率太差。
(1)浅克隆
在浅克隆中,如果原型对象的成员亦量是8大基本数据类型(byte、short、int、long、float、double、char、boolean、除这8种,全部是引用类型,尤其String 底层是字符数组,不是基本数据类型)将复制一份给克降对象,如果原型对象的成员变量是引用类型(如类、接口、数组等复杂数据类型),则将引用对象的地址复制一份给克降对象,也就是说,原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当原型对象被复制时,只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制。
示例:
org.springframework.beans.BeanUtils.copyProperties(source,target);
(2)深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将被复制。
示例:
org.apache.commons.lang3.SerializationUtils.clone(source);
建造者模式:
|
class Product { private String partA; private String partB; private String partC; public void setPartA(String partA) { this.partA = partA; } public void setPartB(String partB) { this.partB = partB; } public void setPartC(String partC) { this.partC = partC; } public void show() { //显示产品的特性 } } Product abstract class Builder { //创建产品对象 protected Product product = new Product(); public abstract void buildPartA(); public abstract void buildPartB(); public abstract void buildPartC(); //返回产品对象 public Product getResult() { return product; } } Builder public class ConcreteBuilder extends Builder { public void buildPartA() { product.setPartA("建造 PartA"); } public void buildPartB() { product.setPartB("建造 PartB"); } public void buildPartC() { product.setPartC("建造 PartC"); } } ConcreteBuilder class Director { private Builder builder; public Director(Builder builder) { this.builder = builder; } //产品构建与组装方法 public Product construct() { builder.buildPartA(); builder.buildPartB(); builder.buildPartC(); return builder.getResult(); } } Director public class Client { public static void main(String[] args) { Builder builder = new ConcreteBuilder(); Director director = new Director(builder); Product product = director.construct(); product.show(); } } Client
|
建造者模式,主要针对对象建造过程复杂,一般由很多子部件按一定步骤组合而成。产品的组成部分是不变的,但是每部分都是可以灵活选择的。
例如:我们攒电脑的时候,都是将各种部件的要求告诉组装店,电脑组成就那些,但是硬盘,cpu可以有很多种,他帮我们组装好电脑(然后就被坑了。。。。)
本文来自博客园,作者:wanglifeng,转载请注明原文链接:https://www.cnblogs.com/wanglifeng717/p/16339222.html