面向抽象编程

  1. 1. Preface
  2. 2. Talk is cheap, show me the code
    1. 2.1. 需求
    2. 2.2. 现有代码
    3. 2.3. 修改思路

Preface

说来惭愧,直到近几天才明白了一点面向对象设计的。给我带来启发的是SOLID中的D,它代表Dependency Inversion(依赖反转)。尽管写/背定义很无聊,但我还是想写一下依赖反转的核心

上层模块不应该依赖下层模块,他们都应该依赖抽象

Talk is cheap, show me the code

最近在写一个音乐电台应用,采用服务端、客户端的方式实现。在服务端,用户可以指定一个路径,程序根据这个路径生成播放列表。

需求

服务器是一个一旦开起来就不会轻易关闭的程序,我希望播放列表能够自动刷新。这样当用户添加或删除了某首音乐后不用重启服务器就可以反映变化。考虑到易用性,应该支持由路径直接生成播放列表。歌曲是有封面等其他信息的,要满足这些信息的可定制化,程序也支持由配置文件指定的播放列表。

现有代码

1
2
3
4
5
6
7
8
9
internal class PlaylistManager
{
public IReadonlyList<Song> AllSong => _backLogs.AsReadOnly();
private readonly List<Song> _backLogs;
public PlaylistManager(IEnumerable<Song> backlog)
{
_backLogs = new List<Song>(backlog);
}
}

现有代码中构造函数的IEnumerable\参数指定了播放列表,可是刷新却需要外部的支持:基于路径的刷新和基于配置文件的刷新是完全不一样的。我完全可以把这两个刷新都放到PlaylistManager里,根据生成播放列表的类型决定调用那个版本。这样就把PlaylistManager完全和列表生成的逻辑绑死在一起了,如果以后要在加一个新的生成方式就还要修改PlaylistManager的代码,尽管它跟PlaylistManager并无关系

修改思路

如果我们将生成播放列表这一行为抽象为接口IPlaylistProvider的话,PlaylistManager就可以完全跟这部分逻辑分开了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface IPlaylistProvider
{
IReadonlyList<Song> AllSongs { get; }
void Refresh();
}
internal class PlaylistManager
{
public IReadonlyList<Song> AllSong => _backLogs.AsReadOnly();
private readonly List<Song> _backLogs;
private readonly IPlaylistProvider _provider;
public PlaylistManager(IPlaylistProvider provider)
{
_provider = provider;
_backLogs = new List<Song>(_provider.AllSongs);
}
internal void Refresh()
{
Refresh();
_backLogs.Clear();
_backLogs.AddRange(_provider.AllSongs);
}
}

我们可以分别实现基于路径和配置文件的IPlaylistProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class FileSystemProvider : IPlaylistProvider
{
public FileSystemProvider(string path,bool recursive)
{
if (!Directory.Exists(path))
{
throw new ArgumentException($"{path} is not a directory");
}
Path = path;
Recursive = recursive;
_songs = ReadSongs();
}
static readonly string[] MusicExtension = new[] {".mp3",".wma" };
private List<Song> ReadSongs()
{
var song = new List<Song>();
var dir = new DirectoryInfo(Path);
var files = dir.GetFiles().Where(i => MusicExtension.Contains(i.Extension));
song.AddRange(files.Select(f => new Song()
{
FilePath = f.FullName,
Title = f.Name,
}));
return song;
}
public IReadOnlyList<Song> AllSongs => _songs.AsReadOnly();
public string Path { get; }
public bool Recursive { get; }
private List<Song> _songs;
public void Refresh()
{
_songs = ReadSongs();
}
}
public class JsonPlaylistProvider : IPlaylistProvider
{
public JsonPlaylistProvider(string configPath)
{
_songs = ReadSongs();
ConfigPath = configPath;
if (!File.Exists(ConfigPath))
{
throw new ArgumentException($"Can't find config file at {ConfigPath}");
}
ReadSongs();
}
private List<Song> ReadSongs()
{
var json = File.ReadAllText(ConfigPath);
return JsonConvert.DeserializeObject<List<Song>>(json)??new List<Song>();
}
private List<Song> _songs;
public string ConfigPath { get; }
public IReadOnlyList<Song> AllSongs => _songs.AsReadOnly();
public void Refresh()
{
ReadSongs();
}
}

这个例子中,PlaylistManager不依赖FileSystemProviderJsonPlaylistProvider,它们三者都依赖于IPlaylistProvider这一接口。代码的可读性和可维护性相比于把刷新的逻辑放在PlaylistManager里高了好多

这里体现了依赖反转的原则:

上层模块不应该依赖下层模块,它们都应该依赖与抽象

我在用SOLID重构手头的项目,改的差不多的时候打算写写开发笔记。咕咕咕