引子
最近股市很火,小帅看着身边的人在股市上赚了不少钱,不免心里痒痒的,天天被各种炒股发财的新闻吸引,终于忍不住杀入股市,结果随着市场的几次大跌,毫不意外地当了韭菜,最终心灰意冷。
幸好小帅身边刚好有个投资做的不错的朋友,某IT公司技术总监老张,最近几年的平均收益都超过了20%,小帅决定向他请教一下投资的诀窍。
“老张啊,听说你最近几年在股市上赚了不少钱,能不能教教我啊,我也想学学炒股。”小帅向老张取经。
老张微微一笑道:”好呀,不过投资太复杂了,你让我教你,一下子也无从下手啊,你先说说你想通过投资实现什么目标呢?“
投资组合
“实不相瞒,我最近炒股亏了不少钱,股票的风险实在太大了。有没有一种投资策略,在任何经济环境下都能保护投资者,让大家获得安全、稳定、长期的回报,同时又非常简单,哪怕最不懂投资的人也能使用呢?”小帅急切地问。
“还真有,有个非常有名的投资者布朗提出了一个叫做‘永久组合’(The Permanent Portfolio)的资产配置方法,刚好解决了这个问题。”
老张继续说道:
永久组合把要投资的资金分成了四等份:25%的股票、25%的国债、25%的现金和25%的黄金。
不过对于你这样的新手,是不适合直接投资个股的,我把股票换成指数基金,国债换成灵活性更高的债券基金,黄金就买纸黄金好了。
我把永久组合改造一下:25%的指数基金、25%的债券基金、25%的现金和25%的纸黄金。
我把投资组合的结构图画出来了,小帅,你不是程序员吗?如果我要遍历每项投资,计算投资的总金额,能不能用代码写出来呢?
遍历文件
小帅说:“这个不难,其实这个结构和我们经常用的文件系统很像,是一种树结构,计算资金总额和遍历文件的操作很像,我们可以先看看如何遍历目录和文件。”
目录分为文件夹和文件,文件夹是分支,文件则是叶子节点,我们可以定义个IBranch表示目录接口,ILeaf表示文件接口。
/**
* 目录接口
*/
public interface IBranch {
/**
* 打印目录名
* @param depth
*/
void print(int depth);
/**
* 添加目录
* @param branch
*/
void add(IBranch branch);
/**
* 添加文件
* @param leaf
*/
void add(ILeaf leaf);
/**
* 获目录内容
* @return
*/
List<Object> getSubList();
}
/**
* 目录实现类
*/
public class Branch implements IBranch{
List<Object> branchList = new ArrayList<Object>();
String name;
public Branch(String name) {
this.name = name;
}
@Override
public void print(int depth) {
System.out.println(String.join("", Collections.nCopies(depth, "--"))
+ name);
}
@Override
public void add(IBranch branch) {
branchList.add(branch);
}
@Override
public void add(ILeaf leaf) {
branchList.add(leaf);
}
@Override
public List getSubList() {
return branchList;
}
}
/**
* 文件接口
*/
public interface ILeaf {
/**
* 打印文件名
* @param depth
*/
void print(int depth);
}
/**
* 文件实现类
*/
public class Leaf implements ILeaf{
String name;
public Leaf(String name) {
this.name = name;
}
@Override
public void print(int depth) {
System.out.println(String.join("", Collections.nCopies(depth, "--"))
+ name);
}
}
构造目录和文件,然后打印:
public class BuildBranch {
public static void main(String[] args) {
IBranch root = new Branch("根目录");
IBranch firstLevelBranch1 = new Branch("1级目录1");
ILeaf firstLevelBranch1File1 = new Leaf("1级目录1-文件1");
ILeaf firstLevelBranch1File2 = new Leaf("1级目录1-文件2");
firstLevelBranch1.add(firstLevelBranch1File1);
firstLevelBranch1.add(firstLevelBranch1File2);
// 把1级目录1添加到根目录
root.add(firstLevelBranch1);
IBranch firstLevelBranch2 = new Branch("1级目录2");
ILeaf firstLevelBranch2File1 = new Leaf("1级目录2-文件1");
firstLevelBranch2.add(firstLevelBranch2File1);
// 把1级目录2添加到根目录
root.add(firstLevelBranch2);
IBranch secondLevelBranch1 = new Branch("2级目录1");
ILeaf secondLevelBranch1File1 = new Leaf("2级目录1-文件1");
ILeaf secondLevelBranch1File2 = new Leaf("2级目录1-文件2");
secondLevelBranch1.add(secondLevelBranch1File1);
secondLevelBranch1.add(secondLevelBranch1File2);
// 把2级目录添加到1级目录
firstLevelBranch1.add(secondLevelBranch1);
root.print(0);
// 遍历所有文件和文件夹
printAll(root.getSubList(), 1);
}
/**
* 遍历所有文件和文件夹
* @param list
* @param depth
*/
public static void printAll(List<Object> list, int depth) {
for(Object object : list) {
if(object instanceof Leaf) {
((Leaf) object).print(depth);
} else {
((Branch) object).print(depth);
printAll(((Branch) object).getSubList(), depth + 2);
}
}
}
}
输出结果:
根目录
--1级目录1
------1级目录1-文件1
------1级目录1-文件2
------2级目录1
----------2级目录1-文件1
----------2级目录1-文件2
--1级目录2
------1级目录2-文件1
“你看,这样就可以了”,小帅面露笑容。
老张一眼就看出了问题:你这里有两个接口IBranch和ILeaf,其实就是把文件夹和文件看做两种不同的东西。客户端遍历的时候需要区分文件夹和文件,要先在程序中判断对象的类型,才能接着进行不同的操作。
其实文件夹和文件是整体和部分的关系,就是树的分支和叶子的关系,我们可不可以把它们看做同一种东西呢?叶子就是没有子节点的分支,不是吗?
老张继续讲:”能不能想办法让客户端不用区分文件夹和文件,让客户端能用同样的方式来操作分支和叶子呢?这样一来,客户端的调用就会简单很多哦。“
小帅说道:“这个,我一下想不出来啊,老张那你快给我讲讲,怎么写更好呢?”
组合模式
”针对这种情况,有种专门的设计模式,叫组合模式,就让我们一起来看看吧。“
组合模式:将一组对象组织成树形结构,以表示“部分 - 整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
也就是说组合模式把单个对象和组合对象看做同一个东西。上面的例子中文件就是单个对象,文件夹就是组合对象。
我们来看一下类图:
遍历文件
我们现在用组合模式来改造一下上面的代码:
/**
* 组件类
*/
public abstract class Component {
/**
* 添加组件
* @param
*/
public void add(Component component) {
throw new UnsupportedOperationException("不支添加操作");
}
/**
* 删除组件
* @param component
*/
public void remove(Component component) {
throw new UnsupportedOperationException("不支删除操作");
}
/**
* 打印名称
* @param depth
*/
public abstract void print(int depth);
}
/**
* 文件类
*/
public class File extends Component{
String name;
public File(String name) {
this.name = name;
}
/**
* 打印名称
* @param depth
*/
@Override
public void print(int depth) {
System.out.println(String.join("", Collections.nCopies(depth, "--"))
+ name);
}
}
/**
* 目录类
*/
public class Directory extends Component{
List<Component> componentList = new ArrayList<Component>();
String name;
public Directory(String name) {
this.name = name;
}
/**
* 添加组件
* @param
*/
@Override
public void add(Component component) {
componentList.add(component);
}
/**
* 删除组件
* @param component
*/
@Override
public void remove(Component component) {
componentList.remove(component);
}
/**
* 打印名称
* @param depth
*/
@Override
public void print(int depth) {
System.out.println(String.join("", Collections.nCopies(depth, "--"))
+ name);
// 递归打印子组件
for (Component component : this.componentList) {
component.print(depth + 2);
}
}
}
public class BuildDirectory {
public static void main(String[] args) {
Directory root = new Directory("root");
Directory booksDirectory = new Directory("books");
File coreJavaFile = new File("Java核心技术");
File codeCompleteFile = new File("代码大全");
booksDirectory.add(coreJavaFile);
booksDirectory.add(codeCompleteFile);
// books文件夹添加到根目录
root.add(booksDirectory);
Directory picturesDirectory = new Directory("pictures");
File pictures1 = new File("照片1");
File pictures2 = new File("照片2");
File pictures3 = new File("照片3");
picturesDirectory.add(pictures1);
picturesDirectory.add(pictures2);
picturesDirectory.add(pictures3);
// 删除照片2
picturesDirectory.remove(pictures2);
// 把pictures文件夹添加到根目录
root.add(picturesDirectory);
Directory networkDirectory = new Directory("network");
File computerNetworkingFile = new File("计算机网络:自顶向下方法");
File tcpIpFile = new File("TCP/IP详解");
networkDirectory.add(computerNetworkingFile);
networkDirectory.add(tcpIpFile);
// 把network文件夹添加到books文件夹
booksDirectory.add(networkDirectory);
root.print(0);
}
}
输出:
root
----books
--------Java核心技术
--------代码大全
--------network
------------计算机网络:自顶向下方法
------------TCP/IP详解
----pictures
--------照片1
--------照片3
”你看,这样看上去是不是简洁很多呢?“老张说道。
小帅想了想说:”简洁是简洁多了,但是我总感觉有点奇怪,add方法和remove方法为什么要直接抛出异常呢?“
老张笑了笑说:你观察很仔细啊,因为add方法和remove方法是文件夹类特有的方法,文件类中是没有这些方法的,但是为了增加客户端使用的透明性,也就是说,为了在客户端眼里显得文件夹和文件都是一样的,就让文件夹类和文件类有了相同的方法。
但是,为了防止客户端误操作,在文件类中操作add和remove方法,所以我们在Component类中声明的时候就直接抛出了异常,这也是对程序的一种保护措施。而文件夹类Directory在自己的类中会重写add和remove方法来实现新增和删除的功能。
小帅好像忽然明白了:”所谓的透明性,就是通过让组件类Component同时包含分支和叶子节点的所有操作,这样客户端就可以把分支和叶子节点看做同一种东西,也就是说一个元素究竟是分支还是叶子节点对客户端来说是透明的。“
老张补充道:”你理解的没错,为了实现’透明性‘,我们就失去了一定的’安全性‘,比如客户端在叶子节点上调用add和remove方法就会抛出异常,但是,如果我们强调’安全性‘就会失去’透明性‘,就像刚开始的例子中我们需要在程序中用instanceof来区分不同的类型。“
老张接着说,”透明性和安全性不能兼得,在前面的例子中我们选择了透明性,放弃了安全性,同理,在投资的时候,低风险和高收益也是不可兼得的,永久组合就是倾向控制风险,为了保住本金,放弃了一点高收益,鱼和熊掌不能兼得嘛。“
”好了,现在你用组合模式来实现下计算永久组合总金额的代码吧。“
”好的,你看我的。“
计算投资组合总金额
/**
* 组件类
*/
public abstract class Component {
/**
* 添加组件
* @param
*/
public void add(Component component) {
throw new UnsupportedOperationException("不支添加操作");
}
/**
* 删除组件
* @param component
*/
public void remove(Component component) {
throw new UnsupportedOperationException("不支删除操作");
}
/**
* 打印名称
* @param depth
*/
public abstract void print(int depth);
/**
* 计算总金额
*/
public abstract int countSum();
}
/**
* 投资项目
*/
public class Project extends Component{
String name;
int price;
public Project(String name, int price) {
this.name = name;
this.price = price;
}
/**
* 打印名称
* @param depth
*/
@Override
public void print(int depth) {
System.out.println(String.join("", Collections.nCopies(depth, "--"))
+ name + " 投资:" + price);
}
/**
* 返回金额
* @return
*/
@Override
public int countSum() {
return price;
}
}
/**
* 投资品类
*/
public class Category extends Component{
List<Component> componentList = new ArrayList<Component>();
String name;
public Category(String name) {
this.name = name;
}
/**
* 添加组件
* @param
*/
@Override
public void add(Component component) {
componentList.add(component);
}
/**
* 删除组件
* @param component
*/
@Override
public void remove(Component component) {
componentList.remove(component);
}
/**
* 打印名称
* @param depth
*/
@Override
public void print(int depth) {
System.out.println(String.join("", Collections.nCopies(depth, "--"))
+ name);
// 递归打印子组件
for (Component component : componentList) {
component.print(depth + 2);
}
}
/**
* 计算总金额
* @return
*/
@Override
public int countSum() {
int sum = 0;
for (Component component : componentList) {
sum += component.countSum();
}
return sum;
}
}
public class BuildProject {
public static void main(String[] args) {
Category root = new Category("永久组合");
Project cash = new Project("现金",25000);
Project paperGold = new Project("纸黄金", 25000);
root.add(cash);
root.add(paperGold);
Category indexFund = new Category("指数基金");
Project hushen300 = new Project("沪深300",10000);
Project zhongzheng500 = new Project("中证500",10000);
Project chaungyeban = new Project("创业板指数",5000);
indexFund.add(hushen300);
indexFund.add(zhongzheng500);
indexFund.add(chaungyeban);
// 指数基金添加到永久组合中
root.add(indexFund);
Category bondFund = new Category("债券基金");
Project hybridBondFund = new Project("混合债券基金", 15000);
Project prueBondFund = new Project("纯债券基金", 10000);
Project corporateonds = new Project("企业债券", 5000);
bondFund.add(hybridBondFund);
bondFund.add(prueBondFund);
bondFund.add(corporateonds);
// 企业债券风险太高,删掉
bondFund.remove(corporateonds);
// 把pictures文件夹添加到根目录
root.add(bondFund);
root.print(0);
System.out.println("投资总金额:" + root.countSum());
}
}
输出结果:
永久组合
----现金 投资:25000
----纸黄金 投资:25000
----指数基金
--------沪深300 投资:10000
--------中证500 投资:10000
--------创业板指数 投资:5000
----债券基金
--------混合债券基金 投资:15000
--------纯债券基金 投资:10000
投资总金额:100000
“很不错嘛!”,老张赞许道。
总结
组合模式的应用场景:
- 如果你需要实现树状对象结构,可以使用组合模式。
- 如果你希望客户端代码以相同方式处理简单和复杂元素,可以使用该模式。
组合模式的优点:
- 使用方便,你可以利用多态和递归机制更方便地使用复杂的树结构。
- 简化了客户端代码,客户端可以一致地使用一个组合结构和单个对象,通常用户不知道(也不关心)处理的是单个对象还是整个组合结构。
- 符合开闭原则,无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分。
组合模式的缺点:
- 对于功能差异较大的类, 提供公共接口会比较困难,会增加客户端理解的难度,客户端很容易会调用错误的操作。
最后
老张拍了拍小帅说道:按照这个投资组合可以很好的控制风险,又可以获得不错的回报,非常适合你这样的小白投资。你也可以根据自己的实际情况稍微调整一下,比如,现在的余额宝其实就是货币基金,风险很小,用起来也很方便,你可以把现金存到余额宝或者其他货币基金中还可以获取一定的收益。
如果你对风险能力承受比较高,可以适当地把指数基金的比例调高,把现金,纸黄金,债券基金的比例调低,投资是种艺术,没有最好的,只有最适合你自己的。
还有,我们最好用定投的方式来买基金,比如每个月买1000块钱,不要一次性全部买进,定投可以很好的平摊风险,不会让你买在最高点,毕竟择时是很难的啊。
”真是谢谢老张啦!今天不仅学到了投资的知识,还顺便学了个设计模式,哈哈!“小帅开心地说道。