几乎我们所有的代码都是样板:我们不断重复模式和代码段,却很少改动每个类和项目。那么,到底该如何更有趣、更有效的进行呢?
译者 | 弯月
责编 | Elle
出品 | CSDN(ID:CSDNnews)
虽然很可悲,但我不得不承认:我们编写代码的能力越强,获得的乐趣就越少。我们都知道SOLID原则、不可变性、抽象、组成和可维护的代码。但是,当我们在实际的编程(Java或C#编程)中尝试使用这些原则时却总是觉得有问题。几乎我们所有的代码都是样板:我们不断重复模式和代码段,却很少改动每个类和项目。编程的工作变得如此单调,我们需要输入大量代码才能产生少量的功能。
我发现,当人们抱怨Java的使用(这是如今的流行趋势)时,通常是因为他们开发了一种编写可维护代码的方法,但是Java对此一无所知,而且Java还没有足够的表达能力来简洁地描述这种方法。
在本文中,为了演示这一点,我们将利用Java构建一个玩具应用程序,然后再与一种更有效、更有趣的表达方法进行比较。我们将在文章的末尾比较这两种方法,以证明标题中的数字很准确。
我们将创建一个查询数据库的程序,通过ID获取特定的用户,并将该用户名中的字母数输出到控制台。虽然这个应用程序并没有实用性,但足以表明我的意思。
我们将在文中展示代码示例,但我想强调的是,你不必仔细分析每一行代码,只需大致浏览类的定义并看清代码中的模式。
Java的方法
下面让我们开始。首先我们编写一个Java类:
public class UsernameLetterCountPublisher { }
这一段代码全是模板。我们的应用尚无具体的内容,只有一个名字,但是创建类是一项我们需要反复执行的任务,却无需太多改动,因此我认为这种代码就是样板。
UsernameLetterCountPublisher有一个从在数据库中查找用户的依赖项。我们希望尽可能地降低UsernameLetterCountPublisher类与其依赖项的耦合。这主要是因为如此一来,我们就可以在没有实际数据库的情况下对其进行测试,而且还可以减少将来更改这个类的可能性。我们定义一个接口,并将该接口的实例注入UsernameLetterCountPublisher的构造函数中,如下所示:
public class UsernameLetterCountPublisher { private final UserProvider userProvider; public UsernameLetterCountPublisher(final UserProvider userProvider) { this.userProvider = userProvider; } }
通常,我们都会通过构造函数注入单元的依赖项,所以这段代码也是模板。我们定义的UserProvider如下所示:
public interface UserProvider { User execute(final String id); }
还有User数据类型:
public class User { public final String username; public User(final String username) { this.username = username; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(username, user.username); } @Override public int hashCode() { return Objects.hash(username); } @Override public String toString() { return "User{" + " username=" + username + "}"; } }
你可能会说UserProvider接口中包含了应用程序的特定逻辑,但我认为通过ID查找实体并没有特定于某个应用程序。我认为这里唯一的非样板代码(除命名之外)是User对象拥有的username字段。User类的定义,例如为了保证不可变性它的构造中包含了public final,以及toString、equals和hashCode函数的定义对于所有数据结构都是相同的。
另一种方法: UnitilyLang
接下来,让我们定义一种自己的编程语言,取名为UnitilyLang,该语言的设计旨在为我们提供简洁的编程模式。Java代码与UnitilyLang的实现之间存在一对一的映射,但是后者要简洁得多。这凸显了Java方法中的样板代码。我们可以使用UnitilyLang,仅用30个字符替代Java代码的最后两部分(596个字符):
data User username: String
上述代码中的data表明这个类仅带有public final字段,而且我们可以针对这些字段应用equals、hashCode和toString函数,并通过该类的构造函数进行初始化。User是这个类的名称,username是其唯一的字符串类型的字段。我们将在本文后面介绍为什么我们不需要定义接口。
下面我们需要创建UserProvider接口的实现,这个接口将从DynamoDB(一个AWS的No SQL数据库,你不需要了解太多)中读取数据。
public class DynamoDbUserProvider { private final ConfigProvider configProvider; private final UserTableProvider userTableProvider; private final DynamoItemReader dynamoReader; private final ItemToUserConverter itemToUserConverter; public DynamoDbUserProvider( final ConfigProvider configProvider, final UserTableProvider userTableProvider, final DynamoItemReader dynamoReader, final ItemToUserConverter itemToUserConverter) { this.configProvider = configProvider; this.userTableProvider = userTableProvider; this.dynamoReader = dynamoReader; this.itemToUserConverter = itemToUserConverter; } public User execute(final String id) { final Config config = configProvider.execute(); final DynamoTable dynamoTable = userTableProvider.execute(config); final Item item = dynamoReader.execute(dynamoTable, id); final User user = itemToUserConverter.execute(item); return user; } }
我们称DynamoDbUserProvider单元为工作流单元。工作流单元不需要执行任何操作,只是为了组成其依赖关系的执行。创建抽象时我们需要这样的工作流单元,例如我们可以有一个名为DynamoDbUserProvider的单元,其功能很明显,所以我们无需在意其内部工作原理。
DynamoDbUserProvider单元拥有四个依赖项。为了避免本文涉及过多逻辑,我不打算展示dynamoReader的代码。我也没有展示ConfigProvider依赖的代码。它的实现通常为:读取环境变量或磁盘上的文件,并提供应用程序级的Config,这是另一个数据类。在我们的示例中,Config的代码具体如下:
public class Config { public final DynamoTable userTable; public final String userId; public Config(final DynamoTable userTable, final String userId) { this.userTable = userTable; this.userId = userId; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Config config = (Config) o; return Objects.equals(userTable, config.userTable) && Objects.equals(userId, config.userId); } @Override public int hashCode() { return Objects.hash(userTable, userId); } @Override public String toString() { return "Config{" + " userTable=" + userTable + " userId=" + userId + "}"; } }
它还有一个DynamoTable,我们在查找某个表时需要用到这个字段,这是另一个数据类:
public class DynamoTable { public final String region; public final String tableName; public final String idFieldName; public DynamoTable(final String region, final String tableName, final String idFieldName) { this.region = region; this.tableName = tableName; this.idFieldName = idFieldName; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DynamoTable that = (DynamoTable) o; return Objects.equals(region, that.region) && Objects.equals(tableName, that.tableName) && Objects.equals(idFieldName, that.idFieldName); } @Override public int hashCode() { return Objects.hash(region, tableName, idFieldName); } @Override public String toString() { return "DynamoTable{" + "region='" + region + '/'' + ", tableName='" + tableName + '/'' + ", idFieldName='" + idFieldName + '/'' + '}'; } }
上述DynamoDbUserStore也需要依赖一个单元来提供我们所需的某一段config:
public class UserTableProvider { public DynamoTable execute(final Config config) { return config.userTable; } }
还有一个ItemToUserConverter是为了将AWS DynamoDb SDK的结果(Item)转换成我们的数据类User。
public class ItemToUserConverter { public User execute(final Item item) { String username = item.getString("username"); User user = new User(username); return user; } }
这又是一段样本代码。我们构建的每一个应用都会重复使用ConfigProvider的实现,实际的Config会有所不同,但是创建Config的逻辑不会变,所以我认为这也是样板。DynamoItemReader依赖项也是如此。最后,我们所有的服务类都通过构造函数注入了它们的依赖关系,并作为private final字段保存到了我们所有的项目中。
DynamoDbUserProvider中唯一涉及应用程序特定逻辑的就是命名,及其依赖项组合在一起的顺序,目的是为了通过id获取User。
如果使用UnitilyLang的话,我们可以用(248个字符)替换前面的5段代码(隐藏在ConfigProvider和DynamoItemReader类中的3006个字符在UnitilyLang中都没有必要)。
data DynamoTable region: String tableName: String idFieldName: String data Config table: DynamoTable userId: String pure ItemToUserConverter: Item -> User item -> new User(item.getString("name")) workflow DynamoDbUserStore = 4 . (3 (2 1))
我们可以仿照User对象的定义,将Config和DynamoTable声明为数据类。
要定义ItemToUserConverter,我们只能使用目标编程语言(在本例中为Java)。UnitilyLang的设计目标是抽象、不可变性和组合。除了编写逻辑之外,它都与语言无关。为了定义纯单位,我们需要编写一些代码。
工作流单位
工作流的声明为通过构造函数注入的每个依赖项定义了一个带有private final字段的类。至少也定义了DynamoDbUserProvider的功能。如果本段的后续内容不是特别清晰,那么也请不要担心,因为我的写作能力不足,我们稍后会以更清晰的方式进行说明。下面,我们开始吧……每个数字n对应于第n个依赖项。声明4 . (3 (2 1))定义了这些依赖关系之间的组合方式。我们将第二个依赖项应用于第一个依赖项的结果。然后,再部分地应用第3个依赖关系,并用第4个依赖关系组成结果函数。这种声明函数的方式称为零点风格(zero point style)。
UnitilyLang可以用更少的代码表达相同含义,原因仅仅是因为Java编程语言和库并没有按照最自然的方式表达我们遵循的简洁代码模式。UnitilyLang可能更简洁,但仍然没有按照自然的方式表达DynamoDbUserStore单元的工作,即构成其依赖项的方式。上一段的自然语言描述显然没有解释清楚这一问题。那么,什么才是更自然的方式呢?他们说一图胜千言,所以让我们来画一张DynamoDbUserProvider。
上图就相当于4 . (3 (2 1)),只是更容易理解,我相信你看得懂,所以就不多做解释了。
关于UnitilyLang(和上图)工作流单位的有趣之处在于它们是通用的。它们不会因依赖关系的类型而有所变化。只要在实例化时每个箭头末尾处的类型相同,就可以通过编译。这实际上限制了函数能做的事情,并更容易推理。它们只能使用依赖项执行的结果。这也意味着我们不需要声明接口,任何类型正确的依赖项都可以使用。
Java与UnitilyLang之间、UnitilyLang与上图之间存在一对一的映射。上图远比你想象得更好。与代码相比,人类在图片的理解和推理方面拥有无限的优势。重构也要容易得多,如果你想更改数据流的方式,那么只需擦掉一个箭头,并重新画出指向其他方向的箭头即可!
现在,我们已经编写了足够的代码,本文开头的UsernameLetterCountPublisher已经编写完成。我们需要为它创建execute方法来查询数据库,统计字母数并在屏幕上显示一条消息。
public class UsernameLetterCountPublisher { private final ConfigProvider configProvider; private final UserIdProvider userIdProvider; private final UserProvider userProvider; private final UserMessageCreator userMessageCreator; private final Publisher publisher; public UsernameLetterCountPublisher( final ConfigProvider configProvider, final UserIdProvider userIdProvider, final UserProvider userProvider, final UserMessageCreator userMessageCreator, final Publisher publisher) { this.configProvider = configProvider; this.userIdProvider = userIdProvider; this.userProvider = userProvider; this.userMessageCreator = userMessageCreator; this.publisher = publisher; } public void execute() { final Config config = configProvider.execute(); final String id = userIdProvider.execute(config); final User user = userProvider.execute(id); final String message = userMessageCreator.execute(user); publisher.execute(message); } }
我们快速介绍一下它的依赖项。我们重用ConfigProvider并注入了一个UserIdProvider:
public class UserIdProvider { String getUserId(ApplicationConfig applicationConfig) { return applicationConfig.userId; } }
目的是为了获得某段config,就像我们在加载userTableName时的做法。我们还注入了一个UserMessageCreator类:
public class UserMessageCreator { public String execute(final User user) { String message = String.format("%s has %d letters in his/her name", user.username, user.username.length()); return message; } }
这个单元很纯粹,而且与其父级的命名相关联,因此我并不觉得需要通过更抽象的公共接口来引用它。此外,还有一个Publisher依赖项。这个依赖项并不是很纯粹(会影响到屏幕上的输出),我们想要使用多种实现方式,例如模拟测试,因此我们定义了一个需要注入的接口。
public interface Publisher { void execute(final String x); }
而且还创建了真正的应用实现:
public class ConsolePrinter { public void execute(final String x) { System.out.println(x); } }
我们认为这个通用名称最合适当前的各个类,如果我们选择发布到消息队列(而不是控制台),那么Publisher仍然说得通。接下来,我们可以注入MessageQueuePublisher(而不是ConsolePrinter),同时无需对UsernameLetterCountPublisher类进行任何修改。敏锐的你可能已经注意到,我们没有使用Implements Publisher语句标记ConsolePrinter类,也没有在UserProvider接口的DynamoDbUserProvider实现中加入该操作。我们将在本文结尾处说明为什么不实例化单元的原因。
同样,大多数UsernameLetterCountPublisher代码都是样板。你可以通过比较DynamoDbUserProvider来确认这种重复的模式。我们需要定义如何构成其依赖关系(就像我们处理DynamoDbUserProvider的那样),而且UserMessageCreator和ConsolePrinter中有一些特定于应用程序的逻辑,但这就是所有我们必须定义的内容。
因此,上述5段代码(1545个字符)可以被UnitilyLang的274个字符代替:
pure UserMessageCreator: User -> String user -> String.format("%s has %d letters in his/her name", user.username, user.username.length()) sideeffect ConsolePrinter: String -> () message -> System.out.println(message) workflow UsernameLetterCountPublisher = 5 (4 (3 (2 1)))
之前我们已经见过与UserMessageCreator和UsernameLetterCountPublisher类似的声明。副作用声明类似于纯单元声明,但我们可以得知这会导致副作用。
我们可以通过下图可视化UsernameLetterCountPublisher。
单元测试与集成测试
到此为止,我们编写好了完成任务所需的所有代码。现在,我们需要编写一些测试和一个程序来运行代码。猜猜下一步是什么?没错,更多样板代码。
我们将从纯单元开始,下面是UserMessageCreator的测试代码:
public class UserMessageCreatorTest { /** Variables */ private static final User user; private static final String message; /** Test fixture */ private UserMessageCreator userMessageCreator; static { final String username = "aUsersName"; user = new User(username); message = "aUsersName has 10 letters in his/her name"; } @BeforeEach public void setupTestFixture() { userMessageCreator = new UserMessageCreator(); } @Test public void test1() { assertEquals( message, userMessageCreator.execute(user), "should create a message containing the number of characters in the username"); } }
比较一下我们的另一个纯单元ItemToUserConverter的测试:
public class ItemToUserConverter { /** Variables */ private static final Item item; private static final User user; /** Test fixture */ private UserTableProvider userTableProvider; static { final String username = "aUsersName"; item = new Item().withString("username", username); user = new User(username); } @BeforeEach public void setupTestFixture() { userTableProvider = new UserTableProvider(); } @Test public void test1() { assertEquals( user, userTableProvider.execute(item), "Should convert a Dynamo SDK item to a User entity object."); } }
看到这些模式和代码的重复了吗?更多样板!我们总是会创建一些final static测试数据,然后在每个测试前,我们都会创建一个待测试单元的新实例。最后,测试运行单元的execute方法,并断言结果是否符合我们的期望。UnitilyLang针对该模式进行了优化,我们只需定义需要创建的单元、测试数据和断言即可。因此,上述两段代码(1286个字符)可以替换为(484个字符):
testdata username = "aUsersName"; user = new User(username); message = "aUsersName has 10 letters in his/her name"; unit UserMessageCreator asserts "should create a message containing the number of characters in the username" user -> message testdata username = "aUsersName"; item = new Item().withString("username", username); user = new User(username); unit ItemToUserConverter asserts "Should convert a Dynamo SDK item to a User entity object." item -> user
如何测试依赖关系会导致副作用的单元?例如DynamoDbUserProvider等。这与测试纯单元类似,只不过我们需要模拟会引起副作用的依赖项。
public class DynamoDbUserProviderTest { /** Variables */ private static final Item item; private static final User user; private static final String userId; private static final DynamoTable table; private static final Config config; /** Mocked dependencies */ private ConfigProvider configProvider; private DynamoItemReader dynamoReader; /** Test fixture */ private DynamoDbUserProvider dynamoDbUserProvider; static { final String username = "aUsersName"; item = new Item().withString("username", username); user = new User(username); userId = "aUserId"; table = new DynamoTable("region", "table", "idField"); config = new Config(table, null); } @BeforeEach public void setupTestFixture() { configProvider = mock(ConfigProvider.class); dynamoReader = mock(DynamoItemReader.class); dynamoDbUserProvider = new DynamoDbUserProvider( configProvider, new UserTableProvider(), dynamoReader, new ItemToUserConverter()); } @Test public void test1() { when(configProvider.execute()).thenReturn(config); when(dynamoReader.execute(table, userId)).thenReturn(item); assertEquals( user, dynamoDbUserProvider.execute(userId), "Should return a user created from the Item returned from Dynamo DB."); } }
让我们比较一下UsernameLetterCountPublisher的测试:
public class UsernameLetterCountPublisherTest { /** Variables */ private static final String userId; private static final User user; private static final Config config; private static final String message; /** Mocked dependencies */ private ConfigProvider configProvider; private UserProvider userProvider; private Publisher publisher; /** Test fixture */ private UsernameLetterCountPublisher usernameLetterCountPublisher; static { userId = "aUserId"; user = new User("aUsersName"); config = new Config(null, userId); message = "aUsersName has 10 letters in his/her name"; } @BeforeEach public void setupTestFixture() { configProvider = mock(ConfigProvider.class); userProvider = mock(UserProvider.class); publisher = mock(Publisher.class); usernameLetterCountPublisher = new UsernameLetterCountPublisher( configProvider, new UserIdProvider(), userProvider, new UserMessageCreator(), publisher); } @Test public void test1() { when(configProvider.execute()).thenReturn(config); when(userProvider.execute(userId)).thenReturn(user); usernameLetterCountPublisher.execute(); verify(publisher).execute(message); } }
还 是相同的模式、相同的复制、相同的模板。 只不过现在我们使用模拟创建单元并在测试中设置这些模拟。 我们可以用756个UnitilyLang字符替换上述2605个字符:
testdata username = "aUsersName"; item = new Item().withString("username", username); user = new User(username); userId = "aUserId"; table = new DynamoTable("region", "table", "idField"); config = new Config(table, null); unit DynamoDbUserProvider -> config SubConfigProvider Config usersTable userId -> item ItemToUserConverter asserts "Should return a user created from the Item returned from Dynamo DB." userId -> user testdata userId = "aUserId"; user = new User("aUsersName"); config = new Config(null, userId); message = "aUsersName has 10 letters in his/her name"; unit UsernameLetterCountPublisher -> config SubConfigProvider Config userId userId -> user UserMessageCreator message -> -- No asserts so any dependencies without outputs will be verified
正如我们在本文中所见,UnitilyLang更加简洁,但可能没有那么透明。我们可以通过绘制图片,兼顾两者。下面是我们绘制的DynamoDbUserProviderTest:
以及UsernameLetterCountPublisherTest:
带有单位名称的灰色框表示具体的依赖关系,虚线框表示模拟,标签定义了模拟期望并返回的测试数据。
运行程序
现在,代码已经写好了,而且我们也通过测试验证了代码。我们需要的最后一样东西就是程序。在Java中,我们需要创建一个main函数,在其中创建根单元(及其所有依赖项)并执行它。
public class Main { public static void main(String[] args) { new UsernameLetterCountPublisher( new ConfigProvider(new EnvironmentProvider()), new UserIdProvider(), new UserProvider() { private final DynamoDbUserProvider dynamoDbUserProvider = new DynamoDbUserProvider( new ConfigProvider(new EnvironmentProvider()), new UserTableProvider(), new DynamoItemReader(new DynamoClientProvider()), new ItemToUserConverter()); public User execute(final String id) { dynamoDbUserProvider.execute(id); } }, new UserMessageCreator(), new Publisher() { private final ConsolePrinter consolePrinter = new ConsolePrinter(); public void execute(final String x) { consolePrinter.execute(x); } }) .execute(); } }
我们又一次看到,这段代码与我们为所有其他应用程序编写的代码完全相同。我们可能会使用DI框架来创建单元,但是这种方法对于小型应用程序是完全有效的。此处唯一真正有意思的是,我们创建了接口的匿名实现,例如new UserProvider(),这意味着我们可以使用任何类来实现接口,而无需使用标签implements。例如,它更类似于go或Typescript,这两种语言的对象默认就实现了接口。当然,这意味着代码中存在一些未经测试的逻辑,但是我相信自己(或更重要的是IDE)可以做到这一点。
在UnitilyLang中,我们只需按照如下方式声明主入口点:
main UsernameLetterCountPublisher DynamoDbUserProvider ConfigProvider Config SubConfigProvider Config userTable DynamoItemReader ItemToUserConverter UsernameLetterCountPublisher ConfigProvider Config SubConfigProvider Config userId DynamoDbUserProvider UserMessageCreator ConsolePrinter
或者,我们可以这样画出来:
总结
本文编写代码、构建脚本等的活动就到此为止,因为这些活动只会产生更多的样本。我们在文末附上了完整的UnitilyLang代码。总共2106个字符。而Java代码库的总长度为22084个字符。也就是说,我们只用10%的代码就可以在UnitilyLang中构建相同的项目。
UnitilyLang减少了我们所需编写的代码量,UnitilyLang的图示简化了我们的项目。想一想我们在本文中花费了多少时间来编写Java代码,然后再将这个时间量与绘制几个方框所需的时间进行比较。想一想在了解应用程序的时候,如果我们只需查看上述的一张图片,而非大量的Java代码库,那么该有多么容易。
最后,我想以一个大反转来结束本文:我并没有编写本文所示的任何Java代码,这些代码都是根据UnitilyLang图片生成的,具体做法请参照视频链接:https://youtu.be/b2NrD-e89PU。
UnitilyLang是一款根据图片定义生成代码的应用程序。你来绘制图片,它来生成代码。图片越是直观,就越易于创建和重构代码。我之前说重构Java代码就像在图片中重新绘制箭头一样简单。Unitliy就可以完成这样的操作。如果想提取依赖项,只需画一个框并勾上箭头。
你可以仔细阅读github上(https://github.com/rowland-street/boilerplate-demo)上的生成项目,并将其与下面的UnitilyLang代码进行比较。
-- Units data User username: String data Config table: DynamoTable userId: String pure ItemToUserConverter: Item -> User item -> new User(item.getString("name")) workflow DynamoDbUserStore = 4 . (3 (2 1)) pure UserMessageCreator: User -> String user -> String.format("%s has %d letters in his/her name", user.username, user.username.length()) sideeffect ConsolePrinter: String -> () message -> = System.out.println(message) workflow UsernameLetterCountPublisher = 5 (4 (3 (2 1))) -- Tests testdata username = "aUsersName"; user = new User(username); message = "aUsersName has 10 letters in his/her name"; unit UserMessageCreator asserts "should create a message containing the number of characters in the username" user -> message testdata username = "aUsersName"; item = new Item().withString("username", username); user = new User(username); unit ItemToUserConverter asserts "Should convert a Dynamo SDK item to a User entity object." item -> user testdata username = "aUsersName"; item = new Item().withString("username", username); user = new User(username); userId = "aUserId"; table = new DynamoTable("region", "table", "idField"); config = new Config(table, null); unit DynamoDbUserProvider -> config SubConfigProvider Config usersTable userId -> item ItemToUserConverter asserts "Should return a user created from the Item returned from Dynamo DB." userId -> user testdata userId = "aUserId"; user = new User("aUsersName"); config = new Config(null, userId); message = "aUsersName has 10 letters in his/her name"; unit UsernameLetterCountPublisher -> config SubConfigProvider Config userId userId -> user UserMessageCreator message -> -- App main UsernameLetterCountPublisher DynamoDbUserProvider ConfigProvider Config SubConfigProvider Config userTable DynamoItemReader ItemToUserConverter UsernameLetterCountPublisher ConfigProvider Config SubConfigProvider Config userId DynamoDbUserProvider UserMessageCreator ConsolePrinter
原文:https://www.unitily.com/articles/boilerplate.html
本文为 CSDN 翻译,转载请注明来源出处。
【End】
Python系列学习成长课来了!15年经验专家、CSDN特级讲师亲自授课,还等什么?立即扫码报名学习:
热 文推 荐
☞ 互联网诞生记:风起于青萍之末
☞罗永浩回应“鲨鱼皮技术遭质疑”; 消息称马蜂窝开启裁员; Dart 2.7 发布 | 极客头条
☞ “弃用 Google AMP! ”
☞ 20行 Python 代码爬取王者荣耀全英雄皮肤 | 原力计划
☞ 操作系统兴衰史
☞ 图灵奖得主Bengio:深度学习不会被取代,我想让AI会推理、计划和想象
☞ 我在华为做外包的真实经历
☞ 搞定面试算法系列 | 分治算法三步走
点击阅读原文 ,参与有奖调查!
你点的每个“在看”,我都认真当成了喜欢