kotlin編寫后臺
by Adam Arold
亞當·阿羅德(Adam Arold)
在Kotlin編寫圖書館的提示 (Tips for Writing a Library in Kotlin)
Writing a library in Kotlin seems easy but it can get tricky if you want to support multiple platforms. In this article we’ll explore ways for dealing with this problem.
用Kotlin編寫庫似乎很容易,但是如果要支持多個平臺,可能會變得很棘手。 在本文中,我們將探討解決此問題的方法。
Why would I write a library in Kotlin? you might ask. If you have been using Kotlin for a while now you might think that you have everything within the Java ecosystem and it is not necessary to write anything new in Kotlin (apart from the application you are working on).
我為什么要在Kotlin編寫圖書館? 你可能會問。 如果您已經使用Kotlin一段時間了,您可能會認為您已經在Java生態系統中擁有了一切,并且不需要用Kotlin編寫任何新東西(除了您正在開發的應用程序之外)。
While Kotlin started out as a JVM language, now it can run on multiple platforms and you can even have isomorphic Kotlin in your project. As I have written about this before, Kotlin now can be used with Gradle in place of Groovy, on the frontend and also in your backend projects.
Kotlin最初是一種JVM語言,但現在它可以在多個平臺上運行,甚至在項目中甚至可以包含同構的Kotlin。 正如我之前所寫,Kotlin現在可以在前端和后端項目中與Gradle一起代替Groovy一起使用。
With Kotlin multiplatform projects and Coroutines getting out of beta, we now have everything at our disposal to write production-grade code which is independent of the platform (JVM, Javascript, Native) it runs on.
隨著Kotlin 跨 平臺項目和Coroutines脫離beta版本,我們現在可以隨意編寫生產級代碼,而該代碼與運行平臺無關(JVM,Javascript,Native)。
There is one thing which stands in the way though: the lack of mature multiplatform libraries. So if you want to help with this effort this article is for you.
但是,有一點阻礙:缺少成熟的多平臺庫 。 因此,如果您想為此做些幫助,那么本文適合您。
那么什么是圖書館? (So What is a Library?)
Now that we decided to write a library it is useful to define what a library really is.
現在,我們決定編寫一個庫,定義一個庫真正是有用的。
Note that this is just my opinion, I’m not an authority figure on the topic. Feel free to point out if I missed something or if you just simply disagree, in the comments section.
請注意,這只是我的觀點,我不是該主題的權威人物。 請隨意在評論部分中指出我是否錯過了什么,或者您只是不同意。
In my definition a library is
在我看來,圖書館是
- Generic program code 通用程序代碼
- Written to perform a single task 編寫執行單個任務
- Bundled in a package 捆綁包裝
- And distributed to the public in this form 并以這種形式分發給公眾
Apache Commons IO is a good example for this. We add it to our projects as a dependency, we use its functions, but it doesn’t try to change how we structure our project or write our code.
Apache Commons IO就是一個很好的例子。 我們將其作為依賴項添加到我們的項目中,我們使用其功能,但它不會嘗試更改我們構造項目或編寫代碼的方式。
什么不是圖書館? (What is Not a Library?)
A framework! A good example for this is Ruby on Rails. It is a framework which specifies for you how to code and how to structure your project. In this article we’re going to talk about libraries.
一個框架! Ruby on Rails是一個很好的例子。 它是一個框架,可為您指定如何編碼以及如何構建項目。 在本文中,我們將討論庫 。
注意事項 (Things to Keep in Mind)
Whenever you sit down to write a library there are some general guidelines which are applicable in any domain, not just for Kotlin ones.
每當您坐下來編寫庫時,都有一些通用準則適用于任何領域,而不僅僅是Kotlin準則。
保持API小 (Keep Your API Small)
Giving an API to your users is similar to asking someone to hit a target with an arrow. The smaller the target, the harder it is to hit. This is true with libraries as well. The smaller the API you have, the easier it is to maintain, and you also minimize the chance that you accidentally expose something which you didn’t intend to. Take this API for example:
向用戶提供API類似于要求某人用箭頭擊中目標。 目標越小,擊中的難度就越大。 庫也是如此。 您使用的API越小,維護起來就越容易,并且還最大程度地減少了意外暴露您不想要的內容的機會。 以這個API為例:
In the following examples we’re going to work on an imaginary library which exposes an API for handling UI widgets.
在以下示例中,我們將在一個虛構的庫上工作,該庫公開了用于處理UI小部件的API。
Here we expose the MyComponent
class which has a property to tell whether it is focused, it exposes its children, and also the draw surface which is used for rendering.
在這里,我們公開MyComponent
類,該類具有一個屬性來告訴它是否聚焦,它公開其子級以及用于渲染的繪制表面。
There are a lot of cases when you might have the feeling that you need to expose things because you think that it might be useful for you users. What actually happens most of the time is that you expose too much, your users start to rely on them, and later when you figure out that some of the internals of your library need to be refactored you have to break the API for your users if you want to fix it.
在很多情況下,您可能會覺得有必要公開事物,因為您認為它可能對用戶有用。 實際上,大多數情況下實際發生的情況是您暴露了太多信息,用戶開始依賴它們,后來當您發現需要重構庫的某些內部結構時,如果出現以下情況,則必須破壞用戶的API:您想修復它。
Let’s take a look at the same class with a smaller API:
讓我們看一下具有較小API的同一類:
So after evaluating our class it turns out that
因此,在評估了我們的課程后,事實證明
isFocused
was not needed at all and we only want our users to be able to eitherclear
orrequest
the focus. With this we preserved the same functionality but retained the ability to handle focus in any way we wish.完全不需要
isFocused
,我們只希望我們的用戶能夠clear
或request
焦點。 這樣,我們保留了相同的功能,但保留了以我們希望的任何方式處理焦點的功能。drawSurface
is something which we take as a dependency but it is internal to our library so we shouldn’t allow the external world to tamper with it. It happens quite often that users start to use things which you exposed in ways you didn’t intend it to work so this helps with that problem.drawSurface
是我們依賴的東西,但它在我們庫的內部,因此我們不應該允許外部世界對其進行篡改。 用戶經常以您不希望其工作的方式開始使用您公開的內容,因此可以解決該問題。It also turns out that
render
andattachTo
had the same problem asdrawSurface
: they are internal to the library and the users shouldn’t do rendering or component (re)attaching by hand.這也證明,
render
和attachTo
有同樣的問題,因為drawSurface
:他們是內部的庫和用戶不應該做渲染或成分(Re)手工安裝。
Let’s take this a step further by introducing better abstractions.
通過引入更好的抽象,讓我們更進一步。
保持API的抽象和整潔 (Keep Your API Abstract and Clean)
In the previous example we’ve cleaned up parts of our API and removed / hid some things which were not intended to be public. Now we’ll take a look at things we do expose:
在前面的示例中,我們清理了API的一部分,并刪除/隱藏了一些不希望公開的內容。 現在,我們來看一下我們公開的內容 :
After careful investigation we concluded that:
經過仔細調查,我們得出以下結論:
We don’t really need the functionality
List
s provide for ourchildren
and anIterable
will suffice. A good example for this is that if you expose aList
of something which your users just want to iterate over with a for loop, you either lose the ability to construct items on the fly or make it much harder to implement. With anIterable
or aSequence
you can do this easily. It also enables you to return an insanely high number of elements without filling up the memory.我們實際上并不需要
List
為children
提供的功能,并且Iterable
就足夠了。 一個很好的例子是,如果您公開了用戶僅想通過for循環進行迭代的內容的List
,則可能會失去即時構造項目的能力,或者使其難以實施。 使用Iterable
或Sequence
您可以輕松地做到這一點。 它還使您可以返回大量的元素,而無需占用內存。As it turns out
PixelGraphicsImpl
is a concrete implementation ofDrawSurface
and it has some internal things which we don’t want to expose. It can become problematic if we expose implementation classes through the API and make it impossible to change the implementation behind the scenes without breaking your users’ code.事實證明,
PixelGraphicsImpl
是DrawSurface
的具體實現,它具有一些我們不想公開的內部功能。 如果我們通過API公開實現類,并且在不破壞用戶代碼的情況下無法在后臺更改實現,則可能會引起問題。
All of these lead us to the realization that to clean up this mess we should start to…
所有這些使我們認識到,要清理這一混亂局面,我們應該開始……
使用界面 (Use Interfaces)
By taking a hard look at what we have, we can conclude that by exposing classes and concrete implementations through our API will lead to all sorts of problems, so using interfaces is a better approach overall:
通過仔細研究我們所擁有的東西,我們可以得出結論,通過我們的API公開類和具體實現會導致各種問題,因此使用接口總體上是一種更好的方法:
This way we are free to implement Component
as we see fit. We can have any number of implementations for it if we want and it won’t affect our users. An important caveat for this is to only return abstract types from our factories:
這樣,我們可以自由地實現我們認為合適的Component
。 如果需要,我們可以有多種實現方式,并且不會影響用戶。 重要的警告是僅從工廠返回抽象類型:
This might seem obvious, but this is often overlooked.
這看起來似乎很明顯,但是卻經常被忽略。
模塊化問題 (The Modularization Problem)
So we separated our API and our concrete implementations into interfaces and classes. The problem is that we can’t prevent our users from circumventing our clean API and using MyComponent
directly since Kotlin doesn’t have its own module system. What we can do is to separate our packages into api
and internal
(or anything similar) and clearly state in the documentation that everything in internal
is subject to change:
因此,我們將API和具體的實現分為接口和類。 問題在于,由于Kotlin沒有自己的模塊系統,我們不能阻止用戶繞過我們的干凈API并直接使用MyComponent
。 我們可以做的是將我們的程序包分為api
和internal
(或類似的東西),并在文檔中明確指出internal
所有內容都可能發生變化:
This solution is not perfect, but it helps.
該解決方案不是完美的,但可以幫助您。
Kotlin提示 (Kotlin Tips)
We’ve discussed a lot of things already, but we haven’t seen any Kotlin-specific tips yet, so let’s take a look at some.
我們已經討論了很多事情,但是還沒有看到任何Kotlin特有的技巧,因此讓我們來看一些。
添加companion object
(Add a companion object
)
It might be a case that you don’t use companion object
s in your project or you don’t have the need for them in some API classes. What’s important to point out here is that companion object
s enable your users to define extension functions on your classes which can be invoked without an instance. You can add an empty companion object
:
在某些情況下,您可能不會在項目中使用companion object
,或者在某些API類中不需要它們。 這里要指出的重要一點是, companion object
使用戶可以在類上定義擴展函數,而無需實例即可調用它們。 您可以添加一個空的companion object
:
and your users gain the ability to augment your interface as they see fit:
并且您的用戶可以根據自己的需要擴展您的界面:
將擴展功能更上一層樓 (Take Extension Functions to the Next Level)
Extension functions can also help you to create a more fluent API. Take a look at this example, where our user has a list of Subscription
s:
擴展功能還可以幫助您創建更流暢的API。 看一下這個例子,我們的用戶有一個Subscription
的列表:
In order to cancel them all they most probably write something like this:
為了取消它們,他們最有可能寫這樣的東西:
But what if we provide this functionality out of the box?
但是,如果我們提供現成的功能呢?
This way cancelAll
can be called on any MutableList
which holds Subscription
s:
這樣,可以在任何包含Subscription
的MutableList
上調用cancelAll
:
有reified
功能委派工作 (Have reified
Functions Delegate Work)
reified
functions are very useful but they come with some caveats which are very frustrating. One of them is that we need to use @PublishedApi
if we want to access the internals of a class. For this reason it helps greatly if we simply delegate the work from them to functions which take KClass
objects as parameters so we get the utility of reified
functions without the problems:
reified
功能非常有用,但附帶一些警告,令人非常沮喪。 其中之一是,如果要訪問類的內部,則需要使用@PublishedApi
。 出于這個原因,它可以幫助極大,如果我們簡單地從委托他們的工作,這需要功能KClass
對象作為參數,所以我們得到的效用reified
功能,而問題:
Astute readers might spot the problem with this API. We’re not using interfaces! Unfortunately interface
s don’t support reified
functions, but there is a solution which solves this problem:
精明的讀者可能會發現此API的問題。 我們沒有使用接口 ! 不幸的是, interface
不支持reified
功能,但是有一個解決方案可以解決此問題:
讓reified
功能為擴展功能 (Let reified
Functions be Extension Functions)
It is true that we can’t have reified
functions in an interface
:
的確,我們不能在interface
使用reified
函reified
功能:
but we can have reified
extension functions:
但是我們可以使用reified
擴展功能:
https://gist.github.com/adam-arold/19ade46f1bce1f4d58cc5ac63e230885
https://gist.github.com/adam-arold/19ade46f1bce1f4d58cc5ac63e230885
With this we get the best of both worlds, and usage stays the same:
這樣,我們可以兼得兩全其美,使用率保持不變:
The tips above are applicable on any Kotlin project but there is a special kind of project which needs more care than a regular one:
上面的提示適用于任何Kotlin項目,但是有一種特殊的項目需要比常規項目更多的注意:
多平臺圖書館 (Multiplatform Libraries)
If you are working on a multiplatform library you need to write code which is idiomatic on all platforms. In the following section we’ll take a look at some tips which will help with this.
如果您正在使用多平臺庫,則需要編寫在所有平臺上都是慣用的代碼。 在下一節中,我們將介紹一些有助于此操作的技巧。
使用屬性代替吸氣劑 (Use Properties Instead of Getters)
Writing a getter (getX
) for a property is not idiomatic in Kotlin. On the other hand accessing fields in Java without getters is not idiomatic either! It turns out that Kotlin properties are implemented in a way that both sides will see an API they wish to see:
在Kotlin中,為屬性編寫getter( getX
)并不是習慣做法。 另一方面,不使用getter來訪問Java中的字段也不是習慣! 事實證明,Kotlin屬性是以雙方都能看到他們希望看到的API的方式實現的:
隱藏Kotlin API (Hiding a Kotlin API)
Sometimes you have functions which look weird for Java users. A good example for this is a lambda which has to return Unit
. Having to return Unit
for Java users is just weird. Luckily we have some ways to hide things from Java users:
有時,您所擁有的功能對于Java用戶而言似乎很奇怪。 一個很好的例子是必須返回Unit
的lambda。 必須為Java用戶返回Unit
太奇怪了。 幸運的是,我們有一些方法可以向Java用戶隱藏事物:
This is nice but what if I want to…
很好,但是如果我想...
隱藏Java API (Hide a Java API)
Unfortunately there is no “official” way of hiding something from Kotlin users, but there is a hack which we can use:
不幸的是,沒有向Kotlin用戶隱藏某些東西的“官方”方法,但是我們可以使用一種技巧:
internal
functions are not visible for Kotlin users, but it is visible from Java. There are some caveats though:
internal
功能對Kotlin用戶不可見,但是從Java中可見。 但是有一些警告:
This is a hack!
這是駭客!
Interfaces can’t have
internal
members接口不能有
internal
成員We need to use
@JvmName
becauseinternal
functions have a funky name when we try to access them from Java我們需要使用
@JvmName
因為當我們嘗試從Java訪問internal
函數時,internal
函數的名稱很時髦
可擴展性 (Extensibility)
If you work on a library chances are that you want to design it for extensibility so your users can add their custom things. Take this interface for example, which we want to make extensible:
如果您使用的是庫,則可能要設計其可擴展性,以便用戶可以添加其自定義內容。 以這個接口為例,我們要使其具有可擴展性:
The problem here is that from the Java side calculateArea
won’t have a default implementation only if we apply @JvmDefault
to it. The problem is that this will only work with Java 8+ which might not be available (on Android for example).
這里的問題是,從Java角度@JvmDefault
,僅當我們將@JvmDefault
應用于它時, calculateArea
才會具有默認實現。 問題在于,這僅適用于可能不可用的Java 8+(例如,在Android上)。
So what we can do is to create base classes.
因此,我們可以做的是創建基類 。
If we want a base class which doesn’t implement all members we can provide abstract
classes:
如果我們想要一個不能實現所有成員的基類,則可以提供abstract
類:
If they do, an open
class will do:
如果他們這樣做,則open
課將這樣做:
Just keep those functions final
which you don’t want your users to override.
只要保持這些功能final
,你不想讓你的用戶覆蓋。
多平臺注釋 (Multiplatform Annotations)
Kotlin comes with some annotations which were designed to help with multiplatform development. One of them is @JvmStatic
which we can use to make members static
in the resulting Java bytecode, but it comes with some caveats:
Kotlin附帶了一些注釋,這些注釋旨在幫助進行多平臺開發。 其中之一是@JvmStatic
,我們可以使用它來使成員在生成的Java字節碼中變為static
,但有一些警告:
Note that in the past this was not usable in common projects but they were modified to be optional so we can now put them on any class regardless of the platform.
請注意,過去這在普通項目中 不可用, 但是將它們修改為可選的, 因此我們現在可以將它們放在任何類上,而與平臺無關。
One of those problems is that we can’t use it in interface
s, not even on companion objects
defined in them.
這些問題之一是我們不能在interface
使用它,甚至不能在它們中定義的companion objects
上使用它。
A solution for this is to use object
s and have them delegate to the functions defined in an interface
companion object
:
一種解決方案是使用object
,并將它們委托給interface
companion object
定義的功能:
多平臺SAM問題 (Multiplatform SAM Problem)
Suppose that we have an interface
which has a function which takes a lambda:
假設我們有一個interface
,該interface
具有一個接受lambda的函數:
If we want to use this from the Java side it is awkward:
如果要從Java方面使用它,則很尷尬:
If we create a Listener
interface to be used as a parameter:
如果我們創建一個Listener
接口用作參數:
@FunctionalInterface would help here but we can’t use it in multiplatform common projects.
@FunctionalInterface在這里會有所幫助,但我們不能在多平臺通用項目中使用它。
we won’t be able to use Kotlin lambdas here:
我們將無法在此處使用Kotlin lambda:
A solution for this problem is to keep the Listener
interface and provide Kotlin users with an extension function which accepts a lambda:
解決此問題的方法是保留Listener
界面,并為Kotlin用戶提供接受lambda的擴展功能:
This way it will be idiomatic from both Java and Kotlin.
這樣,Java和Kotlin都會習慣使用它。
如何部署? (How to Deploy?)
So we now have a nice library which is idiomatic, easy to maintain and behaves well in multiplatform environments. The question is how to deploy it? As of the time of writing, Maven Central and Bintray is hard to set up and the latter is not reliable. So what do we do?
因此,我們現在有了一個不錯的庫,該庫是慣用的,易于維護的并且在多平臺環境中表現良好。 問題是如何部署它? 在撰寫本文時,Maven Central和Bintray很難設置,而后者并不可靠 。 那么我們該怎么辦?
As it turns out there is a free service which works out of the box and deployment is as easy as creating a tag on GitHub: JitPack.
事實證明,有一項免費服務可以直接使用,并且部署就像在GitHub上創建標簽一樣簡單: JitPack 。
My suggestion is to use this until official tooling arrives for Maven Central releases.
我的建議是使用此工具,直到Maven Central版本的官方工具到來為止。
Note that I’m not affiliated with JitPack in any way.
請注意,我不以任何方式隸屬于JitPack。
結論 (Conclusion)
We’ve explored some of the intricacies of library development with Kotlin. It might seem hard to do at first, but by following some simple guidelines it can become much easier with some practice. So armed with this knowledge…
我們探索了Kotlin開發圖書館的一些復雜之處。 起初似乎很難做到,但是通過遵循一些簡單的準則,可以通過一些實踐變得更加容易。 所以有了這些知識...
Let’s go forth and kode on!
讓我們去和KODE上 !
Thanks for reading! You can read more of my articles on my blog.
謝謝閱讀! 您可以在我的博客上我的文章。
翻譯自: https://www.freecodecamp.org/news/tips-for-writing-a-library-in-kotlin-cd5f9e14e102/
kotlin編寫后臺