几乎我们所有的代码都是样板:我们不断重复模式和代码段,却很少改动每个类和项目。那么,到底该如何更有趣、更有效的进行呢?
以下为译文:
虽然很可悲,但我不得不承认:我们编写代码的能力越强,获得的乐趣就越少。我们都知道SOLID原则、不可变性、抽象、组成和可维护的代码。但是,当我们在实际的编程(Java或C#编程)中尝试使用这些原则时却总是觉得有问题。几乎我们所有的代码都是样板:我们不断重复模式和代码段,却很少改动每个类和项目。编程的工作变得如此单调,我们需要输入大量代码才能产生少量的功能。
我发现,当人们抱怨Java的使用(这是如今的流行趋势)时,通常是因为他们开发了一种编写可维护代码的方法,但是Java对此一无所知,而且Java还没有足够的表达能力来简洁地描述这种方法。
在本文中,为了演示这一点,我们将利用Java构建一个玩具应用程序,然后再与一种更有效、更有趣的表达方法进行比较。我们将在文章的末尾比较这两种方法,以证明标题中的数字很准确。
我们将创建一个查询数据库的程序,通过ID获取特定的用户,并将该用户名中的字母数输出到控制台。虽然这个应用程序并没有实用性,但足以表明我的意思。
我们将在文中展示代码示例,但我想强调的是,你不必仔细分析每一行代码,只需大致浏览类的定义并看清代码中的模式。
下面让我们开始。首先我们编写一个Java类:
public class UsernameLetterCountPublisher {
}
UsernameLetterCountPublisher有一个从在数据库中查找用户的依赖项。我们希望尽可能地降低UsernameLetterCountPublisher类与其依赖项的耦合。这主要是因为如此一来,我们就可以在没有实际数据库的情况下对其进行测试,而且还可以减少将来更改这个类的可能性。我们定义一个接口,并将该接口的实例注入UsernameLetterCountPublisher的构造函数中,如下所示:
public class UsernameLetterCountPublisher {
private final UserProvider userProvider;
public UsernameLetterCountPublisher(final UserProvider userProvider) {
this.userProvider = userProvider;
}
}
public interface UserProvider {
User execute(final String id);
}
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 + "}";
}
}
接下来,让我们定义一种自己的编程语言,取名为UnitilyLang,该语言的设计旨在为我们提供简洁的编程模式。Java代码与UnitilyLang的实现之间存在一对一的映射,但是后者要简洁得多。这凸显了Java方法中的样板代码。我们可以使用UnitilyLang,仅用30个字符替代Java代码的最后两部分(596个字符):
data User
username: String
下面我们需要创建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单元拥有四个依赖项。为了避免本文涉及过多逻辑,我不打算展示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 + "}";
}
}
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
+ '\''
+ '}';
}
}
public class UserTableProvider {
public DynamoTable execute(final Config config) {
return config.userTable;
}
}
public class ItemToUserConverter {
public User execute(final Item item) {
String username = item.getString("username");
User user = new User(username);
return user;
}
}
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))
要定义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);
}
}
public class UserIdProvider {
String getUserId(ApplicationConfig applicationConfig) {
return applicationConfig.userId;
}
}
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;
}
}
public interface Publisher {
void execute(final String x);
}
public class ConsolePrinter {
public void execute(final String x) {
System.out.println(x);
}
}
同样,大多数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)))
我们可以通过下图可视化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");
}
}
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.");
}
}
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
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.");
}
}
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);
}
}
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
以及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();
}
}
在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代码进行比较。
完整的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
【End】
Python系列学习成长课来了!15年经验专家、CSDN特级讲师亲自授课,还等什么?立即扫码报名学习:
热 文 推 荐
☞图灵奖得主Bengio:深度学习不会被取代,我想让AI会推理、计划和想象
点击阅读原文,参与有奖调查!